c111d121d6
It will be hooked up to the markdown editor later
190 lines
6.1 KiB
JavaScript
190 lines
6.1 KiB
JavaScript
import $ from 'jquery';
|
|
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);
|
|
// Disable autocomplete for keywords which do not have dataSources available
|
|
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
|
|
Object.keys(this.enableGFM).forEach(item => {
|
|
if (item !== 'emojis') {
|
|
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
|
|
this.setupForm();
|
|
this.form.data('glForm', this);
|
|
}
|
|
|
|
destroy() {
|
|
// Clean form listeners
|
|
this.clearEventListeners();
|
|
if (this.autoComplete) {
|
|
this.autoComplete.destroy();
|
|
}
|
|
this.form.data('glForm', null);
|
|
}
|
|
|
|
setupForm() {
|
|
const isNewForm = this.form.is(':not(.gfm-form)');
|
|
this.form.removeClass('js-new-note-form');
|
|
if (isNewForm) {
|
|
this.form.find('.div-dropzone').remove();
|
|
this.form.addClass('gfm-form');
|
|
// remove notify commit author checkbox for non-commit notes
|
|
gl.utils.disableButtonIfEmptyField(
|
|
this.form.find('.js-note-text'),
|
|
this.form.find('.js-comment-button, .js-note-new-discussion'),
|
|
);
|
|
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
|
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
|
|
dropzoneInput(this.form);
|
|
autosize(this.textarea);
|
|
}
|
|
// form and textarea event listeners
|
|
this.addEventListeners();
|
|
addMarkdownListeners(this.form);
|
|
this.form.show();
|
|
if (this.isAutosizeable) this.setupAutosize();
|
|
}
|
|
|
|
setupAutosize() {
|
|
this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this));
|
|
|
|
this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this));
|
|
|
|
setTimeout(() => {
|
|
autosize(this.textarea);
|
|
this.textarea.css('resize', 'vertical');
|
|
}, 0);
|
|
}
|
|
|
|
setHeightData() {
|
|
this.textarea.data('height', this.textarea.outerHeight());
|
|
}
|
|
|
|
destroyAutosize() {
|
|
const outerHeight = this.textarea.outerHeight();
|
|
|
|
if (this.textarea.data('height') === outerHeight) return;
|
|
|
|
autosize.destroy(this.textarea);
|
|
|
|
this.textarea.data('height', outerHeight);
|
|
this.textarea.outerHeight(outerHeight);
|
|
this.textarea.css('max-height', window.outerHeight);
|
|
}
|
|
|
|
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)
|
|
.closest('.md-area')
|
|
.addClass('is-focused');
|
|
});
|
|
this.textarea.on('blur', function blurTextArea() {
|
|
$(this)
|
|
.closest('.md-area')
|
|
.removeClass('is-focused');
|
|
});
|
|
this.textarea.on('keydown', e => this.handleKeyShortcuts(e.originalEvent));
|
|
}
|
|
}
|