diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 index 0d1e9b0a952..63291853548 100644 --- a/app/assets/javascripts/copy_as_gfm.js.es6 +++ b/app/assets/javascripts/copy_as_gfm.js.es6 @@ -231,20 +231,13 @@ let clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; - if (!window.getSelection) return; - - let selection = window.getSelection(); - if (!selection.rangeCount || selection.rangeCount === 0) return; - - let selectedDocument = selection.getRangeAt(0).cloneContents(); - if (!selectedDocument) return; - - if (selectedDocument.textContent.length === 0) return; + let documentFragment = CopyAsGFM.getSelectedFragment(); + if (!documentFragment) return; e.preventDefault(); - clipboardData.setData('text/plain', selectedDocument.textContent); + clipboardData.setData('text/plain', documentFragment.textContent); - let gfm = CopyAsGFM.nodeToGFM(selectedDocument); + let gfm = CopyAsGFM.nodeToGFM(documentFragment); clipboardData.setData('text/x-gfm', gfm); } @@ -257,11 +250,25 @@ e.preventDefault(); - this.insertText(e.target, gfm); + CopyAsGFM.insertText(e.target, gfm); } - insertText(target, text) { - // Firefox doesn't support `document.execCommand('insertText', false, text);` on textareas + static getSelectedFragment() { + if (!window.getSelection) return null; + + let selection = window.getSelection(); + if (!selection.rangeCount || selection.rangeCount === 0) return null; + + let documentFragment = selection.getRangeAt(0).cloneContents(); + if (!documentFragment) return null; + + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; + } + + static insertText(target, text) { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas let selectionStart = target.selectionStart; let selectionEnd = target.selectionEnd; @@ -292,7 +299,7 @@ for (let selector in rules) { let func = rules[selector]; - if (!node.matches(selector)) continue; + if (!CopyAsGFM.nodeMatchesSelector(node, selector)) continue; let result = func(node, text); if (result === false) continue; @@ -315,11 +322,38 @@ let clonedNode = clonedNodes[i]; let text = this.nodeToGFM(node); + + // `clonedNode.replaceWith(text)` is not yet widely supported clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); } return clonedParentNode.innerText || clonedParentNode.textContent; } + + static nodeMatchesSelector(node, selector) { + let matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + let matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 426821873d7..e9ede122ab7 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -39,26 +39,22 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, selectedDocument, selected, selection, separator; - if (!window.getSelection) return; + var quote, replyField, documentFragment, selected, separator; - selection = window.getSelection(); - if (!selection.rangeCount || selection.rangeCount === 0) return; + documentFragment = window.gl.CopyAsGFM.getSelectedFragment(); + if (!documentFragment) return; - selectedDocument = selection.getRangeAt(0).cloneContents(); - if (!selectedDocument) return; - - selected = window.gl.CopyAsGFM.nodeToGFM(selectedDocument); + selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); replyField = $('.js-main-target-form #note_note'); if (selected.trim() === "") { return; } quote = _.map(selected.split("\n"), function(val) { - return "> " + val + "\n"; + return ("> " + val).trim() + "\n"; }); // If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() !== "" && "\n" || ''; + separator = replyField.val().trim() !== "" && "\n\n" || ''; replyField.val(function(_, current) { return current + separator + quote.join('') + "\n"; }); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index ae5d639ad9c..7e5c0e2f144 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ /* global ShortcutsIssuable */ +/*= require copy_as_gfm */ /*= require shortcuts_issuable */ (function() { @@ -14,10 +15,12 @@ }); return describe('#replyWithSelectedText', function() { var stubSelection; - // Stub window.getSelection to return the provided String. - stubSelection = function(text) { - return window.getSelection = function() { - return text; + // Stub window.gl.CopyAsGFM.getSelectedFragment to return a node with the provided HTML. + stubSelection = function(html) { + window.gl.CopyAsGFM.getSelectedFragment = function() { + var node = document.createElement('div'); + node.innerHTML = html; + return node; }; }; beforeEach(function() { @@ -32,13 +35,13 @@ }); describe('with any selection', function() { beforeEach(function() { - return stubSelection('Selected text.'); + return stubSelection('

Selected text.

'); }); it('leaves existing input intact', function() { $(this.selector).val('This text was already here.'); expect($(this.selector).val()).toBe('This text was already here.'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n"); + return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); }); it('triggers `input`', function() { var triggered; @@ -61,16 +64,16 @@ }); describe('with a one-line selection', function() { return it('quotes the selection', function() { - stubSelection('This text has been selected.'); + stubSelection('

This text has been selected.

'); this.shortcut.replyWithSelectedText(); return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); }); }); return describe('with a multi-line selection', function() { return it('quotes the selected lines as a group', function() { - stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n"); + stubSelection("

Selected line one.

\n\n

Selected line two.

\n\n

Selected line three.

"); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n"); + return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); }); }); });