Commit 3f3067fc authored by Phil Hughes's avatar Phil Hughes

Merge branch '56989-reduce-bundle-size-by-loading-markdown-it-only-when-needed' into 'master'

Reduce Bundle Size by lazy loading markdown-it

Closes #56989

See merge request gitlab-org/gitlab-ce!24763
parents 7c54409f 81429f61
import $ from 'jquery'; import $ from 'jquery';
import { DOMParser } from 'prosemirror-model';
import { getSelectedFragment } from '~/lib/utils/common_utils'; import { getSelectedFragment } from '~/lib/utils/common_utils';
import schema from './schema';
import markdownSerializer from './serializer';
export class CopyAsGFM { export class CopyAsGFM {
constructor() { constructor() {
...@@ -39,9 +36,13 @@ export class CopyAsGFM { ...@@ -39,9 +36,13 @@ export class CopyAsGFM {
div.appendChild(el.cloneNode(true)); div.appendChild(el.cloneNode(true));
const html = div.innerHTML; const html = div.innerHTML;
clipboardData.setData('text/plain', el.textContent); CopyAsGFM.nodeToGFM(el)
clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); .then(res => {
clipboardData.setData('text/html', html); clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', res);
clipboardData.setData('text/html', html);
})
.catch(() => {});
} }
static pasteGFM(e) { static pasteGFM(e) {
...@@ -137,11 +138,21 @@ export class CopyAsGFM { ...@@ -137,11 +138,21 @@ export class CopyAsGFM {
} }
static nodeToGFM(node) { static nodeToGFM(node) {
const wrapEl = document.createElement('div'); return Promise.all([
wrapEl.appendChild(node.cloneNode(true)); import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
const doc = DOMParser.fromSchema(schema).parse(wrapEl); import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
return markdownSerializer.serialize(doc); ])
.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(() => {});
} }
} }
......
...@@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts {
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const blockquoteEl = document.createElement('blockquote'); const blockquoteEl = document.createElement('blockquote');
blockquoteEl.appendChild(el); blockquoteEl.appendChild(el);
const text = CopyAsGFM.nodeToGFM(blockquoteEl); CopyAsGFM.nodeToGFM(blockquoteEl)
.then(text => {
if (text.trim() === '') { if (text.trim() === '') {
return false; return false;
} }
// If replyField already has some content, add a newline before our quote // If replyField already has some content, add a newline before our quote
const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
$replyField $replyField
.val((a, current) => `${current}${separator}${text}\n\n`) .val((a, current) => `${current}${separator}${text}\n\n`)
.trigger('input') .trigger('input')
.trigger('change'); .trigger('change');
// Trigger autosize // Trigger autosize
const event = document.createEvent('Event'); const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false); event.initEvent('autosize:update', true, false);
$replyField.get(0).dispatchEvent(event); $replyField.get(0).dispatchEvent(event);
// Focus the input field
$replyField.focus();
// Focus the input field return false;
$replyField.focus(); })
.catch(() => {});
return false; return false;
} }
......
...@@ -843,6 +843,7 @@ describe 'Copy as GFM', :js do ...@@ -843,6 +843,7 @@ describe 'Copy as GFM', :js do
def verify(selector, gfm, target: nil) def verify(selector, gfm, target: nil)
html = html_for_selector(selector) html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target) output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
wait_for_requests
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
end end
...@@ -861,6 +862,9 @@ describe 'Copy as GFM', :js do ...@@ -861,6 +862,9 @@ describe 'Copy as GFM', :js do
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil) def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<~JS js = <<~JS
(function(html) { (function(html) {
// Setting it off so the import already starts
window.CopyAsGFM.nodeToGFM(document.createElement('div'));
var transformer = window.CopyAsGFM[#{transformer.inspect}]; var transformer = window.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div'); var node = document.createElement('div');
...@@ -875,9 +879,18 @@ describe 'Copy as GFM', :js do ...@@ -875,9 +879,18 @@ describe 'Copy as GFM', :js do
node = transformer(node, target); node = transformer(node, target);
if (!node) return null; if (!node) return null;
return window.CopyAsGFM.nodeToGFM(node);
window.gfmCopytestRes = null;
window.CopyAsGFM.nodeToGFM(node)
.then((res) => {
window.gfmCopytestRes = res;
});
})("#{escape_javascript(html)}") })("#{escape_javascript(html)}")
JS JS
page.evaluate_script(js) page.execute_script(js)
loop until page.evaluate_script('window.gfmCopytestRes !== null')
page.evaluate_script('window.gfmCopytestRes')
end end
end end
import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
describe('CopyAsGFM', () => { describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => { describe('CopyAsGFM.pasteGFM', () => {
...@@ -79,27 +79,46 @@ describe('CopyAsGFM', () => { ...@@ -79,27 +79,46 @@ describe('CopyAsGFM', () => {
return clipboardData; 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')); beforeEach(() => spyOn(clipboardData, 'setData'));
describe('list handling', () => { describe('list handling', () => {
it('uses correct gfm for unordered lists', () => { it('uses correct gfm for unordered lists', done => {
const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL'); const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
spyOn(window, 'getSelection').and.returnValue(selection); spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy(); 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('<li>List Item1</li><li>List Item2</li>\n', 'OL'); const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
spyOn(window, 'getSelection').and.returnValue(selection); spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy(); 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();
});
}); });
}); });
}); });
......
...@@ -3,17 +3,26 @@ ...@@ -3,17 +3,26 @@
*/ */
import $ from 'jquery'; 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'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
initCopyAsGFM();
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
describe('ShortcutsIssuable', function() { describe('ShortcutsIssuable', function() {
const fixtureName = 'snippets/show.html.raw'; const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName); 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(() => { beforeEach(() => {
loadFixtures(fixtureName); loadFixtures(fixtureName);
$('body').append( $('body').append(
...@@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() { ...@@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() {
stubSelection('<p>Selected text.</p>'); stubSelection('<p>Selected text.</p>');
}); });
it('leaves existing input intact', () => { it('leaves existing input intact', done => {
$(FORM_SELECTOR).val('This text was already here.'); $(FORM_SELECTOR).val('This text was already here.');
expect($(FORM_SELECTOR).val()).toBe('This text was already here.'); expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
ShortcutsIssuable.replyWithSelectedText(true); 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; let triggered = false;
$(FORM_SELECTOR).on('input', () => { $(FORM_SELECTOR).on('input', () => {
triggered = true; triggered = true;
...@@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() { ...@@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true); 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'); const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(spy).toHaveBeenCalled(); setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
});
}); });
}); });
describe('with a one-line selection', () => { describe('with a one-line selection', () => {
it('quotes the selection', () => { it('quotes the selection', done => {
stubSelection('<p>This text has been selected.</p>'); stubSelection('<p>This text has been selected.</p>');
ShortcutsIssuable.replyWithSelectedText(true); 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', () => { describe('with a multi-line selection', () => {
it('quotes the selected lines as a group', () => { it('quotes the selected lines as a group', done => {
stubSelection( stubSelection(
'<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>', '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
); );
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect($(FORM_SELECTOR).val()).toBe( setTimeout(() => {
'> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', 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() { ...@@ -119,17 +145,23 @@ describe('ShortcutsIssuable', function() {
stubSelection('<p>Selected text.</p>', true); stubSelection('<p>Selected text.</p>', true);
}); });
it('does not add anything to the input', () => { it('does not add anything to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true); 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'); const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(spy).toHaveBeenCalled(); setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
});
}); });
}); });
...@@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() { ...@@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() {
stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true); stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true);
}); });
it('only adds the valid part to the input', () => { it('only adds the valid part to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true); 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'); const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(spy).toHaveBeenCalled(); setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
});
}); });
it('triggers `input`', () => { it('triggers `input`', done => {
let triggered = false; let triggered = false;
$(FORM_SELECTOR).on('input', () => { $(FORM_SELECTOR).on('input', () => {
triggered = true; triggered = true;
...@@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() { ...@@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(triggered).toBe(true); setTimeout(() => {
expect(triggered).toBe(true);
done();
});
}); });
}); });
...@@ -183,20 +224,26 @@ describe('ShortcutsIssuable', function() { ...@@ -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); 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'); const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(spy).toHaveBeenCalled(); setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
});
}); });
it('triggers `input`', () => { it('triggers `input`', done => {
let triggered = false; let triggered = false;
$(FORM_SELECTOR).on('input', () => { $(FORM_SELECTOR).on('input', () => {
triggered = true; triggered = true;
...@@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() { ...@@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(triggered).toBe(true); setTimeout(() => {
expect(triggered).toBe(true);
done();
});
}); });
}); });
...@@ -228,17 +278,23 @@ describe('ShortcutsIssuable', function() { ...@@ -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); 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'); const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true); ShortcutsIssuable.replyWithSelectedText(true);
expect(spy).toHaveBeenCalled(); setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
});
}); });
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment