diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 947d019c725a5a680aaff78ef6ab70c33a55d07d..52d9f2f03227592326db476d316db45728388485 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,8 +1,5 @@ import $ from 'jquery'; -import { DOMParser } from 'prosemirror-model'; import { getSelectedFragment } from '~/lib/utils/common_utils'; -import schema from './schema'; -import markdownSerializer from './serializer'; export class CopyAsGFM { constructor() { @@ -39,9 +36,13 @@ export class CopyAsGFM { div.appendChild(el.cloneNode(true)); const html = div.innerHTML; - clipboardData.setData('text/plain', el.textContent); - clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); - clipboardData.setData('text/html', html); + CopyAsGFM.nodeToGFM(el) + .then(res => { + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', res); + clipboardData.setData('text/html', html); + }) + .catch(() => {}); } static pasteGFM(e) { @@ -137,11 +138,21 @@ export class CopyAsGFM { } static nodeToGFM(node) { - const wrapEl = document.createElement('div'); - wrapEl.appendChild(node.cloneNode(true)); - const doc = DOMParser.fromSchema(schema).parse(wrapEl); - - return markdownSerializer.serialize(doc); + return Promise.all([ + import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'), + ]) + .then(([prosemirrorModel, schema, markdownSerializer]) => { + const { DOMParser } = prosemirrorModel; + const wrapEl = document.createElement('div'); + wrapEl.appendChild(node.cloneNode(true)); + const doc = DOMParser.fromSchema(schema.default).parse(wrapEl); + + const res = markdownSerializer.default.serialize(doc); + return res; + }) + .catch(() => {}); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 0eb067d49630c6952cfcf41aa1356e2cb2e2b980..680f2031409970f52cdffff085158252e00fb57d 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts { const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); const blockquoteEl = document.createElement('blockquote'); blockquoteEl.appendChild(el); - const text = CopyAsGFM.nodeToGFM(blockquoteEl); - - if (text.trim() === '') { - return false; - } - - // If replyField already has some content, add a newline before our quote - const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; - $replyField - .val((a, current) => `${current}${separator}${text}\n\n`) - .trigger('input') - .trigger('change'); - - // Trigger autosize - const event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - $replyField.get(0).dispatchEvent(event); + CopyAsGFM.nodeToGFM(blockquoteEl) + .then(text => { + if (text.trim() === '') { + return false; + } + + // If replyField already has some content, add a newline before our quote + const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; + $replyField + .val((a, current) => `${current}${separator}${text}\n\n`) + .trigger('input') + .trigger('change'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + $replyField.get(0).dispatchEvent(event); + + // Focus the input field + $replyField.focus(); - // Focus the input field - $replyField.focus(); + return false; + }) + .catch(() => {}); return false; } diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 1675403507687b9453c5e3f236adeb89c860a011..60ddb02da2c855fdfd2ba54fa639111e9305dcaf 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -843,6 +843,7 @@ describe 'Copy as GFM', :js do def verify(selector, gfm, target: nil) html = html_for_selector(selector) output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target) + wait_for_requests expect(output_gfm.strip).to eq(gfm.strip) end end @@ -861,6 +862,9 @@ describe 'Copy as GFM', :js do def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil) js = <<~JS (function(html) { + // Setting it off so the import already starts + window.CopyAsGFM.nodeToGFM(document.createElement('div')); + var transformer = window.CopyAsGFM[#{transformer.inspect}]; var node = document.createElement('div'); @@ -875,9 +879,18 @@ describe 'Copy as GFM', :js do node = transformer(node, target); if (!node) return null; - return window.CopyAsGFM.nodeToGFM(node); + + window.gfmCopytestRes = null; + window.CopyAsGFM.nodeToGFM(node) + .then((res) => { + window.gfmCopytestRes = res; + }); })("#{escape_javascript(html)}") JS - page.evaluate_script(js) + page.execute_script(js) + + loop until page.evaluate_script('window.gfmCopytestRes !== null') + + page.evaluate_script('window.gfmCopytestRes') end end diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js index 6179a02ce16ee3299ed0fef956de898af2d7a975..ca849f7586074fc94751a0d917e768ff9e332f21 100644 --- a/spec/javascripts/behaviors/copy_as_gfm_spec.js +++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js @@ -1,4 +1,4 @@ -import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; describe('CopyAsGFM', () => { describe('CopyAsGFM.pasteGFM', () => { @@ -79,27 +79,46 @@ describe('CopyAsGFM', () => { return clipboardData; }; + beforeAll(done => { + initCopyAsGFM(); + + // Fake call to nodeToGfm so the import of lazy bundle happened + CopyAsGFM.nodeToGFM(document.createElement('div')) + .then(() => { + done(); + }) + .catch(done.fail); + }); + beforeEach(() => spyOn(clipboardData, 'setData')); describe('list handling', () => { - it('uses correct gfm for unordered lists', () => { + it('uses correct gfm for unordered lists', done => { const selection = stubSelection('
  • List Item1
  • List Item2
  • \n', 'UL'); + spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '* List Item1\n\n* List Item2'; + setTimeout(() => { + const expectedGFM = '* List Item1\n\n* List Item2'; - expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); }); - it('uses correct gfm for ordered lists', () => { + it('uses correct gfm for ordered lists', done => { const selection = stubSelection('
  • List Item1
  • List Item2
  • \n', 'OL'); + spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '1. List Item1\n\n1. List Item2'; + setTimeout(() => { + const expectedGFM = '1. List Item1\n\n1. List Item2'; - expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); }); }); }); diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js index fe827bb1e18dbf7e02069a4d55a31b707fda02b0..4843a0386b5836da08a7f57c0d5bef6086d6eca0 100644 --- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -3,17 +3,26 @@ */ import $ from 'jquery'; -import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm'; +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; -initCopyAsGFM(); - const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; describe('ShortcutsIssuable', function() { const fixtureName = 'snippets/show.html.raw'; preloadFixtures(fixtureName); + beforeAll(done => { + initCopyAsGFM(); + + // Fake call to nodeToGfm so the import of lazy bundle happened + CopyAsGFM.nodeToGFM(document.createElement('div')) + .then(() => { + done(); + }) + .catch(done.fail); + }); + beforeEach(() => { loadFixtures(fixtureName); $('body').append( @@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() { stubSelection('

    Selected text.

    '); }); - it('leaves existing input intact', () => { + it('leaves existing input intact', done => { $(FORM_SELECTOR).val('This text was already here.'); expect($(FORM_SELECTOR).val()).toBe('This text was already here.'); ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe( + 'This text was already here.\n\n> Selected text.\n\n', + ); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); describe('with a one-line selection', () => { - it('quotes the selection', () => { + it('quotes the selection', done => { stubSelection('

    This text has been selected.

    '); ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); + done(); + }); }); }); describe('with a multi-line selection', () => { - it('quotes the selected lines as a group', () => { + it('quotes the selected lines as a group', done => { stubSelection( '

    Selected line one.

    \n

    Selected line two.

    \n

    Selected line three.

    ', ); ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe( - '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', - ); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe( + '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', + ); + done(); + }); }); }); @@ -119,17 +145,23 @@ describe('ShortcutsIssuable', function() { stubSelection('

    Selected text.

    ', true); }); - it('does not add anything to the input', () => { + it('does not add anything to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe(''); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); @@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() { stubSelection('
    Selected text.

    Invalid selected text.

    ', true); }); - it('only adds the valid part to the input', () => { + it('only adds the valid part to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n'); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); }); @@ -183,20 +224,26 @@ describe('ShortcutsIssuable', function() { }); }); - it('adds the quoted selection to the input', () => { + it('adds the quoted selection to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); }); @@ -228,17 +278,23 @@ describe('ShortcutsIssuable', function() { }); }); - it('does not add anything to the input', () => { + it('does not add anything to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe(''); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); });