From 62e5be1ed25e7135d531afd8ebddf8f8bae3967a Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 21 Jun 2017 17:49:35 +0200 Subject: [PATCH] Implemented issue #707 - Edge looses text formatting on reply (may also on other browsers). --- .../ticket_zoom/article_actions.coffee | 24 ++-- .../app/lib/app_post/clipboard.coffee | 80 ++++++----- .../javascripts/app/lib/app_post/utils.coffee | 130 +++++++++++++++--- public/assets/tests/html_utils.js | 29 ++++ 4 files changed, 200 insertions(+), 63 deletions(-) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee index c1d50f842..6b60eb01e 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee @@ -391,19 +391,19 @@ class App.TicketZoomArticleActions extends App.Controller body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' # check if quote need to be added - selectedText = App.ClipBoard.getSelected() - if selectedText + selected = App.ClipBoard.getSelected('html') + if selected + selected = App.Utils.htmlCleanup(selected).html() + if !selected + selected = App.ClipBoard.getSelected('text') + if selected + selected = App.Utils.textCleanup(selected) + selected = App.Utils.text2html(selected) + if selected + selected = "


#{selected}

" - # clean selection - selectedText = App.Utils.textCleanup(selectedText) - - # convert to html - selectedText = App.Utils.text2html(selectedText) - if selectedText - selectedText = "


#{selectedText}

