summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/injections/js/bug1739489-draftjs-beforeinput.js
blob: 5ae55ec6f3236146df1106e45514b602940c26db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * Bug 1739489 - Entering an emoji using the MacOS IME "crashes" Draft.js editors.
 */

/* globals exportFunction */

console.info(
  "textInput event has been remapped to beforeinput for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1739489 for details."
);

window.wrappedJSObject.TextEvent = window.wrappedJSObject.InputEvent;

const { CustomEvent, Event, EventTarget } = window.wrappedJSObject;
var Remapped = [
  [CustomEvent, "constructor"],
  [Event, "constructor"],
  [Event, "initEvent"],
  [EventTarget, "addEventListener"],
  [EventTarget, "removeEventListener"],
];

for (const [obj, name] of Remapped) {
  const { prototype } = obj;
  const orig = prototype[name];
  Object.defineProperty(prototype, name, {
    value: exportFunction(function (type, b, c, d) {
      if (type?.toLowerCase() === "textinput") {
        type = "beforeinput";
      }
      return orig.call(this, type, b, c, d);
    }, window),
  });
}

if (location.host === "www.reddit.com") {
  (function () {
    const EditorCSS = ".public-DraftEditor-content[contenteditable=true]";
    let obsEditor, obsStart, obsText, obsKey, observer;
    const obsConfig = { characterData: true, childList: true, subtree: true };
    const obsHandler = () => {
      observer.disconnect();
      const finalTextNode = obsEditor.querySelector(
        `[data-offset-key="${obsKey}"] [data-text='true']`
      ).firstChild;
      const end = obsStart + obsText.length;
      window
        .getSelection()
        .setBaseAndExtent(finalTextNode, end, finalTextNode, end);
    };
    observer = new MutationObserver(obsHandler);

    document.documentElement.addEventListener(
      "beforeinput",
      e => {
        if (e.inputType != "insertFromPaste") {
          return;
        }
        const { target } = e;
        obsEditor = target.closest(EditorCSS);
        if (!obsEditor) {
          return;
        }
        const items = e?.dataTransfer.items;
        for (let item of items) {
          if (item.type === "text/plain") {
            e.preventDefault();
            item.getAsString(text => {
              obsText = text;

              // find the editor-managed <span> which contains the text node the
              // cursor starts on, and the cursor's location (or the selection start)
              const sel = window.getSelection();
              obsStart = sel.anchorOffset;
              let anchor = sel.anchorNode;
              if (!anchor.closest) {
                anchor = anchor.parentElement;
              }
              anchor = anchor.closest("[data-offset-key]");
              obsKey = anchor.getAttribute("data-offset-key");

              // set us up to wait for the editor to either update or replace the
              // <span> with that key (the one containing the text to be changed).
              // we will then make sure the cursor is after the pasted text, as if
              // the editor recreates the node, the cursor position is lost
              observer.observe(obsEditor, obsConfig);

              // force the editor to "paste". sending paste or other events will not
              // work, nor using execCommand (adding HTML will screw up the DOM that
              // the editor expects, and adding plain text will make it ignore newlines).
              target.dispatchEvent(
                new InputEvent("beforeinput", {
                  inputType: "insertText",
                  data: text,
                  bubbles: true,
                  cancelable: true,
                })
              );

              // blur the editor to force it to update/flush its state, because otherwise
              // the paste works, but the editor doesn't show it (until it is re-focused).
              obsEditor.blur();
            });
            break;
          }
        }
      },
      true
    );
  })();
}