summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorMichael Kozono <mkozono@gmail.com>2019-08-01 20:44:38 +0000
committerMichael Kozono <mkozono@gmail.com>2019-08-01 20:44:38 +0000
commit52b857f119debb5a03c216c4199eb21a49d815b6 (patch)
treee024d7638e8683c1902bf4b220d44fcdd57fe807 /app/assets
parentb2dd581be375e4f21af9d2c487528ffd06508618 (diff)
parent84b6c7a5f3bf3d6f96331d73225903d3fd92b4e2 (diff)
downloadgitlab-ce-52b857f119debb5a03c216c4199eb21a49d815b6.tar.gz
Merge branch 'revert-editor-indents' into 'master'
Revert "Merge branch 'mh/editor-indents' into 'master'" See merge request gitlab-org/gitlab-ce!31391
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/gl_form.js87
-rw-r--r--app/assets/javascripts/helpers/indent_helper.js182
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js65
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js10
-rw-r--r--app/assets/javascripts/lib/utils/undo_stack.js105
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue41
7 files changed, 19 insertions, 472 deletions
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index daa941a63cd..7a6ad3dc771 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -12,7 +12,6 @@ import 'core-js/es/promise/finally';
import 'core-js/es/string/code-point-at';
import 'core-js/es/string/from-code-point';
import 'core-js/es/string/includes';
-import 'core-js/es/string/repeat';
import 'core-js/es/string/starts-with';
import 'core-js/es/string/ends-with';
import 'core-js/es/symbol';
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index b98fe9f6ce2..a66555838ba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,16 +3,9 @@ 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 { keystroke } from './lib/utils/common_utils';
-import * as keys from './lib/utils/keycodes';
-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);
@@ -23,10 +16,6 @@ export default class GLForm {
this.enableGFM[item] = Boolean(dataSources[item]);
}
});
-
- this.undoStack = new UndoStack();
- this.indentHelper = new IndentHelper(this.textarea[0]);
-
// Before we start, we should clean up any previous data for this form
this.destroy();
// Set up the form
@@ -96,84 +85,9 @@ 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]);
- }
-
- /*
- Handle keypresses for a custom undo/redo stack.
- We need this because the toolbar buttons and indentation helpers mess with the browser's
- native undo/redo capability.
- */
- handleUndo(event) {
- const content = this.textarea.val();
- const { selectionStart, selectionEnd } = this.textarea[0];
- const stack = this.undoStack;
-
- if (stack.isEmpty()) {
- // ==== Save initial state in undo history ====
- stack.save(content);
- }
-
- if (keystroke(event, keys.Z_KEY_CODE, 'l')) {
- // ==== Undo ====
- event.preventDefault();
- stack.save(content);
- if (stack.canUndo()) {
- this.setState(stack.undo());
- }
- } else if (keystroke(event, keys.Z_KEY_CODE, 'ls') || keystroke(event, keys.Y_KEY_CODE, 'l')) {
- // ==== Redo ====
- event.preventDefault();
- if (stack.canRedo()) {
- this.setState(stack.redo());
- }
- } else if (
- keystroke(event, keys.SPACE_KEY_CODE) ||
- keystroke(event, keys.ENTER_KEY_CODE) ||
- selectionStart !== selectionEnd
- ) {
- // ==== Save after finishing a word or before deleting a large selection ====
- stack.save(content);
- } else if (content === '') {
- // ==== Save after deleting everything ====
- stack.save('');
- } else {
- // ==== Save after 1 second of inactivity ====
- stack.scheduleSave(content);
- }
- }
-
- handleIndent(event) {
- if (keystroke(event, keys.LEFT_BRACKET_KEY_CODE, 'l')) {
- // ==== Unindent selected lines ====
- event.preventDefault();
- this.indentHelper.unindent();
- } else if (keystroke(event, keys.RIGHT_BRACKET_KEY_CODE, 'l')) {
- // ==== Indent selected lines ====
- event.preventDefault();
- this.indentHelper.indent();
- } else if (keystroke(event, keys.ENTER_KEY_CODE)) {
- // ==== Auto-indent new lines ====
- event.preventDefault();
- this.indentHelper.newline();
- } else if (keystroke(event, keys.BACKSPACE_KEY_CODE)) {
- // ==== 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)
@@ -185,6 +99,5 @@ 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/helpers/indent_helper.js b/app/assets/javascripts/helpers/indent_helper.js
deleted file mode 100644
index a8815fac04e..00000000000
--- a/app/assets/javascripts/helpers/indent_helper.js
+++ /dev/null
@@ -1,182 +0,0 @@
-const INDENT_SEQUENCE = ' ';
-
-function countLeftSpaces(text) {
- const i = text.split('').findIndex(c => c !== ' ');
- return i === -1 ? text.length : i;
-}
-
-/**
- * IndentHelper provides methods that allow manual and smart indentation in
- * textareas. It supports line indent/unindent, selection indent/unindent,
- * auto indentation on newlines, and smart deletion of indents with backspace.
- */
-export default class IndentHelper {
- /**
- * Creates a new IndentHelper and binds it to the given `textarea`. You can provide a custom indent sequence in the second parameter, but the `newline` and `backspace` operations may work funny if the indent sequence isn't spaces only.
- */
- constructor(textarea, indentSequence = INDENT_SEQUENCE) {
- this.element = textarea;
- this.seq = indentSequence;
- }
-
- getSelection() {
- return { start: this.element.selectionStart, end: this.element.selectionEnd };
- }
-
- isRangeSelection() {
- return this.element.selectionStart !== this.element.selectionEnd;
- }
-
- /**
- * Re-implementation of textarea's setRangeText method, because IE/Edge don't support it.
- *
- * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea%2Finput-setrangetext
- */
- setRangeText(replacement, start, end, selectMode) {
- // Disable eslint to remain as faithful as possible to the above linked spec
- /* eslint-disable no-param-reassign, no-case-declarations */
- const text = this.element.value;
-
- if (start > end) {
- throw new RangeError('setRangeText: start index must be less than or equal to end index');
- }
-
- // Clamp to [0, len]
- start = Math.max(0, Math.min(start, text.length));
- end = Math.max(0, Math.min(end, text.length));
-
- let selection = { start: this.element.selectionStart, end: this.element.selectionEnd };
-
- this.element.value = text.slice(0, start) + replacement + text.slice(end);
-
- const newLength = replacement.length;
- const newEnd = start + newLength;
-
- switch (selectMode) {
- case 'select':
- selection = { start, newEnd };
- break;
- case 'start':
- selection = { start, end: start };
- break;
- case 'end':
- selection = { start: newEnd, end: newEnd };
- break;
- case 'preserve':
- default:
- const oldLength = end - start;
- const delta = newLength - oldLength;
- if (selection.start > end) {
- selection.start += delta;
- } else if (selection.start > start) {
- selection.start = start;
- }
- if (selection.end > end) {
- selection.end += delta;
- } else if (selection.end > start) {
- selection.end = newEnd;
- }
- }
-
- this.element.setSelectionRange(selection.start, selection.end);
-
- /* eslint-enable no-param-reassign, no-case-declarations */
- }
-
- /**
- * Returns an array of lines in the textarea, with information about their
- * start/end offsets and whether they are included in the current selection.
- */
- splitLines() {
- const { start, end } = this.getSelection();
-
- const lines = this.element.value.split('\n');
- let textStart = 0;
- const lineObjects = [];
- lines.forEach(line => {
- const lineObj = {
- text: line,
- start: textStart,
- end: textStart + line.length,
- };
- lineObj.inSelection = lineObj.start <= end && lineObj.end >= start;
- lineObjects.push(lineObj);
- textStart += line.length + 1;
- });
- return lineObjects;
- }
-
- /**
- * Indents selected lines by one level.
- */
- indent() {
- const { start } = this.getSelection();
-
- const selectedLines = this.splitLines().filter(line => line.inSelection);
- if (!this.isRangeSelection() && start === selectedLines[0].start) {
- // Special case: if cursor is at the beginning of the line, move it one
- // indent right.
- const line = selectedLines[0];
- this.setRangeText(this.seq, line.start, line.start, 'end');
- } else {
- selectedLines.reverse();
- selectedLines.forEach(line => {
- this.setRangeText(INDENT_SEQUENCE, line.start, line.start, 'preserve');
- });
- }
- }
-
- /**
- * Unindents selected lines by one level.
- */
- unindent() {
- const lines = this.splitLines().filter(line => line.inSelection);
- lines.reverse();
- lines
- .filter(line => line.text.startsWith(this.seq))
- .forEach(line => {
- this.setRangeText('', line.start, line.start + this.seq.length, 'preserve');
- });
- }
-
- /**
- * Emulates a newline keypress, automatically indenting the new line.
- */
- newline() {
- const { start, end } = this.getSelection();
-
- if (this.isRangeSelection()) {
- // Manually kill the selection before calculating the indent
- this.setRangeText('', start, end, 'start');
- }
-
- // Auto-indent the next line
- const currentLine = this.splitLines().find(line => line.end >= start);
- const spaces = countLeftSpaces(currentLine.text);
- this.setRangeText(`\n${' '.repeat(spaces)}`, start, start, 'end');
- }
-
- /**
- * If the cursor is positioned at the end of a line's leading indents,
- * emulates a backspace keypress by deleting a single level of indents.
- * @param event The DOM KeyboardEvent that triggers this action, or null.
- */
- backspace(event) {
- const { start } = this.getSelection();
-
- // If the cursor is at the end of leading indents, delete an indent.
- if (!this.isRangeSelection()) {
- const currentLine = this.splitLines().find(line => line.end >= start);
- const cursorPosition = start - currentLine.start;
- if (countLeftSpaces(currentLine.text) === cursorPosition && cursorPosition > 0) {
- if (event) event.preventDefault();
-
- let spacesToDelete = cursorPosition % this.seq.length;
- if (spacesToDelete === 0) {
- spacesToDelete = this.seq.length;
- }
- this.setRangeText('', start - spacesToDelete, start, 'start');
- }
- }
- }
-}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 1a94aee2398..5e90893b684 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -203,71 +203,6 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const getPlatformLeaderKey = () => {
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- if (navigator && navigator.platform && navigator.platform.startsWith('Mac')) {
- return 'meta';
- }
- return 'ctrl';
-};
-
-export const getPlatformLeaderKeyHTML = () => {
- if (getPlatformLeaderKey() === 'meta') {
- return '&#8984;';
- }
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- return 'Ctrl';
-};
-
-export const isPlatformLeaderKey = e => {
- if (getPlatformLeaderKey() === 'meta') {
- return Boolean(e.metaKey);
- }
- return Boolean(e.ctrlKey);
-};
-
-/**
- * Tests if a KeyboardEvent corresponds exactly to a keystroke.
- *
- * This function avoids hacking around an old version of Mousetrap, which we ship at the moment. It should be removed after we upgrade to the newest Mousetrap. See:
- * - https://gitlab.com/gitlab-org/gitlab-ce/issues/63182
- * - https://gitlab.com/gitlab-org/gitlab-ce/issues/64246
- *
- * @example
- * // Matches the enter key with exactly zero modifiers
- * keystroke(event, 13)
- *
- * @example
- * // Matches Control-Shift-Z
- * keystroke(event, 90, 'cs')
- *
- * @param e The KeyboardEvent to test.
- * @param keyCode The key code of the key to test. Why keycodes? IE/Edge don't support the more convenient `key` and `code` properties.
- * @param modifiers A string of modifiers keys. Each modifier key is represented by one character. The set of pressed modifier keys must match the given string exactly. Available options are 'a' for Alt/Option, 'c' for Control, 'm' for Meta/Command, 's' for Shift, and 'l' for the leader key (Meta on MacOS and Control otherwise).
- * @returns {boolean} True if the KeyboardEvent corresponds to the given keystroke.
- */
-export const keystroke = (e, keyCode, modifiers = '') => {
- if (!e || !keyCode) {
- return false;
- }
-
- const leader = getPlatformLeaderKey();
- const mods = modifiers.toLowerCase().replace('l', leader.charAt(0));
-
- // Match depressed modifier keys
- if (
- e.altKey !== mods.includes('a') ||
- e.ctrlKey !== mods.includes('c') ||
- e.metaKey !== mods.includes('m') ||
- e.shiftKey !== mods.includes('s')
- ) {
- return false;
- }
-
- // Match the depressed key
- return keyCode === (e.keyCode || e.which);
-};
-
export const contentTop = () => {
const perfBar = $('#js-peek').outerHeight() || 0;
const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
index e24fcf47d71..5e0f9b612a2 100644
--- a/app/assets/javascripts/lib/utils/keycodes.js
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -1,10 +1,4 @@
-export const BACKSPACE_KEY_CODE = 8;
-export const ENTER_KEY_CODE = 13;
-export const ESC_KEY_CODE = 27;
-export const SPACE_KEY_CODE = 32;
export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40;
-export const Y_KEY_CODE = 89;
-export const Z_KEY_CODE = 90;
-export const LEFT_BRACKET_KEY_CODE = 219;
-export const RIGHT_BRACKET_KEY_CODE = 221;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/lib/utils/undo_stack.js b/app/assets/javascripts/lib/utils/undo_stack.js
deleted file mode 100644
index 6cfdc2a0a0f..00000000000
--- a/app/assets/javascripts/lib/utils/undo_stack.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * UndoStack provides a custom implementation of an undo/redo engine. It was originally written for GitLab's Markdown editor (`gl_form.js`), whose rich text editing capabilities broke native browser undo/redo behaviour.
- *
- * UndoStack supports predictable undos/redos, debounced saves, maximum history length, and duplicate detection.
- *
- * Usage:
- * - `stack = new UndoStack();`
- * - Saves a state to the stack with `stack.save(state)`.
- * - Get the current state with `stack.current()`.
- * - Revert to the previous state with `stack.undo()`.
- * - Redo a previous undo with `stack.redo()`;
- * - Queue a future save with `stack.scheduleSave(state, delay)`. Useful for text editors.
- * - See the full undo history in `stack.history`.
- */
-export default class UndoStack {
- constructor(maxLength = 1000) {
- this.clear();
- this.maxLength = maxLength;
-
- // If you're storing reference-types in the undo stack, you might want to
- // reassign this property to some deep-equals function.
- this.comparator = (a, b) => a === b;
- }
-
- current() {
- if (this.cursor === -1) {
- return undefined;
- }
- return this.history[this.cursor];
- }
-
- isEmpty() {
- return this.history.length === 0;
- }
-
- clear() {
- this.clearPending();
- this.history = [];
- this.cursor = -1;
- }
-
- save(state) {
- this.clearPending();
- if (this.comparator(state, this.current())) {
- // Don't save state if it's the same as the current state
- return;
- }
-
- this.history.length = this.cursor + 1;
- this.history.push(state);
- this.cursor += 1;
-
- if (this.history.length > this.maxLength) {
- this.history.shift();
- this.cursor -= 1;
- }
- }
-
- scheduleSave(state, delay = 1000) {
- this.clearPending();
- this.pendingState = state;
- this.timeout = setTimeout(this.saveNow.bind(this), delay);
- }
-
- saveNow() {
- // Persists scheduled saves immediately
- this.save(this.pendingState);
- this.clearPending();
- }
-
- clearPending() {
- // Cancels any scheduled saves
- if (this.timeout) {
- clearTimeout(this.timeout);
- delete this.timeout;
- delete this.pendingState;
- }
- }
-
- canUndo() {
- return this.cursor > 0;
- }
-
- undo() {
- this.clearPending();
- if (!this.canUndo()) {
- return undefined;
- }
- this.cursor -= 1;
- return this.history[this.cursor];
- }
-
- canRedo() {
- return this.cursor >= 0 && this.cursor < this.history.length - 1;
- }
-
- redo() {
- this.clearPending();
- if (!this.canRedo()) {
- return undefined;
- }
- this.cursor += 1;
- return this.history[this.cursor];
- }
-}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 21c44b59520..8ce5b615795 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,6 +1,5 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
export default {
components: {
@@ -23,28 +22,8 @@ export default {
},
},
computed: {
- toolbarHelpHtml() {
- const mdLinkStart = `<a href="${this.markdownDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`;
- const actionsLinkStart = `<a href="${this.quickActionsDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`;
- const linkEnd = '</a>';
-
- if (this.markdownDocsPath && !this.quickActionsDocsPath) {
- return sprintf(
- s__('Editor|%{mdLinkStart}Markdown is supported%{mdLinkEnd}'),
- { mdLinkStart, mdLinkEnd: linkEnd },
- false,
- );
- } else if (this.markdownDocsPath && this.quickActionsDocsPath) {
- return sprintf(
- s__(
- 'Editor|%{mdLinkStart}Markdown%{mdLinkEnd} and %{actionsLinkStart}quick actions%{actionsLinkEnd} are supported',
- ),
- { mdLinkStart, mdLinkEnd: linkEnd, actionsLinkStart, actionsLinkEnd: linkEnd },
- false,
- );
- }
-
- return null;
+ hasQuickActionsDocsPath() {
+ return this.quickActionsDocsPath !== '';
},
},
};
@@ -53,7 +32,21 @@ export default {
<template>
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
- <span v-html="toolbarHelpHtml"></span>
+ <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{
+ __('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
+ </template>
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">