summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@selenight.nl>2019-02-10 18:42:13 -0800
committerDouwe Maan <douwe@selenight.nl>2019-05-05 22:20:56 +0200
commit314b0aa99e858439c55277745886403fb6a58805 (patch)
tree0024c0d1cd2b8620ba30d8f038eec51284c067e6
parent140d40d25d6c871e6b3f0cca795224a65637f019 (diff)
downloadgitlab-ce-dm-markdown-field-textarea.tar.gz
Move quote-shortcut logic to MarkdownFielddm-markdown-field-textarea
-rw-r--r--app/assets/javascripts/behaviors/markdown/event_hub.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js39
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue46
-rw-r--r--spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js318
5 files changed, 216 insertions, 191 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/event_hub.js b/app/assets/javascripts/behaviors/markdown/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 670f66b005e..c582b835567 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,8 +1,8 @@
-import $ from 'jquery';
import Mousetrap from 'mousetrap';
import Sidebar from '../../right_sidebar';
import Shortcuts from './shortcuts';
import { CopyAsGFM } from '../markdown/copy_as_gfm';
+import eventHub from '../markdown/event_hub';
import { getSelectedFragment } from '~/lib/utils/common_utils';
export default class ShortcutsIssuable extends Shortcuts {
@@ -23,16 +23,10 @@ export default class ShortcutsIssuable extends Shortcuts {
}
static replyWithSelectedText() {
- const $replyField = $('.js-main-target-form .js-vue-comment-form');
-
- if (!$replyField.length || $replyField.is(':hidden') /* Other tab selected in MR */) {
- return false;
- }
-
const documentFragment = getSelectedFragment(document.querySelector('#content-body'));
if (!documentFragment) {
- $replyField.focus();
+ eventHub.$emit('focus');
return false;
}
@@ -56,38 +50,13 @@ export default class ShortcutsIssuable extends Shortcuts {
// If there is no message, just select the reply field
if (!foundMessage) {
- $replyField.focus();
+ eventHub.$emit('focus');
return false;
}
}
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- const blockquoteEl = document.createElement('blockquote');
- blockquoteEl.appendChild(el);
- 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();
-
- return false;
- })
- .catch(() => {});
+ eventHub.$emit('quoteNode', el);
return false;
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 06b0b850941..b41ec21b1ef 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -332,6 +332,7 @@ Please check your network connection and try again.`;
:textarea-supports-quick-actions="true"
:textarea-label="__('Comment')"
:editable="!isSubmitting"
+ :subscribe-to-global-events="true"
@edit-previous="editCurrentUserLastNote()"
@save="handleSave()"
@cancel="cancelHandler(true)"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index a05947a1813..5f352641070 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -6,6 +6,8 @@ import Autosize from 'autosize';
import { __ } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import Flash from '../../../flash';
+import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+import eventHub from '~/behaviors/markdown/event_hub';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
@@ -111,6 +113,11 @@ export default {
required: false,
default: '',
},
+ subscribeToGlobalEvents: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -228,6 +235,11 @@ export default {
Autosize(this.$refs.textarea);
this.autosizeTextarea();
+
+ if (this.subscribeToGlobalEvents) {
+ eventHub.$on('focus', this.focusIfVisible);
+ eventHub.$on('quoteNode', this.quoteNodeIfVisible);
+ }
},
beforeDestroy() {
if (this.autocomplete) {
@@ -239,6 +251,11 @@ export default {
}
Autosize.destroy(this.$refs.textarea);
+
+ if (this.subscribeToGlobalEvents) {
+ eventHub.$off('focus', this.focusIfVisible);
+ eventHub.$off('quoteNode', this.quoteNodeIfVisible);
+ }
},
methods: {
setupAutocomplete() {
@@ -293,6 +310,31 @@ export default {
}
},
+ quoteNode(node) {
+ const blockquoteEl = document.createElement('blockquote');
+ blockquoteEl.appendChild(node.cloneNode(true));
+
+ const current = this.currentValue.trim();
+ const separator = current.length ? '\n\n' : '';
+
+ CopyAsGFM.nodeToGFM(blockquoteEl)
+ .then(markdown => this.setCurrentValue(`${current}${separator}${markdown}\n\n`))
+ .then(this.switchToEditor)
+ .then(this.$nextTick)
+ .then(this.focus)
+ .catch(() => {});
+ },
+
+ ifVisible(func) {
+ if ($(this.$el).is(':visible')) {
+ func();
+ }
+ },
+
+ quoteNodeIfVisible(node) {
+ this.ifVisible(() => this.quoteNode(node));
+ },
+
blur() {
if (this.modeIsMarkdown) {
this.$refs.textarea.blur();
@@ -305,6 +347,10 @@ export default {
}
},
+ focusIfVisible() {
+ this.ifVisible(this.focus);
+ },
+
renderMarkdown() {
if (!this.renderedOutdated || this.renderedLoading) return;
diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
index 5e457a4e823..5a14c1e5156 100644
--- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -2,40 +2,38 @@
no-underscore-dangle
*/
-import $ from 'jquery';
+import Vue from 'vue';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-
-const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
+import fieldComponent from '~/vue_shared/components/markdown/field.vue';
describe('ShortcutsIssuable', function() {
- const fixtureName = 'snippets/show.html';
- preloadFixtures(fixtureName);
-
beforeAll(done => {
initCopyAsGFM();
- // Fake call to nodeToGfm so the import of lazy bundle happened
+ // Fake call to nodeToGFM so the import of lazy bundle happened
CopyAsGFM.nodeToGFM(document.createElement('div'))
- .then(() => {
- done();
- })
+ .then(done)
.catch(done.fail);
});
- beforeEach(() => {
- loadFixtures(fixtureName);
- $('body').append(
- `<div class="js-main-target-form">
- <textare class="js-vue-comment-form"></textare>
- </div>`,
- );
- document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
- this.shortcut = new ShortcutsIssuable(true);
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(fieldComponent);
+
+ vm = new Component({
+ propsData: {
+ markdownPreviewPath: '/preview',
+ markdownDocsPath: '/docs',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
});
afterEach(() => {
- $(FORM_SELECTOR).remove();
+ vm.$destroy();
});
describe('replyWithSelectedText', () => {
@@ -53,17 +51,29 @@ describe('ShortcutsIssuable', function() {
});
};
describe('with empty selection', () => {
- it('does not return an error', () => {
- ShortcutsIssuable.replyWithSelectedText(true);
-
- expect($(FORM_SELECTOR).val()).toBe('');
+ it('does not return an error', done => {
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('triggers `focus`', () => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- expect(spy).toHaveBeenCalled();
+ it('triggers `focus`', done => {
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -73,54 +83,47 @@ describe('ShortcutsIssuable', function() {
});
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);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe(
- 'This text was already here.\n\n> Selected text.\n\n',
- );
- done();
- });
- });
-
- it('triggers `input`', done => {
- let triggered = false;
- $(FORM_SELECTOR).on('input', () => {
- triggered = true;
- });
-
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(triggered).toBe(true);
- done();
- });
+ vm.value = 'This text was already here.';
+
+ Vue.nextTick()
+ .then(() => ShortcutsIssuable.replyWithSelectedText(vm))
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('This text was already here.\n\n> Selected text.\n\n');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('triggers `focus`', done => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('with a one-line selection', () => {
it('quotes the selection', done => {
stubSelection('<p>This text has been selected.</p>');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('> This text has been selected.\n\n');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -129,14 +132,17 @@ describe('ShortcutsIssuable', function() {
stubSelection(
'<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
);
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe(
- '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
- );
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe(
+ '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -146,22 +152,28 @@ describe('ShortcutsIssuable', function() {
});
it('does not add anything to the input', done => {
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe('');
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('triggers `focus`', done => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -171,36 +183,30 @@ describe('ShortcutsIssuable', function() {
});
it('only adds the valid part to the input', done => {
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('> Selected text.\n\n');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('triggers `focus`', done => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
- });
-
- it('triggers `input`', done => {
- let triggered = false;
- $(FORM_SELECTOR).on('input', () => {
- triggered = true;
- });
-
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(triggered).toBe(true);
- done();
- });
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -225,36 +231,30 @@ describe('ShortcutsIssuable', function() {
});
it('adds the quoted selection to the input', done => {
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('> *Selected text.*\n\n');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('triggers `focus`', done => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
- });
-
- it('triggers `input`', done => {
- let triggered = false;
- $(FORM_SELECTOR).on('input', () => {
- triggered = true;
- });
-
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(triggered).toBe(true);
- done();
- });
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -279,22 +279,28 @@ describe('ShortcutsIssuable', function() {
});
it('does not add anything to the input', done => {
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect($(FORM_SELECTOR).val()).toBe('');
- done();
- });
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.currentValue).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('triggers `focus`', done => {
- const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
- ShortcutsIssuable.replyWithSelectedText(true);
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ const spy = spyOn(vm, 'focus');
+ ShortcutsIssuable.replyWithSelectedText(vm);
+
+ Vue.nextTick()
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});