diff options
author | Martin Hanzel <mhanzel@gitlab.com> | 2019-05-30 07:29:39 +0300 |
---|---|---|
committer | Martin Hanzel <mhanzel@gitlab.com> | 2019-06-12 20:43:01 +0300 |
commit | fdfbb26ae1f33b69ab2f8e7d8d849eb3f164430f (patch) | |
tree | 277e7954746060c453c891366ca5eb73addb6c2b | |
parent | 9421c3ef67c1122a006d36899e5f5215a670b90b (diff) |
Support indentation in markdown editors
Affected editors are issue/MR discussions, wiki editor, milestone
editor, etc. Also, to support indentation, these editors need to have a
custom undo stack.
6 files changed, 127 insertions, 3 deletions
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index a66555838ba..dfafb7eb076 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,9 +3,15 @@ import autosize from 'autosize'; import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete'; import dropzoneInput from './dropzone_input'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; +import IndentHelper from './helpers/indent_helper'; +import { getPlatformLeaderKeyHTML, keystroke } from "./lib/utils/common_utils"; +import UndoStack from './lib/utils/undo_stack'; export default class GLForm { constructor(form, enableGFM = {}) { + this.handleKeyShortcuts = this.handleKeyShortcuts.bind(this); + this.setState = this.setState.bind(this); + this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM); @@ -16,6 +22,15 @@ export default class GLForm { this.enableGFM[item] = Boolean(dataSources[item]); } }); + + this.undoStack = new UndoStack(); + this.indentHelper = new IndentHelper(this.textarea[0]); + + // This shows the indent help text in the Wiki editor, since it's still a + // HAML component + $('.js-leader-key').html(getPlatformLeaderKeyHTML()); + $('.js-indent-help-message').removeClass('hidden'); + // Before we start, we should clean up any previous data for this form this.destroy(); // Set up the form @@ -85,9 +100,86 @@ export default class GLForm { clearEventListeners() { this.textarea.off('focus'); this.textarea.off('blur'); + this.textarea.off('keydown'); removeMarkdownListeners(this.form); } + setState(state) { + const selection = [this.textarea[0].selectionStart, this.textarea[0].selectionEnd]; + this.textarea.val(state); + this.textarea[0].setSelectionRange(selection[0], selection[1]); + } + + handleUndo(event) { + /* + Custom undo/redo stack. + We need this because the toolbar buttons and indentation helpers mess with + the browser's native undo/redo capability. + */ + if (this.undoStack.isEmpty()) { + // ==== Save initial state in undo history ==== + this.undoStack.save(this.textarea.val()); + } + if (keystroke(event, 'Leader+Z')) { + // ==== Undo ==== + event.preventDefault(); + this.undoStack.save(this.textarea.val()); + if (this.undoStack.canUndo()) { + this.setState(this.undoStack.undo()); + } + } + else if (keystroke(event, 'Leader+Shift+Z') || keystroke(event, 'Leader+Y')) { + // ==== Redo ==== + event.preventDefault(); + if (this.undoStack.canRedo()) { + this.setState(this.undoStack.redo()); + } + } + else if (keystroke(event, 'Space') || keystroke(event, 'Enter')) { + // ==== Save after finishing a word ==== + this.undoStack.save(this.textarea.val()); + } + else if (this.textarea[0].selectionStart !== this.textarea[0].selectionEnd) { + // ==== Save if killing a large selection ==== + this.undoStack.save(this.textarea.val()); + } + else if (this.textarea.val() === '') { + // ==== Save if deleting everything ==== + this.undoStack.save(''); + } + else { + // ==== Save after 1 second of inactivity ==== + this.undoStack.scheduleSave(this.textarea.val()); + } + } + + handleIndent(event) { + if (keystroke(event, 'Leader+[')) { + // ==== Unindent selected lines ==== + event.preventDefault(); + this.indentHelper.unindent(); + } + else if (keystroke(event, 'Leader+]')) { + // ==== Indent selected lines ==== + event.preventDefault(); + this.indentHelper.indent(); + } + else if (keystroke(event, 'Enter')) { + // ==== Auto-indent new lines ==== + event.preventDefault(); + this.indentHelper.newline(); + } + else if (keystroke(event, 'Backspace')) { + // ==== Auto-delete indents at the beginning of the line ==== + this.indentHelper.backspace(event); + } + } + + handleKeyShortcuts(event) { + this.handleIndent(event); + this.handleUndo(event); + } + addEventListeners() { this.textarea.on('focus', function focusTextArea() { $(this) @@ -99,5 +191,6 @@ export default class GLForm { .closest('.md-area') .removeClass('is-focused'); }); + this.textarea.on('keydown', e => this.handleKeyShortcuts(e.originalEvent)); } } diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 075c28e8d07..6532cb39398 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -347,6 +347,7 @@ Please check your network connection and try again.`; :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" + :can-indent="true" > <textarea id="note-body" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 09ecb695214..ae5b79c54cc 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -245,6 +245,7 @@ export default { :line="line" :note="discussionNote" :can-suggest="canSuggest" + :can-indent="true" :add-spacing-classes="false" :help-page-path="helpPagePath" > diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 0f3b3568414..dd7296f3d98 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -42,6 +42,11 @@ export default { required: false, default: true, }, + canIndent: { + type: Boolean, + required: false, + default: true, + }, enableAutocomplete: { type: Boolean, required: false, @@ -207,6 +212,7 @@ export default { :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" + :can-indent="canIndent" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index d6c398c8946..94c5e324cac 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,5 +1,6 @@ <script> import { GlLink } from '@gitlab/ui'; +import { getPlatformLeaderKeyHTML } from "~/lib/utils/common_utils"; export default { components: { @@ -20,11 +21,19 @@ export default { required: false, default: true, }, + canIndent: { + type: Boolean, + required: false, + default: true, + }, }, computed: { hasQuickActionsDocsPath() { return this.quickActionsDocsPath !== ''; }, + leaderKeyHTML() { + return getPlatformLeaderKeyHTML(); + } }, }; </script> @@ -34,13 +43,17 @@ export default { <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" - >Markdown is supported</gl-link + >Markdown is supported.</gl-link > </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link> - are supported + are supported. + </template> + <template v-if="canIndent"> + <code><span v-html="leaderKeyHTML"></span>+[</code> + and <code><span v-html="leaderKeyHTML"></span>+]</code> to indent. </template> </div> <span v-if="canAttachFile" class="uploading-container"> diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index fae7d6526e8..93b2ccbaaab 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -8,7 +8,17 @@ are - else is - supported + supported. + + %span.hidden.js-indent-help-message + %code + %span.js-leader-key> + +foo + and + %code< + %span.js-leader-key> + +foo + to indent. %span.uploading-container %span.uploading-progress-container.hide |