DEV: refactor textarea text manipulation mixin (#29294)
Refactor of the TextareaTextManipulation from a Mixin to a native class
This commit is contained in:
parent
72f57524b4
commit
5d1e67b3e1
|
@ -101,6 +101,6 @@
|
|||
@isActive={{this.emojiPickerIsActive}}
|
||||
@isEditorFocused={{this.isEditorFocused}}
|
||||
@initialFilter={{this.emojiFilter}}
|
||||
@emojiSelected={{action "emojiSelected"}}
|
||||
@emojiSelected={{this.textManipulation.emojiSelected}}
|
||||
@onEmojiPickerClose={{this.onEmojiPickerClose}}
|
||||
/>
|
|
@ -1,5 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { schedule, scheduleOnce } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import ItsATrap from "@discourse/itsatrap";
|
||||
|
@ -21,14 +22,14 @@ import { linkSeenMentions } from "discourse/lib/link-mentions";
|
|||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
|
||||
import { siteDir } from "discourse/lib/text-direction";
|
||||
import TextareaTextManipulation, {
|
||||
getHead,
|
||||
} from "discourse/lib/textarea-text-manipulation";
|
||||
import {
|
||||
caretPosition,
|
||||
inCodeBlock,
|
||||
translateModKey,
|
||||
} from "discourse/lib/utilities";
|
||||
import TextareaTextManipulation, {
|
||||
getHead,
|
||||
} from "discourse/mixins/textarea-text-manipulation";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
@ -233,12 +234,12 @@ export function onToolbarCreate(func) {
|
|||
}
|
||||
|
||||
@classNames("d-editor")
|
||||
export default class DEditor extends Component.extend(
|
||||
TextareaTextManipulation
|
||||
) {
|
||||
export default class DEditor extends Component {
|
||||
@service("emoji-store") emojiStore;
|
||||
@service modal;
|
||||
|
||||
textManipulation;
|
||||
|
||||
ready = false;
|
||||
lastSel = null;
|
||||
showLink = true;
|
||||
|
@ -246,7 +247,6 @@ export default class DEditor extends Component.extend(
|
|||
emojiFilter = "";
|
||||
isEditorFocused = false;
|
||||
processPreview = true;
|
||||
composerFocusSelector = "#reply-control .d-editor-input";
|
||||
morphingOptions = {
|
||||
beforeAttributeUpdated: (element, attributeName) => {
|
||||
// Don't morph the open attribute of <details> elements
|
||||
|
@ -309,6 +309,15 @@ export default class DEditor extends Component.extend(
|
|||
|
||||
this._textarea = this.element.querySelector("textarea.d-editor-input");
|
||||
this._$textarea = $(this._textarea);
|
||||
|
||||
this.set(
|
||||
"textManipulation",
|
||||
new TextareaTextManipulation(getOwner(this), {
|
||||
markdownOptions: this.markdownOptions,
|
||||
textarea: this._textarea,
|
||||
})
|
||||
);
|
||||
|
||||
this._applyEmojiAutocomplete(this._$textarea);
|
||||
this._applyHashtagAutocomplete(this._$textarea);
|
||||
|
||||
|
@ -345,8 +354,12 @@ export default class DEditor extends Component.extend(
|
|||
});
|
||||
}
|
||||
|
||||
this._itsatrap.bind("tab", () => this.indentSelection("right"));
|
||||
this._itsatrap.bind("shift+tab", () => this.indentSelection("left"));
|
||||
this._itsatrap.bind("tab", () =>
|
||||
this.textManipulation.indentSelection("right")
|
||||
);
|
||||
this._itsatrap.bind("shift+tab", () =>
|
||||
this.textManipulation.indentSelection("left")
|
||||
);
|
||||
this._itsatrap.bind(`${PLATFORM_KEY_MODIFIER}+shift+.`, () =>
|
||||
this.send("insertCurrentTime")
|
||||
);
|
||||
|
@ -366,6 +379,8 @@ export default class DEditor extends Component.extend(
|
|||
this.onBeforeInputSmartList
|
||||
);
|
||||
this._textarea.addEventListener("input", this.onInputSmartList);
|
||||
|
||||
this.element.addEventListener("paste", this.textManipulation.paste);
|
||||
}
|
||||
|
||||
// disable clicking on links in the preview
|
||||
|
@ -374,13 +389,25 @@ export default class DEditor extends Component.extend(
|
|||
.addEventListener("click", this._handlePreviewLinkClick);
|
||||
|
||||
if (this.composerEvents) {
|
||||
this.appEvents.on("composer:insert-block", this, "insertBlock");
|
||||
this.appEvents.on("composer:insert-text", this, "insertText");
|
||||
this.appEvents.on("composer:replace-text", this, "replaceText");
|
||||
this.appEvents.on(
|
||||
"composer:insert-block",
|
||||
this.textManipulation,
|
||||
"insertBlock"
|
||||
);
|
||||
this.appEvents.on(
|
||||
"composer:insert-text",
|
||||
this.textManipulation,
|
||||
"insertText"
|
||||
);
|
||||
this.appEvents.on(
|
||||
"composer:replace-text",
|
||||
this.textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.on("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.on(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
this.textManipulation,
|
||||
"indentSelection"
|
||||
);
|
||||
}
|
||||
|
@ -396,7 +423,7 @@ export default class DEditor extends Component.extend(
|
|||
@bind
|
||||
onInputSmartList() {
|
||||
if (this.handleSmartListAutocomplete) {
|
||||
this.maybeContinueList();
|
||||
this.textManipulation.maybeContinueList();
|
||||
}
|
||||
this.handleSmartListAutocomplete = false;
|
||||
}
|
||||
|
@ -432,13 +459,25 @@ export default class DEditor extends Component.extend(
|
|||
@on("willDestroyElement")
|
||||
_shutDown() {
|
||||
if (this.composerEvents) {
|
||||
this.appEvents.off("composer:insert-block", this, "insertBlock");
|
||||
this.appEvents.off("composer:insert-text", this, "insertText");
|
||||
this.appEvents.off("composer:replace-text", this, "replaceText");
|
||||
this.appEvents.off(
|
||||
"composer:insert-block",
|
||||
this.textManipulation,
|
||||
"insertBlock"
|
||||
);
|
||||
this.appEvents.off(
|
||||
"composer:insert-text",
|
||||
this.textManipulation,
|
||||
"insertText"
|
||||
);
|
||||
this.appEvents.off(
|
||||
"composer:replace-text",
|
||||
this.textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.off("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.off(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
this.textManipulation,
|
||||
"indentSelection"
|
||||
);
|
||||
}
|
||||
|
@ -460,9 +499,7 @@ export default class DEditor extends Component.extend(
|
|||
|
||||
this._previewMutationObserver?.disconnect();
|
||||
|
||||
if (isTesting()) {
|
||||
this.element.removeEventListener("paste", this.paste);
|
||||
}
|
||||
this.element.removeEventListener("paste", this.textManipulation.paste);
|
||||
|
||||
this._cachedCookFunction = null;
|
||||
}
|
||||
|
@ -590,7 +627,11 @@ export default class DEditor extends Component.extend(
|
|||
{
|
||||
afterComplete: (value) => {
|
||||
this.set("value", value);
|
||||
schedule("afterRender", this, this.focusTextArea);
|
||||
schedule(
|
||||
"afterRender",
|
||||
this.textManipulation,
|
||||
this.textManipulation.blurAndFocus
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -606,7 +647,11 @@ export default class DEditor extends Component.extend(
|
|||
key: ":",
|
||||
afterComplete: (text) => {
|
||||
this.set("value", text);
|
||||
schedule("afterRender", this, this.focusTextArea);
|
||||
schedule(
|
||||
"afterRender",
|
||||
this.textManipulation,
|
||||
this.textManipulation.blurAndFocus
|
||||
);
|
||||
},
|
||||
|
||||
onKeyUp: (text, cp) => {
|
||||
|
@ -720,7 +765,7 @@ export default class DEditor extends Component.extend(
|
|||
|
||||
_applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.includes("\n")) {
|
||||
this.applySurround(sel, head, "", exampleKey, opts);
|
||||
this.textManipulation.applySurround(sel, head, "", exampleKey, opts);
|
||||
} else {
|
||||
const [hval, hlen] = getHead(head);
|
||||
if (sel.start === sel.end) {
|
||||
|
@ -737,13 +782,13 @@ export default class DEditor extends Component.extend(
|
|||
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
||||
|
||||
this.set("value", `${preLines}${number}${post}`);
|
||||
this.selectText(preLines.length, number.length);
|
||||
this.textManipulation.selectText(preLines.length, number.length);
|
||||
}
|
||||
}
|
||||
|
||||
_applySurround(head, tail, exampleKey, opts) {
|
||||
const selected = this.getSelected();
|
||||
this.applySurround(selected, head, tail, exampleKey, opts);
|
||||
const selected = this.textManipulation.getSelected();
|
||||
this.textManipulation.applySurround(selected, head, tail, exampleKey, opts);
|
||||
}
|
||||
|
||||
_toggleDirection() {
|
||||
|
@ -802,21 +847,27 @@ export default class DEditor extends Component.extend(
|
|||
}
|
||||
|
||||
newToolbarEvent(trimLeading) {
|
||||
const selected = this.getSelected(trimLeading);
|
||||
const selected = this.textManipulation.getSelected(trimLeading);
|
||||
return {
|
||||
selected,
|
||||
selectText: (from, length) =>
|
||||
this.selectText(from, length, { scroll: false }),
|
||||
this.textManipulation.selectText(from, length, { scroll: false }),
|
||||
applySurround: (head, tail, exampleKey, opts) =>
|
||||
this.applySurround(selected, head, tail, exampleKey, opts),
|
||||
this.textManipulation.applySurround(
|
||||
selected,
|
||||
head,
|
||||
tail,
|
||||
exampleKey,
|
||||
opts
|
||||
),
|
||||
applyList: (head, exampleKey, opts) =>
|
||||
this._applyList(selected, head, exampleKey, opts),
|
||||
formatCode: (...args) => this.send("formatCode", args),
|
||||
addText: (text) => this.addText(selected, text),
|
||||
addText: (text) => this.textManipulation.addText(selected, text),
|
||||
getText: () => this.value,
|
||||
toggleDirection: () => this._toggleDirection(),
|
||||
replaceText: (oldVal, newVal, opts) =>
|
||||
this.replaceText(oldVal, newVal, opts),
|
||||
this.textManipulation.replaceText(oldVal, newVal, opts),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -870,7 +921,7 @@ export default class DEditor extends Component.extend(
|
|||
return;
|
||||
}
|
||||
|
||||
const sel = this.getSelected("", { lineVal: true });
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.includes("\n");
|
||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||
|
@ -882,20 +933,33 @@ export default class DEditor extends Component.extend(
|
|||
if (isFourSpacesIndent) {
|
||||
const example = I18n.t(`composer.code_text`);
|
||||
this.set("value", `${sel.pre} ${example}${sel.post}`);
|
||||
return this.selectText(sel.pre.length + 4, example.length);
|
||||
return this.textManipulation.selectText(
|
||||
sel.pre.length + 4,
|
||||
example.length
|
||||
);
|
||||
} else {
|
||||
return this.applySurround(sel, "```\n", "\n```", "paste_code_text");
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
"```\n",
|
||||
"\n```",
|
||||
"paste_code_text"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return this.applySurround(sel, "`", "`", "code_title");
|
||||
return this.textManipulation.applySurround(sel, "`", "`", "code_title");
|
||||
}
|
||||
} else {
|
||||
if (isFourSpacesIndent) {
|
||||
return this.applySurround(sel, " ", "", "code_text");
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
" ",
|
||||
"",
|
||||
"code_text"
|
||||
);
|
||||
} else {
|
||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||
return this.addText(
|
||||
return this.textManipulation.addText(
|
||||
sel,
|
||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||
);
|
||||
|
@ -905,12 +969,15 @@ export default class DEditor extends Component.extend(
|
|||
|
||||
@action
|
||||
insertCurrentTime() {
|
||||
const sel = this.getSelected("", { lineVal: true });
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const time = moment().format("HH:mm:ss");
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
this.addText(sel, `[date=${date} time=${time} timezone="${timezone}"]`);
|
||||
this.textManipulation.addText(
|
||||
sel,
|
||||
`[date=${date} time=${time} timezone="${timezone}"]`
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { action } from "@ember/object";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import { setOwner } from "@ember/owner";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { generateLinkifyFunction } from "discourse/lib/text";
|
||||
import toMarkdown from "discourse/lib/to-markdown";
|
||||
|
@ -35,52 +36,48 @@ export function getHead(head, prev) {
|
|||
}
|
||||
}
|
||||
|
||||
export default Mixin.create({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
export default class TextareaTextManipulation {
|
||||
@service appEvents;
|
||||
@service siteSettings;
|
||||
@service capabilities;
|
||||
|
||||
// fallback in the off chance someone has implemented a custom composer
|
||||
// which does not define this
|
||||
if (!this.composerEventPrefix) {
|
||||
this.composerEventPrefix = "composer";
|
||||
}
|
||||
eventPrefix;
|
||||
textarea;
|
||||
|
||||
generateLinkifyFunction(this.markdownOptions || {}).then((linkify) => {
|
||||
constructor(owner, { markdownOptions, textarea, eventPrefix = "composer" }) {
|
||||
setOwner(this, owner);
|
||||
|
||||
this.eventPrefix = eventPrefix;
|
||||
this.textarea = textarea;
|
||||
|
||||
generateLinkifyFunction(markdownOptions || {}).then((linkify) => {
|
||||
// When pasting links, we should use the same rules to match links as we do when creating links for a cooked post.
|
||||
this._cachedLinkify = linkify;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.textarea.value;
|
||||
}
|
||||
|
||||
// ensures textarea scroll position is correct
|
||||
focusTextArea() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._textarea.blur();
|
||||
this._textarea.focus();
|
||||
},
|
||||
blurAndFocus() {
|
||||
this.textarea?.blur();
|
||||
this.textarea?.focus();
|
||||
}
|
||||
|
||||
insertBlock(text) {
|
||||
this._addBlock(this.getSelected(), text);
|
||||
},
|
||||
}
|
||||
|
||||
insertText(text, options) {
|
||||
this.addText(this.getSelected(), text, options);
|
||||
},
|
||||
}
|
||||
|
||||
getSelected(trimLeading, opts) {
|
||||
if (!this.ready || !this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this._textarea.value;
|
||||
let start = this._textarea.selectionStart;
|
||||
let end = this._textarea.selectionEnd;
|
||||
const value = this.value;
|
||||
let start = this.textarea.selectionStart;
|
||||
let end = this.textarea.selectionEnd;
|
||||
|
||||
// trim trailing spaces cause **test ** would be invalid
|
||||
while (end > start && /\s/.test(value.charAt(end - 1))) {
|
||||
|
@ -101,33 +98,30 @@ export default Mixin.create({
|
|||
if (opts && opts.lineVal) {
|
||||
const lineVal =
|
||||
value.split("\n")[
|
||||
value.slice(0, this._textarea.selectionStart).split("\n").length - 1
|
||||
value.slice(0, this.textarea.selectionStart).split("\n").length - 1
|
||||
];
|
||||
return { start, end, value: selVal, pre, post, lineVal };
|
||||
} else {
|
||||
return { start, end, value: selVal, pre, post };
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
selectText(from, length, opts = { scroll: true }) {
|
||||
next(() => {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
this._textarea.selectionStart = from;
|
||||
this._textarea.selectionEnd = from + length;
|
||||
this.textarea.selectionStart = from;
|
||||
this.textarea.selectionEnd = from + length;
|
||||
if (opts.scroll === true || typeof opts.scroll === "number") {
|
||||
const oldScrollPos =
|
||||
typeof opts.scroll === "number"
|
||||
? opts.scroll
|
||||
: this._textarea.scrollTop;
|
||||
: this.textarea.scrollTop;
|
||||
if (!this.capabilities.isIOS) {
|
||||
this._textarea.focus();
|
||||
this.textarea.focus();
|
||||
}
|
||||
this._textarea.scrollTop = oldScrollPos;
|
||||
this.textarea.scrollTop = oldScrollPos;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
replaceText(oldVal, newVal, opts = {}) {
|
||||
const val = this.value;
|
||||
|
@ -141,8 +135,8 @@ export default Mixin.create({
|
|||
// Determine post-replace selection.
|
||||
const newSelection = determinePostReplaceSelection({
|
||||
selection: {
|
||||
start: this._textarea.selectionStart,
|
||||
end: this._textarea.selectionEnd,
|
||||
start: this.textarea.selectionStart,
|
||||
end: this.textarea.selectionEnd,
|
||||
},
|
||||
needle: { start: needleStart, end: needleStart + oldVal.length },
|
||||
replacement: { start: needleStart, end: needleStart + newVal.length },
|
||||
|
@ -167,7 +161,7 @@ export default Mixin.create({
|
|||
}
|
||||
|
||||
if (
|
||||
(opts.forceFocus || this._$textarea.is(":focus")) &&
|
||||
(opts.forceFocus || this.textarea === document.activeElement) &&
|
||||
!opts.skipNewSelection
|
||||
) {
|
||||
// Restore cursor.
|
||||
|
@ -176,7 +170,7 @@ export default Mixin.create({
|
|||
newSelection.end - newSelection.start
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
applySurround(sel, head, tail, exampleKey, opts) {
|
||||
const pre = sel.pre;
|
||||
|
@ -240,7 +234,7 @@ export default Mixin.create({
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// perform the same operation over many lines of text
|
||||
_getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
|
||||
|
@ -280,7 +274,7 @@ export default Mixin.create({
|
|||
return result;
|
||||
})
|
||||
.join("\n");
|
||||
},
|
||||
}
|
||||
|
||||
_addBlock(sel, text) {
|
||||
text = (text || "").trim();
|
||||
|
@ -312,9 +306,9 @@ export default Mixin.create({
|
|||
}
|
||||
|
||||
this._insertAt(start, end, text);
|
||||
this._textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
schedule("afterRender", this, this.focusTextArea);
|
||||
},
|
||||
this.textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
schedule("afterRender", this, this.blurAndFocus);
|
||||
}
|
||||
|
||||
addText(sel, text, options) {
|
||||
if (options && options.ensureSpace) {
|
||||
|
@ -331,18 +325,18 @@ export default Mixin.create({
|
|||
}
|
||||
|
||||
this._insertAt(sel.start, sel.end, text);
|
||||
this.focusTextArea();
|
||||
},
|
||||
this.blurAndFocus();
|
||||
}
|
||||
|
||||
_insertAt(start, end, text) {
|
||||
this._textarea.setSelectionRange(start, end);
|
||||
this._textarea.focus();
|
||||
this.textarea.setSelectionRange(start, end);
|
||||
this.textarea.focus();
|
||||
if (start !== end && text === "") {
|
||||
document.execCommand("delete", false);
|
||||
} else {
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
extractTable(text) {
|
||||
if (text.endsWith("\n")) {
|
||||
|
@ -379,17 +373,16 @@ export default Mixin.create({
|
|||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
isInside(text, regex) {
|
||||
const matches = text.match(regex);
|
||||
return matches && matches.length % 2;
|
||||
},
|
||||
}
|
||||
|
||||
@bind
|
||||
paste(e) {
|
||||
const isComposer =
|
||||
document.querySelector(this.composerFocusSelector) === e.target;
|
||||
const isComposer = this.textarea === e.target;
|
||||
|
||||
if (!isComposer && !isTesting()) {
|
||||
return;
|
||||
|
@ -418,11 +411,8 @@ export default Mixin.create({
|
|||
plainText = plainText.replace(/\r/g, "");
|
||||
const table = this.extractTable(plainText);
|
||||
if (table) {
|
||||
this.composerEventPrefix
|
||||
? this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:insert-text`,
|
||||
table
|
||||
)
|
||||
this.eventPrefix
|
||||
? this.appEvents.trigger(`${this.eventPrefix}:insert-text`, table)
|
||||
: this.insertText(table);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -475,9 +465,9 @@ export default Mixin.create({
|
|||
}
|
||||
|
||||
if (isComposer) {
|
||||
this.composerEventPrefix
|
||||
this.eventPrefix
|
||||
? this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:insert-text`,
|
||||
`${this.eventPrefix}:insert-text`,
|
||||
markdown
|
||||
)
|
||||
: this.insertText(markdown);
|
||||
|
@ -489,7 +479,7 @@ export default Mixin.create({
|
|||
if (handled || (canUpload && !plainText)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided char from the provided str up
|
||||
|
@ -506,7 +496,7 @@ export default Mixin.create({
|
|||
}
|
||||
}
|
||||
return str;
|
||||
},
|
||||
}
|
||||
|
||||
_updateListNumbers(text, currentNumber) {
|
||||
return text
|
||||
|
@ -523,12 +513,12 @@ export default Mixin.create({
|
|||
return line;
|
||||
})
|
||||
.join("\n");
|
||||
},
|
||||
}
|
||||
|
||||
@bind
|
||||
maybeContinueList() {
|
||||
const offset = caretPosition(this._textarea);
|
||||
const text = this._textarea.value;
|
||||
const offset = caretPosition(this.textarea);
|
||||
const text = this.value;
|
||||
const lines = text.substring(0, offset).split("\n");
|
||||
|
||||
// Only continue if the previous line was a list item.
|
||||
|
@ -610,7 +600,7 @@ export default Mixin.create({
|
|||
numericBullet + 1
|
||||
);
|
||||
autocompletePrefix += autocompletePostfix;
|
||||
scrollPosition = this._textarea.scrollTop;
|
||||
scrollPosition = this.textarea.scrollTop;
|
||||
|
||||
this.replaceText(
|
||||
text.substring(offset, offset + autocompletePrefix.length),
|
||||
|
@ -636,7 +626,7 @@ export default Mixin.create({
|
|||
);
|
||||
this.selectText(offsetWithoutPrefix, 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@bind
|
||||
indentSelection(direction) {
|
||||
|
@ -711,7 +701,7 @@ export default Mixin.create({
|
|||
this.replaceText(value, newValue, { skipNewSelection: true });
|
||||
this.selectText(this.value.indexOf(newValue), newValue.length);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@action
|
||||
emojiSelected(code) {
|
||||
|
@ -732,9 +722,9 @@ export default Mixin.create({
|
|||
`${code}:`
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
isInsideCodeFence(beforeText) {
|
||||
return this.isInside(beforeText, /(^|\n)```/g);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,52 +3,52 @@ import EmberObject from "@ember/object";
|
|||
import { setOwner } from "@ember/owner";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
|
||||
import TextareaTextManipulation from "discourse/lib/textarea-text-manipulation";
|
||||
|
||||
// This class sole purpose is to provide a way to interact with the textarea
|
||||
// using the existing TextareaTextManipulation mixin without using it directly
|
||||
// in the composer component. It will make future migration easier.
|
||||
export default class TextareaInteractor extends EmberObject.extend(
|
||||
TextareaTextManipulation
|
||||
) {
|
||||
export default class TextareaInteractor extends EmberObject {
|
||||
@service capabilities;
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
|
||||
textManipulation;
|
||||
|
||||
constructor(owner, textarea) {
|
||||
super(...arguments);
|
||||
setOwner(this, owner);
|
||||
this.textarea = textarea;
|
||||
this._textarea = textarea;
|
||||
this.element = this._textarea;
|
||||
this.ready = true;
|
||||
this.composerFocusSelector = `#${textarea.id}`;
|
||||
this.element = textarea;
|
||||
|
||||
this.init(); // mixin init wouldn't be called otherwise
|
||||
this.composerEventPrefix = null; // we don't need app events
|
||||
this.textManipulation = new TextareaTextManipulation(owner, {
|
||||
textarea,
|
||||
// we don't need app events
|
||||
eventPrefix: null,
|
||||
});
|
||||
|
||||
// paste is using old native ember events defined on composer
|
||||
this.textarea.addEventListener("paste", this.paste);
|
||||
this.textarea.addEventListener("paste", this.textManipulation.paste);
|
||||
registerDestructor(this, (instance) => instance.teardown());
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.textarea.removeEventListener("paste", this.paste);
|
||||
this.textarea.removeEventListener("paste", this.textManipulation.paste);
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._textarea.value = value;
|
||||
this.textarea.value = value;
|
||||
const event = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
this._textarea.dispatchEvent(event);
|
||||
this.textarea.dispatchEvent(event);
|
||||
}
|
||||
|
||||
blur() {
|
||||
next(() => {
|
||||
schedule("afterRender", () => {
|
||||
this._textarea.blur();
|
||||
this.textarea.blur();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -69,19 +69,22 @@ export default class TextareaInteractor extends EmberObject.extend(
|
|||
}
|
||||
|
||||
if (opts.addText) {
|
||||
this.addText(this.getSelected(), opts.addText);
|
||||
this.textManipulation.addText(
|
||||
this.textManipulation.getSelected(),
|
||||
opts.addText
|
||||
);
|
||||
}
|
||||
|
||||
this.focusTextArea();
|
||||
this.textManipulation.blurAndFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ensureCaretAtEnd() {
|
||||
schedule("afterRender", () => {
|
||||
this._textarea.setSelectionRange(
|
||||
this._textarea.value.length,
|
||||
this._textarea.value.length
|
||||
this.textarea.setSelectionRange(
|
||||
this.textarea.value.length,
|
||||
this.textarea.value.length
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -90,11 +93,27 @@ export default class TextareaInteractor extends EmberObject.extend(
|
|||
schedule("afterRender", () => {
|
||||
// this is a quirk which forces us to `auto` first or textarea
|
||||
// won't resize
|
||||
this._textarea.style.height = "auto";
|
||||
this.textarea.style.height = "auto";
|
||||
|
||||
// +1 is to workaround a rounding error visible on electron
|
||||
// causing scrollbars to show when they shouldn’t
|
||||
this._textarea.style.height = this._textarea.scrollHeight + 1 + "px";
|
||||
this.textarea.style.height = this.textarea.scrollHeight + 1 + "px";
|
||||
});
|
||||
}
|
||||
|
||||
getSelected() {
|
||||
return this.textManipulation.getSelected(...arguments);
|
||||
}
|
||||
|
||||
applySurround() {
|
||||
return this.textManipulation.applySurround(...arguments);
|
||||
}
|
||||
|
||||
addText() {
|
||||
return this.textManipulation.addText(...arguments);
|
||||
}
|
||||
|
||||
isInside() {
|
||||
return this.textManipulation.isInside(...arguments);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue