2020-05-24 23:13:21 +05:30
|
|
|
/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
|
2018-05-09 12:01:36 +05:30
|
|
|
import $ from 'jquery';
|
2020-11-24 15:15:51 +05:30
|
|
|
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { insertText } from '~/lib/utils/common_utils';
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
const LINK_TAG_PATTERN = '[{text}](url)';
|
2022-08-27 11:52:29 +05:30
|
|
|
const INDENT_CHAR = ' ';
|
|
|
|
const INDENT_LENGTH = 2;
|
2018-12-13 13:39:08 +05:30
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
// at the start of a line, find any amount of whitespace followed by
|
|
|
|
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
|
|
|
|
// OR a number with a . after it and an optional checkbox ([ ] [x])
|
|
|
|
// followed by one or more whitespace characters
|
2022-08-27 11:52:29 +05:30
|
|
|
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([xX~\s])\])?\s)(?<content>.)?/;
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
// detect a horizontal rule that might be mistaken for a list item (not full pattern for an <hr>)
|
|
|
|
const HR_PATTERN = /^((\s{0,3}-+\s*-+\s*-+\s*[\s-]*)|(\s{0,3}\*+\s*\*+\s*\*+\s*[\s*]*))$/;
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
let compositioningNoteText = false;
|
|
|
|
|
2018-05-09 12:01:36 +05:30
|
|
|
function selectedText(text, textarea) {
|
2018-03-17 18:26:18 +05:30
|
|
|
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
function addBlockTags(blockTag, selected) {
|
|
|
|
return `${blockTag}\n${selected}\n${blockTag}`;
|
|
|
|
}
|
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
/**
|
|
|
|
* Returns the line of text that is before the first line
|
|
|
|
* of the current selection
|
|
|
|
*
|
|
|
|
* @param {String} text - the text of the targeted text area
|
|
|
|
* @param {Object} textArea - the targeted text area
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
|
|
|
function lineBeforeSelection(text, textArea) {
|
|
|
|
let split = text.substring(0, textArea.selectionStart);
|
2022-04-04 11:22:00 +05:30
|
|
|
|
|
|
|
split = split.split('\n');
|
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
// Last item, at -1, is the line where the start of selection is.
|
|
|
|
// Line before selection is therefore at -2
|
|
|
|
const lineBefore = split[split.length - 2];
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
return lineBefore === undefined ? '' : lineBefore;
|
|
|
|
}
|
2022-05-07 20:08:51 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
/**
|
|
|
|
* Returns the line of text that is after the last line
|
|
|
|
* of the current selection
|
|
|
|
*
|
|
|
|
* @param {String} text - the text of the targeted text area
|
|
|
|
* @param {Object} textArea - the targeted text area
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
|
|
|
function lineAfterSelection(text, textArea) {
|
|
|
|
let split = text.substring(textArea.selectionEnd);
|
2022-05-07 20:08:51 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
// remove possible leading newline to get at the real line
|
|
|
|
split = split.replace(/^\n/, '');
|
2022-05-07 20:08:51 +05:30
|
|
|
split = split.split('\n');
|
|
|
|
|
|
|
|
return split[0];
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
/**
|
|
|
|
* Returns the text lines that encompass the current selection
|
|
|
|
*
|
|
|
|
* @param {Object} textArea - the targeted text area
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
function linesFromSelection(textArea) {
|
|
|
|
const text = textArea.value;
|
|
|
|
const { selectionStart, selectionEnd } = textArea;
|
|
|
|
|
|
|
|
let startPos = text[selectionStart] === '\n' ? selectionStart - 1 : selectionStart;
|
|
|
|
startPos = text.lastIndexOf('\n', startPos) + 1;
|
|
|
|
|
|
|
|
let endPos = selectionEnd === selectionStart ? selectionEnd : selectionEnd - 1;
|
|
|
|
endPos = text.indexOf('\n', endPos);
|
|
|
|
if (endPos < 0) endPos = text.length;
|
|
|
|
|
|
|
|
const selectedRange = text.substring(startPos, endPos);
|
|
|
|
const lines = selectedRange.split('\n');
|
|
|
|
|
|
|
|
return {
|
|
|
|
lines,
|
|
|
|
selectionStart,
|
|
|
|
selectionEnd,
|
|
|
|
startPos,
|
|
|
|
endPos,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the selection of a textarea such that it maintains the
|
|
|
|
* previous selection before the lines were indented/outdented
|
|
|
|
*
|
|
|
|
* @param {Object} textArea - the targeted text area
|
|
|
|
* @param {Number} selectionStart - start position of original selection
|
|
|
|
* @param {Number} selectionEnd - end position of original selection
|
|
|
|
* @param {Number} lineStart - start pos of first line
|
|
|
|
* @param {Number} firstLineChange - number of characters changed on first line
|
|
|
|
* @param {Number} totalChanged - total number of characters changed
|
|
|
|
*/
|
|
|
|
function setNewSelectionRange(
|
|
|
|
textArea,
|
|
|
|
selectionStart,
|
|
|
|
selectionEnd,
|
|
|
|
lineStart,
|
|
|
|
firstLineChange,
|
|
|
|
totalChanged,
|
|
|
|
) {
|
|
|
|
let newStart = Math.max(lineStart, selectionStart + firstLineChange);
|
|
|
|
let newEnd = Math.max(lineStart, selectionEnd + totalChanged);
|
|
|
|
|
|
|
|
if (selectionStart === selectionEnd) {
|
|
|
|
newEnd = newStart;
|
|
|
|
} else if (selectionStart === lineStart) {
|
|
|
|
newStart = lineStart;
|
|
|
|
}
|
|
|
|
|
|
|
|
textArea.setSelectionRange(newStart, newEnd);
|
|
|
|
}
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
function convertMonacoSelectionToAceFormat(sel) {
|
|
|
|
return {
|
|
|
|
start: {
|
|
|
|
row: sel.startLineNumber,
|
|
|
|
column: sel.startColumn,
|
|
|
|
},
|
|
|
|
end: {
|
|
|
|
row: sel.endLineNumber,
|
|
|
|
column: sel.endColumn,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getEditorSelectionRange(editor) {
|
2021-01-03 14:25:43 +05:30
|
|
|
return convertMonacoSelectionToAceFormat(editor.getSelection());
|
2020-07-28 23:09:34 +05:30
|
|
|
}
|
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
function editorBlockTagText(text, blockTag, selected, editor) {
|
|
|
|
const lines = text.split('\n');
|
2020-07-28 23:09:34 +05:30
|
|
|
const selectionRange = getEditorSelectionRange(editor);
|
2019-02-15 15:39:39 +05:30
|
|
|
const shouldRemoveBlock =
|
|
|
|
lines[selectionRange.start.row - 1] === blockTag &&
|
|
|
|
lines[selectionRange.end.row + 1] === blockTag;
|
|
|
|
|
|
|
|
if (shouldRemoveBlock) {
|
|
|
|
if (blockTag !== null) {
|
|
|
|
const lastLine = lines[selectionRange.end.row + 1];
|
|
|
|
const rangeWithBlockTags = new Range(
|
|
|
|
lines[selectionRange.start.row - 1],
|
|
|
|
0,
|
|
|
|
selectionRange.end.row + 1,
|
|
|
|
lastLine.length,
|
|
|
|
);
|
|
|
|
editor.getSelection().setSelectionRange(rangeWithBlockTags);
|
|
|
|
}
|
|
|
|
return selected;
|
|
|
|
}
|
|
|
|
return addBlockTags(blockTag, selected);
|
|
|
|
}
|
|
|
|
|
2018-05-09 12:01:36 +05:30
|
|
|
function blockTagText(text, textArea, blockTag, selected) {
|
2019-02-15 15:39:39 +05:30
|
|
|
const shouldRemoveBlock =
|
2022-08-27 11:52:29 +05:30
|
|
|
lineBeforeSelection(text, textArea) === blockTag &&
|
|
|
|
lineAfterSelection(text, textArea) === blockTag;
|
2019-02-15 15:39:39 +05:30
|
|
|
|
|
|
|
if (shouldRemoveBlock) {
|
2018-03-17 18:26:18 +05:30
|
|
|
// To remove the block tag we have to select the line before & after
|
|
|
|
if (blockTag != null) {
|
|
|
|
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
|
|
|
|
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
|
|
|
|
}
|
|
|
|
return selected;
|
|
|
|
}
|
2019-02-15 15:39:39 +05:30
|
|
|
return addBlockTags(blockTag, selected);
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
function moveCursor({
|
|
|
|
textArea,
|
|
|
|
tag,
|
|
|
|
cursorOffset,
|
|
|
|
positionBetweenTags,
|
|
|
|
removedLastNewLine,
|
|
|
|
select,
|
|
|
|
editor,
|
|
|
|
editorSelectionStart,
|
|
|
|
editorSelectionEnd,
|
|
|
|
}) {
|
2020-01-01 13:55:28 +05:30
|
|
|
let pos;
|
2019-02-15 15:39:39 +05:30
|
|
|
if (textArea && !textArea.setSelectionRange) {
|
2018-05-09 12:01:36 +05:30
|
|
|
return;
|
|
|
|
}
|
2018-12-05 23:21:45 +05:30
|
|
|
if (select && select.length > 0) {
|
2019-02-15 15:39:39 +05:30
|
|
|
if (textArea) {
|
|
|
|
// calculate the part of the text to be selected
|
|
|
|
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
|
|
|
|
const endPosition = startPosition + select.length;
|
|
|
|
return textArea.setSelectionRange(startPosition, endPosition);
|
|
|
|
} else if (editor) {
|
2021-01-03 14:25:43 +05:30
|
|
|
editor.selectWithinSelection(select, tag);
|
2019-02-15 15:39:39 +05:30
|
|
|
return;
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
2019-02-15 15:39:39 +05:30
|
|
|
}
|
|
|
|
if (textArea) {
|
|
|
|
if (textArea.selectionStart === textArea.selectionEnd) {
|
|
|
|
if (positionBetweenTags) {
|
|
|
|
pos = textArea.selectionStart - tag.length;
|
|
|
|
} else {
|
|
|
|
pos = textArea.selectionStart;
|
|
|
|
}
|
2018-05-09 12:01:36 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
if (removedLastNewLine) {
|
|
|
|
pos -= 1;
|
|
|
|
}
|
2018-05-09 12:01:36 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
if (cursorOffset) {
|
|
|
|
pos -= cursorOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
return textArea.setSelectionRange(pos, pos);
|
|
|
|
}
|
|
|
|
} else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
|
|
|
|
if (positionBetweenTags) {
|
2021-01-03 14:25:43 +05:30
|
|
|
editor.moveCursor(tag.length * -1);
|
2019-02-15 15:39:39 +05:30
|
|
|
}
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
export function insertMarkdownText({
|
|
|
|
textArea,
|
|
|
|
text,
|
|
|
|
tag,
|
|
|
|
cursorOffset,
|
|
|
|
blockTag,
|
|
|
|
selected = '',
|
|
|
|
wrap,
|
|
|
|
select,
|
|
|
|
editor,
|
|
|
|
}) {
|
2020-01-01 13:55:28 +05:30
|
|
|
let removedLastNewLine = false;
|
|
|
|
let removedFirstNewLine = false;
|
|
|
|
let currentLineEmpty = false;
|
|
|
|
let editorSelectionStart;
|
|
|
|
let editorSelectionEnd;
|
|
|
|
let lastNewLine;
|
|
|
|
let textToInsert;
|
2021-01-03 14:25:43 +05:30
|
|
|
selected = selected.toString();
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
if (editor) {
|
2020-07-28 23:09:34 +05:30
|
|
|
const selectionRange = getEditorSelectionRange(editor);
|
2019-02-15 15:39:39 +05:30
|
|
|
|
|
|
|
editorSelectionStart = selectionRange.start;
|
|
|
|
editorSelectionEnd = selectionRange.end;
|
|
|
|
}
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
// check for link pattern and selected text is an URL
|
|
|
|
// if so fill in the url part instead of the text part of the pattern.
|
|
|
|
if (tag === LINK_TAG_PATTERN) {
|
|
|
|
if (URL) {
|
|
|
|
try {
|
2022-10-11 01:57:18 +05:30
|
|
|
const url = new URL(selected);
|
|
|
|
|
|
|
|
if (url.origin !== 'null' || url.origin === null) {
|
|
|
|
tag = '[text]({text})';
|
|
|
|
select = 'text';
|
|
|
|
}
|
2018-12-13 13:39:08 +05:30
|
|
|
} catch (e) {
|
|
|
|
// ignore - no valid url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
// Remove the first newline
|
|
|
|
if (selected.indexOf('\n') === 0) {
|
|
|
|
removedFirstNewLine = true;
|
|
|
|
selected = selected.replace(/\n+/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the last newline
|
2019-02-15 15:39:39 +05:30
|
|
|
if (textArea) {
|
|
|
|
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
|
|
|
|
removedLastNewLine = true;
|
|
|
|
selected = selected.replace(/\n$/, '');
|
|
|
|
}
|
|
|
|
} else if (editor) {
|
|
|
|
if (editorSelectionStart.row !== editorSelectionEnd.row) {
|
|
|
|
removedLastNewLine = true;
|
|
|
|
selected = selected.replace(/\n$/, '');
|
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
const selectedSplit = selected.split('\n');
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
if (editor && !wrap) {
|
|
|
|
lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
|
|
|
|
|
|
|
|
if (/^\s*$/.test(lastNewLine)) {
|
|
|
|
currentLineEmpty = true;
|
|
|
|
}
|
|
|
|
} else if (textArea && !wrap) {
|
2018-03-17 18:26:18 +05:30
|
|
|
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
|
|
|
|
|
|
|
|
// Check whether the current line is empty or consists only of spaces(=handle as empty)
|
|
|
|
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
|
|
|
|
currentLineEmpty = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
const isBeginning =
|
|
|
|
(textArea && textArea.selectionStart === 0) ||
|
|
|
|
(editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
const startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
|
2018-12-05 23:21:45 +05:30
|
|
|
const textPlaceholder = '{text}';
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
|
|
|
|
if (blockTag != null && blockTag !== '') {
|
2019-02-15 15:39:39 +05:30
|
|
|
textToInsert = editor
|
|
|
|
? editorBlockTagText(text, blockTag, selected, editor)
|
|
|
|
: blockTagText(text, textArea, blockTag, selected);
|
2018-03-17 18:26:18 +05:30
|
|
|
} else {
|
2018-12-13 13:39:08 +05:30
|
|
|
textToInsert = selectedSplit
|
2021-03-08 18:12:59 +05:30
|
|
|
.map((val) => {
|
2018-12-13 13:39:08 +05:30
|
|
|
if (tag.indexOf(textPlaceholder) > -1) {
|
|
|
|
return tag.replace(textPlaceholder, val);
|
|
|
|
}
|
|
|
|
if (val.indexOf(tag) === 0) {
|
2019-09-04 21:01:54 +05:30
|
|
|
return String(val.replace(tag, ''));
|
2018-12-13 13:39:08 +05:30
|
|
|
}
|
2020-05-24 23:13:21 +05:30
|
|
|
return String(tag) + val;
|
2018-12-13 13:39:08 +05:30
|
|
|
})
|
|
|
|
.join('\n');
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
2018-12-05 23:21:45 +05:30
|
|
|
} else if (tag.indexOf(textPlaceholder) > -1) {
|
2021-11-11 11:23:49 +05:30
|
|
|
textToInsert = tag.replace(textPlaceholder, () =>
|
2021-12-11 22:18:48 +05:30
|
|
|
selected.replace(/\\n/g, '\n').replace(/%br/g, '\\n'),
|
2021-11-11 11:23:49 +05:30
|
|
|
);
|
2018-03-17 18:26:18 +05:30
|
|
|
} else {
|
2020-05-24 23:13:21 +05:30
|
|
|
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
if (removedFirstNewLine) {
|
2019-12-21 20:55:43 +05:30
|
|
|
textToInsert = `\n${textToInsert}`;
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
if (removedLastNewLine) {
|
2018-05-09 12:01:36 +05:30
|
|
|
textToInsert += '\n';
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
if (editor) {
|
2021-01-03 14:25:43 +05:30
|
|
|
editor.replaceSelectedText(textToInsert, select);
|
2019-02-15 15:39:39 +05:30
|
|
|
} else {
|
|
|
|
insertText(textArea, textToInsert);
|
|
|
|
}
|
2018-12-13 13:39:08 +05:30
|
|
|
return moveCursor({
|
|
|
|
textArea,
|
|
|
|
tag: tag.replace(textPlaceholder, selected),
|
2019-02-15 15:39:39 +05:30
|
|
|
cursorOffset,
|
|
|
|
positionBetweenTags: wrap && selected.length === 0,
|
2018-12-13 13:39:08 +05:30
|
|
|
removedLastNewLine,
|
|
|
|
select,
|
2019-02-15 15:39:39 +05:30
|
|
|
editor,
|
|
|
|
editorSelectionStart,
|
|
|
|
editorSelectionEnd,
|
2018-12-13 13:39:08 +05:30
|
|
|
});
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
|
2020-01-01 13:55:28 +05:30
|
|
|
const $textArea = $(textArea);
|
2018-03-17 18:26:18 +05:30
|
|
|
textArea = $textArea.get(0);
|
2020-01-01 13:55:28 +05:30
|
|
|
const text = $textArea.val();
|
|
|
|
const selected = selectedText(text, textArea) || tagContent;
|
2018-03-17 18:26:18 +05:30
|
|
|
$textArea.focus();
|
2019-02-15 15:39:39 +05:30
|
|
|
return insertMarkdownText({
|
|
|
|
textArea,
|
|
|
|
text,
|
|
|
|
tag,
|
|
|
|
cursorOffset,
|
|
|
|
blockTag,
|
|
|
|
selected,
|
|
|
|
wrap,
|
|
|
|
select,
|
|
|
|
});
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
/**
|
|
|
|
* Indents selected lines to the right by 2 spaces
|
|
|
|
*
|
2022-11-25 23:54:43 +05:30
|
|
|
* @param {Object} textArea - jQuery object with the targeted text area
|
2022-08-27 11:52:29 +05:30
|
|
|
*/
|
2022-11-25 23:54:43 +05:30
|
|
|
function indentLines($textArea) {
|
|
|
|
const textArea = $textArea.get(0);
|
2022-08-27 11:52:29 +05:30
|
|
|
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
|
|
|
|
const shiftedLines = [];
|
|
|
|
let totalAdded = 0;
|
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
textArea.focus();
|
2022-08-27 11:52:29 +05:30
|
|
|
textArea.setSelectionRange(startPos, endPos);
|
|
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
line = INDENT_CHAR.repeat(INDENT_LENGTH) + line;
|
|
|
|
totalAdded += INDENT_LENGTH;
|
|
|
|
|
|
|
|
shiftedLines.push(line);
|
|
|
|
});
|
|
|
|
|
|
|
|
const textToInsert = shiftedLines.join('\n');
|
|
|
|
|
|
|
|
insertText(textArea, textToInsert);
|
|
|
|
setNewSelectionRange(textArea, selectionStart, selectionEnd, startPos, INDENT_LENGTH, totalAdded);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Outdents selected lines to the left by 2 spaces
|
|
|
|
*
|
|
|
|
* @param {Object} textArea - the targeted text area
|
|
|
|
*/
|
2022-11-25 23:54:43 +05:30
|
|
|
function outdentLines($textArea) {
|
|
|
|
const textArea = $textArea.get(0);
|
2022-08-27 11:52:29 +05:30
|
|
|
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
|
|
|
|
const shiftedLines = [];
|
|
|
|
let totalRemoved = 0;
|
|
|
|
let removedFromFirstline = -1;
|
|
|
|
let removedFromLine = 0;
|
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
textArea.focus();
|
2022-08-27 11:52:29 +05:30
|
|
|
textArea.setSelectionRange(startPos, endPos);
|
|
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
removedFromLine = 0;
|
|
|
|
|
|
|
|
if (line.length > 0) {
|
|
|
|
// need to count how many spaces are actually removed, so can't use `replace`
|
|
|
|
while (removedFromLine < INDENT_LENGTH && line[removedFromLine] === INDENT_CHAR) {
|
|
|
|
removedFromLine += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (removedFromLine > 0) {
|
|
|
|
line = line.slice(removedFromLine);
|
|
|
|
totalRemoved += removedFromLine;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (removedFromFirstline === -1) removedFromFirstline = removedFromLine;
|
|
|
|
shiftedLines.push(line);
|
|
|
|
});
|
|
|
|
|
|
|
|
const textToInsert = shiftedLines.join('\n');
|
|
|
|
|
|
|
|
if (totalRemoved > 0) insertText(textArea, textToInsert);
|
|
|
|
|
|
|
|
setNewSelectionRange(
|
|
|
|
textArea,
|
|
|
|
selectionStart,
|
|
|
|
selectionEnd,
|
|
|
|
startPos,
|
|
|
|
-removedFromFirstline,
|
|
|
|
-totalRemoved,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
/* eslint-disable @gitlab/require-i18n-strings */
|
2022-04-04 11:22:00 +05:30
|
|
|
function handleSurroundSelectedText(e, textArea) {
|
2021-04-17 20:07:23 +05:30
|
|
|
if (!gon.markdown_surround_selection) return;
|
2022-11-25 23:54:43 +05:30
|
|
|
if (e.metaKey || e.ctrlKey) return;
|
2022-04-04 11:22:00 +05:30
|
|
|
if (textArea.selectionStart === textArea.selectionEnd) return;
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
const keys = {
|
|
|
|
'*': '**{text}**', // wraps with bold character
|
|
|
|
_: '_{text}_', // wraps with italic character
|
|
|
|
'`': '`{text}`', // wraps with inline character
|
|
|
|
"'": "'{text}'", // single quotes
|
|
|
|
'"': '"{text}"', // double quotes
|
|
|
|
'[': '[{text}]', // brackets
|
|
|
|
'{': '{{text}}', // braces
|
|
|
|
'(': '({text})', // parentheses
|
|
|
|
'<': '<{text}>', // angle brackets
|
|
|
|
};
|
|
|
|
const tag = keys[e.key];
|
|
|
|
|
|
|
|
if (tag) {
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
updateText({
|
|
|
|
tag,
|
2022-04-04 11:22:00 +05:30
|
|
|
textArea,
|
2020-11-24 15:15:51 +05:30
|
|
|
blockTag: '',
|
|
|
|
wrap: true,
|
|
|
|
select: '',
|
|
|
|
tagContent: '',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* eslint-enable @gitlab/require-i18n-strings */
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
/**
|
|
|
|
* Returns the content for a new line following a list item.
|
|
|
|
*
|
2022-08-27 11:52:29 +05:30
|
|
|
* @param {Object} listLineMatch - regex match of the current line
|
|
|
|
* @param {Object?} nextLineMatch - regex match of the next line
|
2022-05-07 20:08:51 +05:30
|
|
|
* @returns string with the new list item
|
|
|
|
*/
|
2022-08-27 11:52:29 +05:30
|
|
|
function continueOlText(listLineMatch, nextLineMatch) {
|
|
|
|
const { indent, leader } = listLineMatch.groups;
|
|
|
|
const { indent: nextIndent, isOl: nextIsOl } = nextLineMatch?.groups ?? {};
|
2022-05-07 20:08:51 +05:30
|
|
|
|
|
|
|
const [numStr, postfix = ''] = leader.split('.');
|
|
|
|
|
|
|
|
const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1;
|
|
|
|
const num = parseInt(numStr, 10) + incrementBy;
|
|
|
|
|
|
|
|
return `${indent}${num}.${postfix}`;
|
|
|
|
}
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
function handleContinueList(e, textArea) {
|
2022-11-25 23:54:43 +05:30
|
|
|
if (!gon.markdown_automatic_lists) return;
|
2022-04-04 11:22:00 +05:30
|
|
|
if (!(e.key === 'Enter')) return;
|
|
|
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
|
|
if (textArea.selectionStart !== textArea.selectionEnd) return;
|
2022-08-27 11:52:29 +05:30
|
|
|
// prevent unintended line breaks inserted using Japanese IME on MacOS
|
2022-07-16 23:28:13 +05:30
|
|
|
if (compositioningNoteText) return;
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
const firstSelectedLine = linesFromSelection(textArea).lines[0];
|
|
|
|
const listLineMatch = firstSelectedLine.match(LIST_LINE_HEAD_PATTERN);
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
if (listLineMatch) {
|
|
|
|
const { leader, indent, content, isOl } = listLineMatch.groups;
|
|
|
|
const emptyListItem = !content;
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
if (emptyListItem) {
|
|
|
|
// erase empty list item - select the text and allow the
|
|
|
|
// natural line feed to erase the text
|
|
|
|
textArea.selectionStart = textArea.selectionStart - listLineMatch[0].length;
|
2022-04-04 11:22:00 +05:30
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
let itemToInsert;
|
|
|
|
|
2022-06-21 17:19:12 +05:30
|
|
|
// Behaviors specific to either `ol` or `ul`
|
2022-05-07 20:08:51 +05:30
|
|
|
if (isOl) {
|
2022-08-27 11:52:29 +05:30
|
|
|
const nextLine = lineAfterSelection(textArea.value, textArea);
|
|
|
|
const nextLineMatch = nextLine.match(LIST_LINE_HEAD_PATTERN);
|
2022-05-07 20:08:51 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
itemToInsert = continueOlText(listLineMatch, nextLineMatch);
|
2022-05-07 20:08:51 +05:30
|
|
|
} else {
|
2022-08-27 11:52:29 +05:30
|
|
|
if (firstSelectedLine.match(HR_PATTERN)) return;
|
2022-06-21 17:19:12 +05:30
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
itemToInsert = `${indent}${leader}`;
|
|
|
|
}
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
itemToInsert = itemToInsert.replace(/\[[x~]\]/i, '[ ]');
|
2022-06-21 17:19:12 +05:30
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
updateText({
|
2022-05-07 20:08:51 +05:30
|
|
|
tag: itemToInsert,
|
2022-04-04 11:22:00 +05:30
|
|
|
textArea,
|
|
|
|
blockTag: '',
|
|
|
|
wrap: false,
|
|
|
|
select: '',
|
|
|
|
tagContent: '',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function keypressNoteText(e) {
|
|
|
|
const textArea = this;
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
if ($(textArea).atwho?.('isSelecting')) return;
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
handleContinueList(e, textArea);
|
|
|
|
handleSurroundSelectedText(e, textArea);
|
|
|
|
}
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
export function compositionStartNoteText() {
|
|
|
|
compositioningNoteText = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function compositionEndNoteText() {
|
|
|
|
compositioningNoteText = false;
|
|
|
|
}
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
export function updateTextForToolbarBtn($toolbarBtn) {
|
2022-11-25 23:54:43 +05:30
|
|
|
const $textArea = $toolbarBtn.closest('.md-area').find('textarea');
|
|
|
|
|
|
|
|
switch ($toolbarBtn.data('mdCommand')) {
|
|
|
|
case 'indentLines':
|
|
|
|
indentLines($textArea);
|
|
|
|
break;
|
|
|
|
case 'outdentLines':
|
|
|
|
outdentLines($textArea);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return updateText({
|
|
|
|
textArea: $textArea,
|
|
|
|
tag: $toolbarBtn.data('mdTag'),
|
|
|
|
cursorOffset: $toolbarBtn.data('mdCursorOffset'),
|
|
|
|
blockTag: $toolbarBtn.data('mdBlock'),
|
|
|
|
wrap: !$toolbarBtn.data('mdPrepend'),
|
|
|
|
select: $toolbarBtn.data('mdSelect'),
|
|
|
|
tagContent: $toolbarBtn.attr('data-md-tag-content'),
|
|
|
|
});
|
|
|
|
}
|
2020-11-24 15:15:51 +05:30
|
|
|
}
|
|
|
|
|
2018-05-09 12:01:36 +05:30
|
|
|
export function addMarkdownListeners(form) {
|
2020-11-24 15:15:51 +05:30
|
|
|
$('.markdown-area', form)
|
|
|
|
.on('keydown', keypressNoteText)
|
2022-07-16 23:28:13 +05:30
|
|
|
.on('compositionstart', compositionStartNoteText)
|
|
|
|
.on('compositionend', compositionEndNoteText)
|
2020-11-24 15:15:51 +05:30
|
|
|
.each(function attachTextareaShortcutHandlers() {
|
|
|
|
Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn);
|
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
// eslint-disable-next-line @gitlab/no-global-event-off
|
2020-11-24 15:15:51 +05:30
|
|
|
const $allToolbarBtns = $('.js-md', form)
|
2018-12-13 13:39:08 +05:30
|
|
|
.off('click')
|
2021-03-08 18:12:59 +05:30
|
|
|
.on('click', function () {
|
2020-11-24 15:15:51 +05:30
|
|
|
const $toolbarBtn = $(this);
|
|
|
|
|
|
|
|
return updateTextForToolbarBtn($toolbarBtn);
|
2019-02-15 15:39:39 +05:30
|
|
|
});
|
2020-11-24 15:15:51 +05:30
|
|
|
|
|
|
|
return $allToolbarBtns;
|
2019-02-15 15:39:39 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
export function addEditorMarkdownListeners(editor) {
|
2021-02-22 17:27:13 +05:30
|
|
|
// eslint-disable-next-line @gitlab/no-global-event-off
|
2019-02-15 15:39:39 +05:30
|
|
|
$('.js-md')
|
|
|
|
.off('click')
|
2021-03-08 18:12:59 +05:30
|
|
|
.on('click', (e) => {
|
2019-02-15 15:39:39 +05:30
|
|
|
const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data();
|
|
|
|
|
|
|
|
insertMarkdownText({
|
|
|
|
tag: mdTag,
|
|
|
|
blockTag: mdBlock,
|
|
|
|
wrap: !mdPrepend,
|
|
|
|
select: mdSelect,
|
|
|
|
selected: editor.getSelectedText(),
|
|
|
|
text: editor.getValue(),
|
|
|
|
editor,
|
2018-12-13 13:39:08 +05:30
|
|
|
});
|
2019-02-15 15:39:39 +05:30
|
|
|
editor.focus();
|
2018-12-13 13:39:08 +05:30
|
|
|
});
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2018-05-09 12:01:36 +05:30
|
|
|
export function removeMarkdownListeners(form) {
|
2020-11-24 15:15:51 +05:30
|
|
|
$('.markdown-area', form)
|
|
|
|
.off('keydown', keypressNoteText)
|
2022-07-16 23:28:13 +05:30
|
|
|
.off('compositionstart', compositionStartNoteText)
|
|
|
|
.off('compositionend', compositionEndNoteText)
|
2020-11-24 15:15:51 +05:30
|
|
|
.each(function removeTextareaShortcutHandlers() {
|
|
|
|
Shortcuts.removeMarkdownEditorShortcuts($(this));
|
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
// eslint-disable-next-line @gitlab/no-global-event-off
|
2018-03-17 18:26:18 +05:30
|
|
|
return $('.js-md', form).off('click');
|
2018-05-09 12:01:36 +05:30
|
|
|
}
|