Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Hanzel <mhanzel@gitlab.com>2019-05-30 07:29:39 +0300
committerMartin Hanzel <mhanzel@gitlab.com>2019-06-12 20:43:01 +0300
commitfdfbb26ae1f33b69ab2f8e7d8d849eb3f164430f (patch)
tree277e7954746060c453c891366ca5eb73addb6c2b
parent9421c3ef67c1122a006d36899e5f5215a670b90b (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.
-rw-r--r--app/assets/javascripts/gl_form.js93
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue17
-rw-r--r--app/views/shared/notes/_hints.html.haml12
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