" - - # add selected text to body - body = selectedText + body + # add selected text to body + body = selected + body articleNew.body = body diff --git a/app/assets/javascripts/app/lib/app_post/clipboard.coffee b/app/assets/javascripts/app/lib/app_post/clipboard.coffee index 47d2a2678..03dd63b63 100644 --- a/app/assets/javascripts/app/lib/app_post/clipboard.coffee +++ b/app/assets/javascripts/app/lib/app_post/clipboard.coffee @@ -6,25 +6,25 @@ class App.ClipBoard _instance ?= new _Singleton _instance.bind(el) - @getSelected: -> + @getSelected: (type) -> if _instance == undefined _instance ?= new _Singleton - _instance.getSelected() + _instance.getSelected(type) - @getSelectedLast: -> + @getSelectedLast: (type) -> if _instance == undefined _instance ?= new _Singleton - _instance.getSelectedLast() + _instance.getSelectedLast(type) @getPosition: (el) -> if _instance == undefined _instance ?= new _Singleton _instance.getPosition(el) - @setPosition: ( el, pos ) -> + @setPosition: (el, pos) -> if _instance == undefined _instance ?= new _Singleton - _instance.setPosition( el, pos ) + _instance.setPosition(el, pos) @keycode: (code) -> if _instance == undefined @@ -33,54 +33,68 @@ class App.ClipBoard class _Singleton constructor: -> - @selection = '' - @selectionLast = '' + @selection = + html: '' + text: '' + @selectionLast = + html: '' + text: '' # bind to fill selected text into bind: (el) -> - $(el).bind('mouseup', => - # check selection on mouse up - @selection = @_getSelected() - if @selection - @selectionLast = @selection + # check selection on mouse up + $(el).bind('mouseup', => + @_updateSelection() ) $(el).bind('keyup', (e) => # check selection on sonder key if e.keyCode == 91 - @selection = @_getSelected() - if @selection - @selectionLast = @selection + @_updateSelection() # check selection of arrow keys if e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40 - @selection = @_getSelected() - if @selection - @selectionLast = @selection + @_updateSelection() ) + _updateSelection: => + for key in ['html', 'text'] + @selection[key] = @_getSelected(key) + if @selection[key] + @selectionLast[key] = @selection[key] + # get cross browser selected string - _getSelected: -> + _getSelected: (type) -> text = '' + html = '' if window.getSelection - text = window.getSelection() + sel = window.getSelection() + text = sel.toString() else if document.getSelection - text = document.getSelection() + sel = document.getSelection() + text = sel.toString() else if document.selection - text = document.selection.createRange().text - if text -# text = text.toString().trim() - text = $.trim( text.toString() ) - text + sel = document.selection.createRange() + text = sel.text + if type is 'text' + return $.trim(text.toString()) if text + return '' + + if sel && sel.rangeCount + container = document.createElement('div') + for i in [1..sel.rangeCount] + container.appendChild(sel.getRangeAt(i-1).cloneContents()) + html = container.innerHTML + html # get current selection - getSelected: -> - @selection + getSelected: (type) -> + @selection[type] # get latest selection - getSelectedLast: -> - @selectionLast + getSelectedLast: (type) -> + @selectionLast[type] getPosition: (el) -> pos = 0 @@ -104,13 +118,13 @@ class _Singleton # IE Support if el.setSelectionRange el.focus() - el.setSelectionRange( pos, pos ) + el.setSelectionRange(pos, pos) # Firefox support else if el.createTextRange range = el.createTextRange() range.collapse(true) - range.moveEnd( 'character', pos ) + range.moveEnd('character', pos) range.moveStart('character', pos) range.select() diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index 8be20ad0e..9e1265548 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -1,5 +1,80 @@ # coffeelint: disable=no_unnecessary_double_quotes class App.Utils + @mapAttributes: + 'TABLE': ['align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'sortable', 'summary', 'width', 'style'] + 'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style'] + 'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style'] + 'TR': ['width', 'style'] + + @mapCss: + 'TABLE': [ + 'background', 'background-color', 'color', 'font-size', 'vertical-align', + 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'text-align', + 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', + + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + ] + 'TH': [ + 'background', 'background-color', 'color', 'font-size', 'vertical-align', + 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'text-align', + 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', + + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + ] + 'TR': [ + 'background', 'background-color', 'color', 'font-size', 'vertical-align', + 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'text-align', + 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', + + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + ] + 'TD': [ + 'background', 'background-color', 'color', 'font-size', 'vertical-align', + 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'text-align', + 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', + + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + ] # textCleand = App.Utils.textCleanup(rawText) @textCleanup: (ascii) -> @@ -252,26 +327,45 @@ class App.Utils catch err return $("
#{item}
") + @_removeAttribute: (element) -> + return if !element + + if @mapAttributes[element.nodeName] + atts = element.attributes + for att in atts + if att && att.name && !_.contains(@mapAttributes[element.nodeName], att.name) + element.removeAttributeNode(att) + else + $element = $(element) + $element.removeAttr('style') + $element.removeAttr('class') + $element.removeAttr('title') + $element.removeAttr('lang') + $element.removeAttr('type') + $element.removeAttr('id') + $element.removeAttr('wrap') + $element.removeAttrs(/data-/) + + if @mapCss[element.nodeName] + style = element.getAttributeNode('style') + if style && style.nodeValue && style.nodeValue.split + styleNew = '' + for local_pear in style.nodeValue.split(';') + prop = local_pear.split(':') + if prop[0] && prop[0].trim + key = prop[0].trim() + if _.contains(@mapCss[element.nodeName], key) + styleNew += "#{local_pear};" + if styleNew isnt '' + style.nodeValue = styleNew + element.setAttributeNode(style) + else + element.removeAttributeNode(style) + @_removeAttributes: (html, parent = true) -> if parent - html.find('*') - .removeAttr('style') - .removeAttr('class') - .removeAttr('title') - .removeAttr('lang') - .removeAttr('type') - .removeAttr('id') - .removeAttr('wrap') - .removeAttrs(/data-/) - html - .removeAttr('style') - .removeAttr('class') - .removeAttr('title') - .removeAttr('lang') - .removeAttr('type') - .removeAttr('id') - .removeAttr('wrap') - .removeAttrs(/data-/) + html.each((index, element) => @_removeAttribute(element) ) + html.find('*').each((index, element) => @_removeAttribute(element) ) html @_removeComments: (html) -> diff --git a/public/assets/tests/html_utils.js b/public/assets/tests/html_utils.js index 9569c8a8c..6ae7b9b67 100644 --- a/public/assets/tests/html_utils.js +++ b/public/assets/tests/html_utils.js @@ -582,6 +582,35 @@ test("htmlCleanup", function() { result = App.Utils.htmlCleanup(source) equal(result.html(), should, source) + source = "
aaa
value
" + should = "
aaa
value
" + result = App.Utils.htmlCleanup(source) + equal(result.get(0).outerHTML, should, source) + + source = "
aaa
value
" + should = "
aaa
value
" + result = App.Utils.htmlCleanup(source) + //equal(result.get(0).outerHTML, should, source) / string order is different on browsers + equal(result.first().attr('bgcolor'), 'green') + equal(result.first().attr('style'), 'color: red;') + equal(result.first().attr('aaa'), undefined) + equal(result.find('tr').first().attr('style'), 'margin-top: 10px;') + equal(result.find('th').first().attr('colspan'), '2') + equal(result.find('th').first().attr('abc'), undefined) + equal(result.find('th').first().attr('style'), 'margin-top: 12px;') + + source = "
aaa
value
" + should = "
aaa
value
" + result = App.Utils.htmlCleanup(source) + //equal(result.get(0).outerHTML, should, source) / string order is different on browsers + equal(result.first().attr('bgcolor'), 'green') + equal(result.first().attr('style'), 'color:red;') + equal(result.first().attr('aaa'), undefined) + equal(result.find('tr').first().attr('style'), undefined) + equal(result.find('th').first().attr('colspan'), '2') + equal(result.find('th').first().attr('abc'), undefined) + equal(result.find('th').first().attr('style'), undefined) + }); // wrap