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