DEV: Extract textarea text manipulation to mixin (#14201)

DEV: Extract textarea text manipulation to mixin (#14201)

diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js
index c6ebb29..1d337cb 100644
--- a/app/assets/javascripts/discourse/app/components/d-editor.js
+++ b/app/assets/javascripts/discourse/app/components/d-editor.js
@@ -1,18 +1,12 @@
 import { ajax } from "discourse/lib/ajax";
-import {
-  caretPosition,
-  clipboardHelpers,
-  determinePostReplaceSelection,
-  inCodeBlock,
-  safariHacksDisabled,
-} from "discourse/lib/utilities";
+import { caretPosition, inCodeBlock } from "discourse/lib/utilities";
 import discourseComputed, {
   observes,
   on,
 } from "discourse-common/utils/decorators";
 import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
 import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
-import { later, next, schedule, scheduleOnce } from "@ember/runloop";
+import { later, schedule, scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import I18n from "I18n";
 import Mousetrap from "mousetrap";
@@ -34,10 +28,10 @@ import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
 import { inject as service } from "@ember/service";
 import showModal from "discourse/lib/show-modal";
 import { siteDir } from "discourse/lib/text-direction";
-import toMarkdown from "discourse/lib/to-markdown";
 import { translations } from "pretty-text/emoji/data";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 import { action } from "@ember/object";
+import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
 
 // Our head can be a static string or a function that returns a string
 // based on input (like for numbered lists).
@@ -64,11 +58,6 @@ const FOUR_SPACES_INDENT = "4-spaces-indent";
 
 let _createCallbacks = [];
 
-const isInside = (text, regex) => {
-  const matches = text.match(regex);
-  return matches && matches.length % 2;
-};
-
 class Toolbar {
   constructor(opts) {
     const { siteSettings } = opts;
@@ -245,7 +234,7 @@ export function onToolbarCreate(func) {
   addToolbarCallback(func);
 }
 
-export default Component.extend({
+export default Component.extend(TextareaTextManipulation, {
   classNames: ["d-editor"],
   ready: false,
   lastSel: null,
@@ -255,6 +244,7 @@ export default Component.extend({
   emojiStore: service("emoji-store"),
   isEditorFocused: false,
   processPreview: true,
+  composerFocusSelector: "#reply-control .d-editor-input",
 
   @discourseComputed("placeholder")
   placeholderTranslated(placeholder) {
@@ -268,7 +258,7 @@ export default Component.extend({
     this.set("ready", true);
 
     if (this.autofocus) {
-      this.element.querySelector("textarea").focus();
+      this._textarea.focus();
     }
   },
 
@@ -281,15 +271,14 @@ export default Component.extend({
   didInsertElement() {
     this._super(...arguments);
 
-    const $editorInput = $(this.element.querySelector(".d-editor-input"));
-    this._applyEmojiAutocomplete($editorInput);
-    this._applyCategoryHashtagAutocomplete($editorInput);
+    this._textarea = this.element.querySelector("textarea.d-editor-input");
+    this._$textarea = $(this._textarea);
+    this._applyEmojiAutocomplete(this._$textarea);
+    this._applyCategoryHashtagAutocomplete(this._$textarea);
 
     scheduleOnce("afterRender", this, this._readyNow);
 
-    this._mouseTrap = new Mousetrap(
-      this.element.querySelector(".d-editor-input")
-    );
+    this._mouseTrap = new Mousetrap(this._textarea);
     const shortcuts = this.get("toolbar.shortcuts");
 
     Object.keys(shortcuts).forEach((sc) => {
@@ -338,14 +327,6 @@ export default Component.extend({
     }
   },
 
-  _insertBlock(text) {
-    this._addBlock(this._getSelected(), text);
-  },
-
-  _insertText(text, options) {
-    this._addText(this._getSelected(), text, options);
-  },
-
   @on("willDestroyElement")
   _shutDown() {
     if (this.composerEvents) {
@@ -479,7 +460,7 @@ export default Component.extend({
   _applyCategoryHashtagAutocomplete() {
     const siteSettings = this.siteSettings;
 
-    $(this.element.querySelector(".d-editor-input")).autocomplete({
+    this._$textarea.autocomplete({
       template: findRawTemplate("category-tag-autocomplete"),
       key: "#",
       afterComplete: (value) => {
@@ -501,12 +482,12 @@ export default Component.extend({
     });
   },
 
-  _applyEmojiAutocomplete($editorInput) {
+  _applyEmojiAutocomplete($textarea) {
     if (!this.siteSettings.enable_emoji) {
       return;
     }
 
-    $editorInput.autocomplete({
+    $textarea.autocomplete({
       template: findRawTemplate("emoji-selector-autocomplete"),
       key: ":",
       afterComplete: (text) => {
@@ -533,7 +514,7 @@ export default Component.extend({
           this.emojiStore.track(v.code);
           return `${v.code}:`;
         } else {
-          $editorInput.autocomplete({ cancel: true });
+          $textarea.autocomplete({ cancel: true });
           this.set("emojiPickerIsActive", true);
 
           schedule("afterRender", () => {
@@ -624,63 +605,6 @@ export default Component.extend({
     });
   },
 
-  _getSelected(trimLeading, opts) {
-    if (!this.ready || !this.element) {
-      return;
-    }
-
-    const textarea = this.element.querySelector("textarea.d-editor-input");
-    const value = textarea.value;
-    let start = textarea.selectionStart;
-    let end = textarea.selectionEnd;
-
-    // trim trailing spaces cause **test ** would be invalid
-    while (end > start && /\s/.test(value.charAt(end - 1))) {
-      end--;
-    }
-
-    if (trimLeading) {
-      // trim leading spaces cause ** test** would be invalid
-      while (end > start && /\s/.test(value.charAt(start))) {
-        start++;
-      }
-    }
-
-    const selVal = value.substring(start, end);
-    const pre = value.slice(0, start);
-    const post = value.slice(end);
-
-    if (opts && opts.lineVal) {
-      const lineVal = value.split("\n")[
-        value.substr(0, 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;
-      }
-
-      const textarea = this.element.querySelector("textarea.d-editor-input");
-      const $textarea = $(textarea);
-      textarea.selectionStart = from;
-      textarea.selectionEnd = from + length;
-      $textarea.trigger("change");
-      if (opts.scroll) {
-        const oldScrollPos = $textarea.scrollTop();
-        if (!this.capabilities.isIOS || safariHacksDisabled()) {
-          $textarea.focus();
-        }
-        $textarea.scrollTop(oldScrollPos);
-      }
-    });
-  },
-
   // perform the same operation over many lines of text
   _getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
     let operation = OP.NONE;
@@ -813,226 +737,13 @@ export default Component.extend({
     }
   },
 
-  _replaceText(oldVal, newVal, opts = {}) {
-    const val = this.value;
-    const needleStart = val.indexOf(oldVal);
-
-    if (needleStart === -1) {
-      // Nothing to replace.
-      return;
-    }
-
-    const textarea = this.element.querySelector("textarea.d-editor-input");
-
-    // Determine post-replace selection.
-    const newSelection = determinePostReplaceSelection({
-      selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
-      needle: { start: needleStart, end: needleStart + oldVal.length },
-      replacement: { start: needleStart, end: needleStart + newVal.length },
-    });
-
-    if (opts.index && opts.regex) {
-      let i = -1;
-      const newValue = val.replace(opts.regex, (match) => {
-        i++;
-        return i === opts.index ? newVal : match;
-      });
-      this.set("value", newValue);
-    } else {

[... diff too long, it was truncated ...]

GitHub sha: eba317b74e53ed3ac4175fdd523c02c8f3f8567f

This commit appears in #14201 which was approved by eviltrout. It was merged by markvanlan.

This commit has been mentioned on Discourse Meta. There might be relevant details there: