Fixes #3539 - When replying, quote article content only
This commit is contained in:
parent
26db9dbc24
commit
86d65a571b
3 changed files with 190 additions and 13 deletions
|
@ -127,16 +127,14 @@ class EmailReply extends App.Controller
|
||||||
# get current body
|
# get current body
|
||||||
body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
||||||
|
|
||||||
# check if quote need to be added
|
# check if quote need to be added via user selection of content
|
||||||
signaturePosition = 'bottom'
|
signaturePosition = 'bottom'
|
||||||
selected = App.ClipBoard.getSelected('html')
|
|
||||||
if selected
|
if !@hasUserSelectedContent(ui)
|
||||||
selected = App.Utils.htmlCleanup(selected).html()
|
selected = ''
|
||||||
if !selected
|
else
|
||||||
selected = App.ClipBoard.getSelected('text')
|
selected = @getSelectedContent(ui)
|
||||||
if selected
|
selected = @cleanUpHtmlSelection(selected)
|
||||||
selected = App.Utils.textCleanup(selected)
|
|
||||||
selected = App.Utils.text2html(selected)
|
|
||||||
|
|
||||||
# full quote, if needed
|
# full quote, if needed
|
||||||
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
||||||
|
@ -170,6 +168,67 @@ class EmailReply extends App.Controller
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
||||||
|
@cleanUpHtmlSelection: (selected) ->
|
||||||
|
if selected
|
||||||
|
cleaned_up = App.Utils.htmlCleanup(selected).html()
|
||||||
|
|
||||||
|
return cleaned_up if cleaned_up
|
||||||
|
|
||||||
|
text = App.ClipBoard.getSelected('text')
|
||||||
|
return App.Utils.text2html(text) if text
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
# Fixes Issue #3539 - When replying quote article content only
|
||||||
|
@getSelectedContent: (ui) ->
|
||||||
|
range = window.getSelection().getRangeAt(0)
|
||||||
|
parentSelector = ui.el.closest('.ticket-article-item').attr('id')
|
||||||
|
|
||||||
|
return if !parentSelector
|
||||||
|
|
||||||
|
lastSelElem = $('#' + parentSelector + ' .richtext-content')[0]
|
||||||
|
|
||||||
|
startInsideArticle = @isInsideSelectionBoundary(range.startContainer, parentSelector)
|
||||||
|
endInsideArticle = @isInsideSelectionBoundary(range.endContainer, parentSelector)
|
||||||
|
|
||||||
|
if !startInsideArticle && endInsideArticle
|
||||||
|
range.setStart(lastSelElem, 0)
|
||||||
|
else if startInsideArticle && !endInsideArticle
|
||||||
|
range.setEnd(lastSelElem, lastSelElem.childNodes.length)
|
||||||
|
else if @containsNode(lastSelElem)
|
||||||
|
range.setStart(lastSelElem, 0)
|
||||||
|
range.setEnd(lastSelElem, lastSelElem.childNodes.length)
|
||||||
|
|
||||||
|
App.ClipBoard.manuallyUpdateSelection()
|
||||||
|
App.ClipBoard.getSelected('html')
|
||||||
|
|
||||||
|
# checks if user has made any text selection
|
||||||
|
# checks if that text selection is inside article-content only
|
||||||
|
@hasUserSelectedContent: (ui) ->
|
||||||
|
selObject = App.ClipBoard.getSelectedObject()
|
||||||
|
|
||||||
|
if selObject.rangeCount > 0
|
||||||
|
# item on which reply is clicked
|
||||||
|
parentTicketArticleContainer = ui.el.closest('.ticket-article-item')
|
||||||
|
if parentTicketArticleContainer
|
||||||
|
parentSelector = parentTicketArticleContainer.attr('id')
|
||||||
|
range = selObject.getRangeAt(0)
|
||||||
|
return @isInsideSelectionBoundary(range.startContainer, parentSelector) || @isInsideSelectionBoundary(range.endContainer, parentSelector) || @containsNode($('#' + parentSelector + ' .richtext-content')[0])
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
@isInsideSelectionBoundary: (node, parentSelectorId) ->
|
||||||
|
hasParent = $(node).closest('#' + parentSelectorId + ' .richtext-content')
|
||||||
|
return hasParent && hasParent.attr('class') is 'richtext-content'
|
||||||
|
|
||||||
|
# Selection.containsNode is not supported in IE, hence check
|
||||||
|
@containsNode: (node) ->
|
||||||
|
selected = App.ClipBoard.getSelectedObject()
|
||||||
|
if typeof selected.containsNode == 'function'
|
||||||
|
return selected.containsNode(node, false)
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
@date_format: (date_string) ->
|
@date_format: (date_string) ->
|
||||||
options = {
|
options = {
|
||||||
weekday: 'long'
|
weekday: 'long'
|
||||||
|
|
|
@ -6,11 +6,21 @@ class App.ClipBoard
|
||||||
_instance ?= new _Singleton
|
_instance ?= new _Singleton
|
||||||
_instance.bind(el)
|
_instance.bind(el)
|
||||||
|
|
||||||
|
@manuallyUpdateSelection: (type) ->
|
||||||
|
if _instance == undefined
|
||||||
|
_instance ?= new _Singleton
|
||||||
|
_instance.manuallyUpdateSelection(type)
|
||||||
|
|
||||||
@getSelected: (type) ->
|
@getSelected: (type) ->
|
||||||
if _instance == undefined
|
if _instance == undefined
|
||||||
_instance ?= new _Singleton
|
_instance ?= new _Singleton
|
||||||
_instance.getSelected(type)
|
_instance.getSelected(type)
|
||||||
|
|
||||||
|
@getSelectedObject: (type) ->
|
||||||
|
if _instance == undefined
|
||||||
|
_instance ?= new _Singleton
|
||||||
|
_instance.getSelectedObject(type)
|
||||||
|
|
||||||
@getSelectedLast: (type) ->
|
@getSelectedLast: (type) ->
|
||||||
if _instance == undefined
|
if _instance == undefined
|
||||||
_instance ?= new _Singleton
|
_instance ?= new _Singleton
|
||||||
|
@ -36,9 +46,11 @@ class _Singleton
|
||||||
@selection =
|
@selection =
|
||||||
html: ''
|
html: ''
|
||||||
text: ''
|
text: ''
|
||||||
|
sel: null
|
||||||
@selectionLast =
|
@selectionLast =
|
||||||
html: ''
|
html: ''
|
||||||
text: ''
|
text: ''
|
||||||
|
sel: null
|
||||||
|
|
||||||
# bind to fill selected text into
|
# bind to fill selected text into
|
||||||
bind: (el) ->
|
bind: (el) ->
|
||||||
|
@ -59,7 +71,7 @@ class _Singleton
|
||||||
)
|
)
|
||||||
|
|
||||||
_updateSelection: =>
|
_updateSelection: =>
|
||||||
for key in ['html', 'text']
|
for key in ['html', 'text', 'sel']
|
||||||
@selection[key] = @_getSelected(key)
|
@selection[key] = @_getSelected(key)
|
||||||
if @selection[key]
|
if @selection[key]
|
||||||
@selectionLast[key] = @selection[key]
|
@selectionLast[key] = @selection[key]
|
||||||
|
@ -86,12 +98,23 @@ class _Singleton
|
||||||
for i in [1..sel.rangeCount]
|
for i in [1..sel.rangeCount]
|
||||||
container.appendChild(sel.getRangeAt(i-1).cloneContents())
|
container.appendChild(sel.getRangeAt(i-1).cloneContents())
|
||||||
html = container.innerHTML
|
html = container.innerHTML
|
||||||
html
|
|
||||||
|
if type != 'sel'
|
||||||
|
html
|
||||||
|
else
|
||||||
|
sel
|
||||||
|
|
||||||
|
manuallyUpdateSelection: ->
|
||||||
|
@_updateSelection()
|
||||||
|
|
||||||
# get current selection
|
# get current selection
|
||||||
getSelected: (type) ->
|
getSelected: (type) ->
|
||||||
@selection[type]
|
@selection[type]
|
||||||
|
|
||||||
|
# get current selection original object
|
||||||
|
getSelectedObject: ->
|
||||||
|
@selection['sel']
|
||||||
|
|
||||||
# get latest selection
|
# get latest selection
|
||||||
getSelectedLast: (type) ->
|
getSelectedLast: (type) ->
|
||||||
@selectionLast[type]
|
@selectionLast[type]
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr
|
||||||
let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Example Name <asdf1@example.com>') }
|
let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Example Name <asdf1@example.com>') }
|
||||||
let(:customer) { create(:customer) }
|
let(:customer) { create(:customer) }
|
||||||
let(:current_user) { customer }
|
let(:current_user) { customer }
|
||||||
|
let(:selection) { '' }
|
||||||
|
|
||||||
prepend_before do
|
prepend_before do
|
||||||
Setting.set 'ui_ticket_zoom_article_email_full_quote_header', full_quote_header_setting
|
Setting.set 'ui_ticket_zoom_article_email_full_quote_header', full_quote_header_setting
|
||||||
|
@ -107,6 +108,78 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when text is selected on page while replying' do
|
||||||
|
let(:full_quote_header_setting) { false }
|
||||||
|
let(:before_article_content_selector) { '.ticketZoom-header' }
|
||||||
|
let(:after_article_content_selector) { '.ticket-article-item .humanTimeFromNow' }
|
||||||
|
let(:article_content_selector) { '.ticket-article-item .richtext-content' }
|
||||||
|
|
||||||
|
it 'does not quote article when bits other than the article are selected' do
|
||||||
|
within(:active_content) do
|
||||||
|
selection = highlight_and_get_selection(before_article_content_selector, '')
|
||||||
|
click_reply
|
||||||
|
|
||||||
|
within(:richtext) do
|
||||||
|
expect(page).to have_no_text(selection)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes article when bits inside the article are selected' do
|
||||||
|
within(:active_content) do
|
||||||
|
selection = highlight_and_get_selection(article_content_selector, '')
|
||||||
|
click_reply
|
||||||
|
|
||||||
|
within(:richtext) do
|
||||||
|
expect(page).to have_text(selection)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes only article when bits before the article are selected as well' do
|
||||||
|
within(:active_content) do
|
||||||
|
selection = highlight_and_get_selection(before_article_content_selector, article_content_selector)
|
||||||
|
expected_text = find(article_content_selector).text
|
||||||
|
|
||||||
|
click_reply
|
||||||
|
|
||||||
|
within(:richtext) do
|
||||||
|
expect(page).to have_no_text(selection)
|
||||||
|
expect(page).to have_text(expected_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes only article when bits after the article are selected as well' do
|
||||||
|
within(:active_content) do
|
||||||
|
selection = highlight_and_get_selection(article_content_selector, after_article_content_selector)
|
||||||
|
expected_text = find(article_content_selector).text
|
||||||
|
|
||||||
|
click_reply
|
||||||
|
|
||||||
|
within(:richtext) do
|
||||||
|
expect(page).to have_no_text(selection)
|
||||||
|
expect(page).to have_text(expected_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes only article when bits both before and after the article are selected as well' do
|
||||||
|
within(:active_content) do
|
||||||
|
selection = highlight_and_get_selection(before_article_content_selector, after_article_content_selector)
|
||||||
|
expected_text = find(article_content_selector).text
|
||||||
|
|
||||||
|
click_reply
|
||||||
|
|
||||||
|
within(:richtext) do
|
||||||
|
expect(page).to have_no_text(selection)
|
||||||
|
expect(page).to have_text(expected_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
def click_forward
|
def click_forward
|
||||||
click '.js-ArticleAction[data-type=emailForward]'
|
click '.js-ArticleAction[data-type=emailForward]'
|
||||||
end
|
end
|
||||||
|
@ -115,8 +188,30 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr
|
||||||
click '.js-ArticleAction[data-type=internal]'
|
click '.js-ArticleAction[data-type=internal]'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def click_reply
|
||||||
|
click '.js-ArticleAction[data-type=emailReply]'
|
||||||
|
end
|
||||||
|
|
||||||
|
def highlight_and_get_selection(start_selector, end_selector)
|
||||||
|
find(start_selector)
|
||||||
|
.execute_script(<<~JAVASCRIPT, end_selector)
|
||||||
|
let [ end_selector ] = arguments
|
||||||
|
let end_node = $(end_selector)[0]
|
||||||
|
if(!end_node) {
|
||||||
|
end_node = this.nextSibling
|
||||||
|
}
|
||||||
|
window.getSelection().removeAllRanges()
|
||||||
|
var range = window.document.createRange()
|
||||||
|
range.setStart(this, 0)
|
||||||
|
range.setEnd(end_node, end_node.childNodes.length)
|
||||||
|
window.getSelection().addRange(range)
|
||||||
|
JAVASCRIPT
|
||||||
|
|
||||||
|
find(start_selector).evaluate_script 'window.getSelection().toString().trim()'
|
||||||
|
end
|
||||||
|
|
||||||
def highlight_and_click_reply
|
def highlight_and_click_reply
|
||||||
find('.ticket-article-item .textBubble')
|
find('.ticket-article-item .richtext-content')
|
||||||
.execute_script <<~JAVASCRIPT
|
.execute_script <<~JAVASCRIPT
|
||||||
window.getSelection().removeAllRanges()
|
window.getSelection().removeAllRanges()
|
||||||
var range = window.document.createRange()
|
var range = window.document.createRange()
|
||||||
|
@ -125,7 +220,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr
|
||||||
window.getSelection().addRange(range)
|
window.getSelection().addRange(range)
|
||||||
JAVASCRIPT
|
JAVASCRIPT
|
||||||
|
|
||||||
click '.js-ArticleAction[data-type=emailReply]'
|
click_reply
|
||||||
end
|
end
|
||||||
|
|
||||||
define :contain_full_quote do
|
define :contain_full_quote do
|
||||||
|
|
Loading…
Reference in a new issue