DEV: Move text area surround code out of d-editor (#15950)
This commit moves _getMultilineContents and _applySurround into TextareaTextManipulation, so other text area components using that mixin can benefit from them (such as the chat composer). It also creates a public function wrapper for many TextareaTextManipulation functions that should not have underscore prefixes because they are used outside the file. Will make follow-up PRs for each plugin/theme using those functions then a final follow-up core PR to fix these up.
This commit is contained in:
parent
c92e62a271
commit
6a5ef27eaa
|
@ -35,29 +35,15 @@ import { siteDir } from "discourse/lib/text-direction";
|
||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
|
import TextareaTextManipulation, {
|
||||||
|
getHead,
|
||||||
// Our head can be a static string or a function that returns a string
|
} from "discourse/mixins/textarea-text-manipulation";
|
||||||
// based on input (like for numbered lists).
|
|
||||||
function getHead(head, prev) {
|
|
||||||
if (typeof head === "string") {
|
|
||||||
return [head, head.length];
|
|
||||||
} else {
|
|
||||||
return getHead(head(prev));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getButtonLabel(labelKey, defaultLabel) {
|
function getButtonLabel(labelKey, defaultLabel) {
|
||||||
// use the Font Awesome icon if the label matches the default
|
// use the Font Awesome icon if the label matches the default
|
||||||
return I18n.t(labelKey) === defaultLabel ? null : labelKey;
|
return I18n.t(labelKey) === defaultLabel ? null : labelKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OP = {
|
|
||||||
NONE: 0,
|
|
||||||
REMOVED: 1,
|
|
||||||
ADDED: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
||||||
|
|
||||||
let _createCallbacks = [];
|
let _createCallbacks = [];
|
||||||
|
@ -285,8 +271,8 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._itsatrap.bind("tab", () => this._indentSelection("right"));
|
this._itsatrap.bind("tab", () => this.indentSelection("right"));
|
||||||
this._itsatrap.bind("shift+tab", () => this._indentSelection("left"));
|
this._itsatrap.bind("shift+tab", () => this.indentSelection("left"));
|
||||||
|
|
||||||
// disable clicking on links in the preview
|
// disable clicking on links in the preview
|
||||||
this.element
|
this.element
|
||||||
|
@ -294,13 +280,13 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
.addEventListener("click", this._handlePreviewLinkClick);
|
.addEventListener("click", this._handlePreviewLinkClick);
|
||||||
|
|
||||||
if (this.composerEvents) {
|
if (this.composerEvents) {
|
||||||
this.appEvents.on("composer:insert-block", this, "_insertBlock");
|
this.appEvents.on("composer:insert-block", this, "insertBlock");
|
||||||
this.appEvents.on("composer:insert-text", this, "_insertText");
|
this.appEvents.on("composer:insert-text", this, "insertText");
|
||||||
this.appEvents.on("composer:replace-text", this, "_replaceText");
|
this.appEvents.on("composer:replace-text", this, "replaceText");
|
||||||
this.appEvents.on(
|
this.appEvents.on(
|
||||||
"composer:indent-selected-text",
|
"composer:indent-selected-text",
|
||||||
this,
|
this,
|
||||||
"_indentSelection"
|
"indentSelection"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,13 +324,13 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
@on("willDestroyElement")
|
@on("willDestroyElement")
|
||||||
_shutDown() {
|
_shutDown() {
|
||||||
if (this.composerEvents) {
|
if (this.composerEvents) {
|
||||||
this.appEvents.off("composer:insert-block", this, "_insertBlock");
|
this.appEvents.off("composer:insert-block", this, "insertBlock");
|
||||||
this.appEvents.off("composer:insert-text", this, "_insertText");
|
this.appEvents.off("composer:insert-text", this, "insertText");
|
||||||
this.appEvents.off("composer:replace-text", this, "_replaceText");
|
this.appEvents.off("composer:replace-text", this, "replaceText");
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
"composer:indent-selected-text",
|
"composer:indent-selected-text",
|
||||||
this,
|
this,
|
||||||
"_indentSelection"
|
"indentSelection"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +463,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
key: "#",
|
key: "#",
|
||||||
afterComplete: (value) => {
|
afterComplete: (value) => {
|
||||||
this.set("value", value);
|
this.set("value", value);
|
||||||
schedule("afterRender", this, this._focusTextArea);
|
schedule("afterRender", this, this.focusTextArea);
|
||||||
},
|
},
|
||||||
transformComplete: (obj) => {
|
transformComplete: (obj) => {
|
||||||
return obj.text;
|
return obj.text;
|
||||||
|
@ -504,7 +490,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
key: ":",
|
key: ":",
|
||||||
afterComplete: (text) => {
|
afterComplete: (text) => {
|
||||||
this.set("value", text);
|
this.set("value", text);
|
||||||
schedule("afterRender", this, this._focusTextArea);
|
schedule("afterRender", this, this.focusTextArea);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: (text, cp) => {
|
onKeyUp: (text, cp) => {
|
||||||
|
@ -617,117 +603,9 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// perform the same operation over many lines of text
|
|
||||||
_getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
|
|
||||||
let operation = OP.NONE;
|
|
||||||
|
|
||||||
const applyEmptyLines = opts && opts.applyEmptyLines;
|
|
||||||
|
|
||||||
return lines
|
|
||||||
.map((l) => {
|
|
||||||
if (!applyEmptyLines && l.length === 0) {
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
operation !== OP.ADDED &&
|
|
||||||
((l.slice(0, hlen) === hval && tlen === 0) ||
|
|
||||||
(tail.length && l.slice(-tlen) === tail))
|
|
||||||
) {
|
|
||||||
operation = OP.REMOVED;
|
|
||||||
if (tlen === 0) {
|
|
||||||
const result = l.slice(hlen);
|
|
||||||
[hval, hlen] = getHead(head, hval);
|
|
||||||
return result;
|
|
||||||
} else if (l.slice(-tlen) === tail) {
|
|
||||||
const result = l.slice(hlen, -tlen);
|
|
||||||
[hval, hlen] = getHead(head, hval);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else if (operation === OP.NONE) {
|
|
||||||
operation = OP.ADDED;
|
|
||||||
} else if (operation === OP.REMOVED) {
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = `${hval}${l}${tail}`;
|
|
||||||
[hval, hlen] = getHead(head, hval);
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
},
|
|
||||||
|
|
||||||
_applySurround(sel, head, tail, exampleKey, opts) {
|
|
||||||
const pre = sel.pre;
|
|
||||||
const post = sel.post;
|
|
||||||
|
|
||||||
const tlen = tail.length;
|
|
||||||
if (sel.start === sel.end) {
|
|
||||||
if (tlen === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [hval, hlen] = getHead(head);
|
|
||||||
const example = I18n.t(`composer.${exampleKey}`);
|
|
||||||
this.set("value", `${pre}${hval}${example}${tail}${post}`);
|
|
||||||
this._selectText(pre.length + hlen, example.length);
|
|
||||||
} else if (opts && !opts.multiline) {
|
|
||||||
let [hval, hlen] = getHead(head);
|
|
||||||
|
|
||||||
if (opts.useBlockMode && sel.value.split("\n").length > 1) {
|
|
||||||
hval += "\n";
|
|
||||||
hlen += 1;
|
|
||||||
tail = `\n${tail}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
|
|
||||||
this.set(
|
|
||||||
"value",
|
|
||||||
`${pre.slice(0, -hlen)}${sel.value}${post.slice(tail.length)}`
|
|
||||||
);
|
|
||||||
this._selectText(sel.start - hlen, sel.value.length);
|
|
||||||
} else {
|
|
||||||
this.set("value", `${pre}${hval}${sel.value}${tail}${post}`);
|
|
||||||
this._selectText(sel.start + hlen, sel.value.length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const lines = sel.value.split("\n");
|
|
||||||
|
|
||||||
let [hval, hlen] = getHead(head);
|
|
||||||
if (
|
|
||||||
lines.length === 1 &&
|
|
||||||
pre.slice(-tlen) === tail &&
|
|
||||||
post.slice(0, hlen) === hval
|
|
||||||
) {
|
|
||||||
this.set(
|
|
||||||
"value",
|
|
||||||
`${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`
|
|
||||||
);
|
|
||||||
this._selectText(sel.start - hlen, sel.value.length);
|
|
||||||
} else {
|
|
||||||
const contents = this._getMultilineContents(
|
|
||||||
lines,
|
|
||||||
head,
|
|
||||||
hval,
|
|
||||||
hlen,
|
|
||||||
tail,
|
|
||||||
tlen,
|
|
||||||
opts
|
|
||||||
);
|
|
||||||
|
|
||||||
this.set("value", `${pre}${contents}${post}`);
|
|
||||||
if (lines.length === 1 && tlen > 0) {
|
|
||||||
this._selectText(sel.start + hlen, sel.value.length);
|
|
||||||
} else {
|
|
||||||
this._selectText(sel.start, contents.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_applyList(sel, head, exampleKey, opts) {
|
_applyList(sel, head, exampleKey, opts) {
|
||||||
if (sel.value.indexOf("\n") !== -1) {
|
if (sel.value.indexOf("\n") !== -1) {
|
||||||
this._applySurround(sel, head, "", exampleKey, opts);
|
this.applySurround(sel, head, "", exampleKey, opts);
|
||||||
} else {
|
} else {
|
||||||
const [hval, hlen] = getHead(head);
|
const [hval, hlen] = getHead(head);
|
||||||
if (sel.start === sel.end) {
|
if (sel.start === sel.end) {
|
||||||
|
@ -745,7 +623,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
||||||
|
|
||||||
this.set("value", `${preLines}${number}${post}`);
|
this.set("value", `${preLines}${number}${post}`);
|
||||||
this._selectText(preLines.length, number.length);
|
this.selectText(preLines.length, number.length);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -811,16 +689,16 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = this._getSelected(button.trimLeading);
|
const selected = this.getSelected(button.trimLeading);
|
||||||
const toolbarEvent = {
|
const toolbarEvent = {
|
||||||
selected,
|
selected,
|
||||||
selectText: (from, length) =>
|
selectText: (from, length) =>
|
||||||
this._selectText(from, length, { scroll: false }),
|
this.selectText(from, length, { scroll: false }),
|
||||||
applySurround: (head, tail, exampleKey, opts) =>
|
applySurround: (head, tail, exampleKey, opts) =>
|
||||||
this._applySurround(selected, head, tail, exampleKey, opts),
|
this.applySurround(selected, head, tail, exampleKey, opts),
|
||||||
applyList: (head, exampleKey, opts) =>
|
applyList: (head, exampleKey, opts) =>
|
||||||
this._applyList(selected, head, exampleKey, opts),
|
this._applyList(selected, head, exampleKey, opts),
|
||||||
addText: (text) => this._addText(selected, text),
|
addText: (text) => this.addText(selected, text),
|
||||||
getText: () => this.value,
|
getText: () => this.value,
|
||||||
toggleDirection: () => this._toggleDirection(),
|
toggleDirection: () => this._toggleDirection(),
|
||||||
};
|
};
|
||||||
|
@ -855,7 +733,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sel = this._getSelected("", { lineVal: true });
|
const sel = this.getSelected("", { lineVal: true });
|
||||||
const selValue = sel.value;
|
const selValue = sel.value;
|
||||||
const hasNewLine = selValue.indexOf("\n") !== -1;
|
const hasNewLine = selValue.indexOf("\n") !== -1;
|
||||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||||
|
@ -867,25 +745,20 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
if (isFourSpacesIndent) {
|
if (isFourSpacesIndent) {
|
||||||
const example = I18n.t(`composer.code_text`);
|
const example = I18n.t(`composer.code_text`);
|
||||||
this.set("value", `${sel.pre} ${example}${sel.post}`);
|
this.set("value", `${sel.pre} ${example}${sel.post}`);
|
||||||
return this._selectText(sel.pre.length + 4, example.length);
|
return this.selectText(sel.pre.length + 4, example.length);
|
||||||
} else {
|
} else {
|
||||||
return this._applySurround(
|
return this.applySurround(sel, "```\n", "\n```", "paste_code_text");
|
||||||
sel,
|
|
||||||
"```\n",
|
|
||||||
"\n```",
|
|
||||||
"paste_code_text"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this._applySurround(sel, "`", "`", "code_title");
|
return this.applySurround(sel, "`", "`", "code_title");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFourSpacesIndent) {
|
if (isFourSpacesIndent) {
|
||||||
return this._applySurround(sel, " ", "", "code_text");
|
return this.applySurround(sel, " ", "", "code_text");
|
||||||
} else {
|
} else {
|
||||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||||
return this._addText(
|
return this.addText(
|
||||||
sel,
|
sel,
|
||||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "I18n";
|
||||||
import Mixin from "@ember/object/mixin";
|
import Mixin from "@ember/object/mixin";
|
||||||
import { generateLinkifyFunction } from "discourse/lib/text";
|
import { generateLinkifyFunction } from "discourse/lib/text";
|
||||||
import toMarkdown from "discourse/lib/to-markdown";
|
import toMarkdown from "discourse/lib/to-markdown";
|
||||||
|
@ -14,6 +15,22 @@ import { next, schedule } from "@ember/runloop";
|
||||||
const INDENT_DIRECTION_LEFT = "left";
|
const INDENT_DIRECTION_LEFT = "left";
|
||||||
const INDENT_DIRECTION_RIGHT = "right";
|
const INDENT_DIRECTION_RIGHT = "right";
|
||||||
|
|
||||||
|
const OP = {
|
||||||
|
NONE: 0,
|
||||||
|
REMOVED: 1,
|
||||||
|
ADDED: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Our head can be a static string or a function that returns a string
|
||||||
|
// based on input (like for numbered lists).
|
||||||
|
export function getHead(head, prev) {
|
||||||
|
if (typeof head === "string") {
|
||||||
|
return [head, head.length];
|
||||||
|
} else {
|
||||||
|
return getHead(head(prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Mixin.create({
|
export default Mixin.create({
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -24,6 +41,13 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
// ensures textarea scroll position is correct
|
// ensures textarea scroll position is correct
|
||||||
|
//
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
focusTextArea() {
|
||||||
|
this._focusTextArea();
|
||||||
|
},
|
||||||
|
|
||||||
_focusTextArea() {
|
_focusTextArea() {
|
||||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||||
return;
|
return;
|
||||||
|
@ -37,12 +61,30 @@ export default Mixin.create({
|
||||||
this._textarea.focus();
|
this._textarea.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
insertBlock(text) {
|
||||||
|
this._insertBlock(text);
|
||||||
|
},
|
||||||
|
|
||||||
_insertBlock(text) {
|
_insertBlock(text) {
|
||||||
this._addBlock(this._getSelected(), text);
|
this._addBlock(this.getSelected(), text);
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
insertText(text, options) {
|
||||||
|
this._insertText(text, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
_insertText(text, options) {
|
_insertText(text, options) {
|
||||||
this._addText(this._getSelected(), text, options);
|
this._addText(this.getSelected(), text, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
getSelected(trimLeading, opts) {
|
||||||
|
return this._getSelected(trimLeading, opts);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getSelected(trimLeading, opts) {
|
_getSelected(trimLeading, opts) {
|
||||||
|
@ -80,6 +122,12 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
selectText(from, length, opts = { scroll: true }) {
|
||||||
|
this._selectText(from, length, opts);
|
||||||
|
},
|
||||||
|
|
||||||
_selectText(from, length, opts = { scroll: true }) {
|
_selectText(from, length, opts = { scroll: true }) {
|
||||||
next(() => {
|
next(() => {
|
||||||
if (!this.element) {
|
if (!this.element) {
|
||||||
|
@ -99,6 +147,12 @@ export default Mixin.create({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
replaceText(oldVal, newVal, opts = {}) {
|
||||||
|
this._replaceText(oldVal, newVal, opts);
|
||||||
|
},
|
||||||
|
|
||||||
_replaceText(oldVal, newVal, opts = {}) {
|
_replaceText(oldVal, newVal, opts = {}) {
|
||||||
const val = this.value;
|
const val = this.value;
|
||||||
const needleStart = val.indexOf(oldVal);
|
const needleStart = val.indexOf(oldVal);
|
||||||
|
@ -135,13 +189,127 @@ export default Mixin.create({
|
||||||
!opts.skipNewSelection
|
!opts.skipNewSelection
|
||||||
) {
|
) {
|
||||||
// Restore cursor.
|
// Restore cursor.
|
||||||
this._selectText(
|
this.selectText(
|
||||||
newSelection.start,
|
newSelection.start,
|
||||||
newSelection.end - newSelection.start
|
newSelection.end - newSelection.start
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
applySurround(sel, head, tail, exampleKey, opts) {
|
||||||
|
this._applySurround(sel, head, tail, exampleKey, opts);
|
||||||
|
},
|
||||||
|
|
||||||
|
_applySurround(sel, head, tail, exampleKey, opts) {
|
||||||
|
const pre = sel.pre;
|
||||||
|
const post = sel.post;
|
||||||
|
|
||||||
|
const tlen = tail.length;
|
||||||
|
if (sel.start === sel.end) {
|
||||||
|
if (tlen === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hval, hlen] = getHead(head);
|
||||||
|
const example = I18n.t(`composer.${exampleKey}`);
|
||||||
|
this.set("value", `${pre}${hval}${example}${tail}${post}`);
|
||||||
|
this.selectText(pre.length + hlen, example.length);
|
||||||
|
} else if (opts && !opts.multiline) {
|
||||||
|
let [hval, hlen] = getHead(head);
|
||||||
|
|
||||||
|
if (opts.useBlockMode && sel.value.split("\n").length > 1) {
|
||||||
|
hval += "\n";
|
||||||
|
hlen += 1;
|
||||||
|
tail = `\n${tail}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
|
||||||
|
this.set(
|
||||||
|
"value",
|
||||||
|
`${pre.slice(0, -hlen)}${sel.value}${post.slice(tail.length)}`
|
||||||
|
);
|
||||||
|
this.selectText(sel.start - hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
this.set("value", `${pre}${hval}${sel.value}${tail}${post}`);
|
||||||
|
this.selectText(sel.start + hlen, sel.value.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lines = sel.value.split("\n");
|
||||||
|
|
||||||
|
let [hval, hlen] = getHead(head);
|
||||||
|
if (
|
||||||
|
lines.length === 1 &&
|
||||||
|
pre.slice(-tlen) === tail &&
|
||||||
|
post.slice(0, hlen) === hval
|
||||||
|
) {
|
||||||
|
this.set(
|
||||||
|
"value",
|
||||||
|
`${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`
|
||||||
|
);
|
||||||
|
this.selectText(sel.start - hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
const contents = this._getMultilineContents(
|
||||||
|
lines,
|
||||||
|
head,
|
||||||
|
hval,
|
||||||
|
hlen,
|
||||||
|
tail,
|
||||||
|
tlen,
|
||||||
|
opts
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set("value", `${pre}${contents}${post}`);
|
||||||
|
if (lines.length === 1 && tlen > 0) {
|
||||||
|
this.selectText(sel.start + hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
this.selectText(sel.start, contents.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// perform the same operation over many lines of text
|
||||||
|
_getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
|
||||||
|
let operation = OP.NONE;
|
||||||
|
|
||||||
|
const applyEmptyLines = opts && opts.applyEmptyLines;
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((l) => {
|
||||||
|
if (!applyEmptyLines && l.length === 0) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
operation !== OP.ADDED &&
|
||||||
|
((l.slice(0, hlen) === hval && tlen === 0) ||
|
||||||
|
(tail.length && l.slice(-tlen) === tail))
|
||||||
|
) {
|
||||||
|
operation = OP.REMOVED;
|
||||||
|
if (tlen === 0) {
|
||||||
|
const result = l.slice(hlen);
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
} else if (l.slice(-tlen) === tail) {
|
||||||
|
const result = l.slice(hlen, -tlen);
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (operation === OP.NONE) {
|
||||||
|
operation = OP.ADDED;
|
||||||
|
} else if (operation === OP.REMOVED) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = `${hval}${l}${tail}`;
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
},
|
||||||
|
|
||||||
_addBlock(sel, text) {
|
_addBlock(sel, text) {
|
||||||
text = (text || "").trim();
|
text = (text || "").trim();
|
||||||
if (text.length === 0) {
|
if (text.length === 0) {
|
||||||
|
@ -172,6 +340,12 @@ export default Mixin.create({
|
||||||
schedule("afterRender", this, this._focusTextArea);
|
schedule("afterRender", this, this._focusTextArea);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
addText(sel, text, options) {
|
||||||
|
this._addText(sel, text, options);
|
||||||
|
},
|
||||||
|
|
||||||
_addText(sel, text, options) {
|
_addText(sel, text, options) {
|
||||||
if (options && options.ensureSpace) {
|
if (options && options.ensureSpace) {
|
||||||
if ((sel.pre + "").length > 0) {
|
if ((sel.pre + "").length > 0) {
|
||||||
|
@ -196,6 +370,12 @@ export default Mixin.create({
|
||||||
this._focusTextArea();
|
this._focusTextArea();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
extractTable(text) {
|
||||||
|
return this._extractTable(text);
|
||||||
|
},
|
||||||
|
|
||||||
_extractTable(text) {
|
_extractTable(text) {
|
||||||
if (text.endsWith("\n")) {
|
if (text.endsWith("\n")) {
|
||||||
text = text.substring(0, text.length - 1);
|
text = text.substring(0, text.length - 1);
|
||||||
|
@ -233,6 +413,12 @@ export default Mixin.create({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO (martin) clean up this indirection, functions used outside this
|
||||||
|
// file should not be prefixed with lowercase
|
||||||
|
isInside(text, regex) {
|
||||||
|
return this._isInside(text, regex);
|
||||||
|
},
|
||||||
|
|
||||||
_isInside(text, regex) {
|
_isInside(text, regex) {
|
||||||
const matches = text.match(regex);
|
const matches = text.match(regex);
|
||||||
return matches && matches.length % 2;
|
return matches && matches.length % 2;
|
||||||
|
@ -254,7 +440,7 @@ export default Mixin.create({
|
||||||
let html = clipboard.getData("text/html");
|
let html = clipboard.getData("text/html");
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
const selected = this._getSelected(null, { lineVal: true });
|
const selected = this.getSelected(null, { lineVal: true });
|
||||||
const { pre, value: selectedValue, lineVal } = selected;
|
const { pre, value: selectedValue, lineVal } = selected;
|
||||||
const isInlinePasting = pre.match(/[^\n]$/);
|
const isInlinePasting = pre.match(/[^\n]$/);
|
||||||
const isCodeBlock = this._isInside(pre, /(^|\n)```/g);
|
const isCodeBlock = this._isInside(pre, /(^|\n)```/g);
|
||||||
|
@ -349,12 +535,12 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_indentSelection(direction) {
|
indentSelection(direction) {
|
||||||
if (![INDENT_DIRECTION_LEFT, INDENT_DIRECTION_RIGHT].includes(direction)) {
|
if (![INDENT_DIRECTION_LEFT, INDENT_DIRECTION_RIGHT].includes(direction)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = this._getSelected(null, { lineVal: true });
|
const selected = this.getSelected(null, { lineVal: true });
|
||||||
const { lineVal } = selected;
|
const { lineVal } = selected;
|
||||||
let value = selected.value;
|
let value = selected.value;
|
||||||
|
|
||||||
|
@ -414,14 +600,14 @@ export default Mixin.create({
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
if (newValue.trim() !== "") {
|
if (newValue.trim() !== "") {
|
||||||
this._replaceText(value, newValue, { skipNewSelection: true });
|
this.replaceText(value, newValue, { skipNewSelection: true });
|
||||||
this._selectText(this.value.indexOf(newValue), newValue.length);
|
this.selectText(this.value.indexOf(newValue), newValue.length);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
emojiSelected(code) {
|
emojiSelected(code) {
|
||||||
let selected = this._getSelected();
|
let selected = this.getSelected();
|
||||||
const captures = selected.pre.match(/\B:(\w*)$/);
|
const captures = selected.pre.match(/\B:(\w*)$/);
|
||||||
|
|
||||||
if (isEmpty(captures)) {
|
if (isEmpty(captures)) {
|
||||||
|
|
Loading…
Reference in New Issue