以编程方式更改 Facebook 的可编辑 div 输入区域中的输入值

Programmatically change input value in Facebook's editable div input area

我正在尝试编写一个 Chrome 扩展,需要能够在输入字段中的光标位置插入一个字符。

当输入是实际的 HTMLInputElementinsertAtCaretInput 从另一个堆栈答案中借用时,这很容易):

function insertAtCaretInput(text) {
  text = text || '';
  if (document.selection) {
    // IE
    this.focus();
    var sel = document.selection.createRange();
    sel.text = text;
  } else if (this.selectionStart || this.selectionStart === 0) {
    // Others
    var startPos = this.selectionStart;
    var endPos = this.selectionEnd;
    this.value = this.value.substring(0, startPos) + text + this.value.substring(endPos, this.value.length);
    this.selectionStart = startPos + text.length;
    this.selectionEnd = startPos + text.length;
  } else {
    this.value += text;
  }
}

HTMLInputElement.prototype.insertAtCaret = insertAtCaretInput;

onKeyDown(e){
  ...
  targetElement = e.target;
  target.insertAtCaret(charToInsert);
  ...
}

但是当输入在 HTML 结构中实际表示不同时(例如 Facebook 有一个 <div><span> 元素出现并在奇怪的时间合并)我可以'不知道如何可靠地做到这一点。当我开始与输入交互时,新字符消失或改变位置或光标跳到不可预知的地方。

示例 HTML Facebook 的结构(Chrome 桌面页面、新 post 或消息输入字段)可编辑 <div> 包含字符串 Test :

<div data-offset-key="87o4u-0-0" class="_1mf _1mj">
  <span>
    <span data-offset-key="87o4u-0-0">
      <span data-text="true">Test
      </span>
    </span>
  </span>
  <span data-offset-key="87o4u-1-0">
    <span data-text="true"> 
    </span>
  </span>
</div>

这是我迄今为止最成功的尝试。我像这样扩展了 span 元素(insertTextAtCursor 也借鉴了另一个答案):

function insertTextAtCursor(text) {
  let selection = window.getSelection();
  let range = selection.getRangeAt(0);
  range.deleteContents();
  let node = document.createTextNode(text);
  range.insertNode(node);

  for (let position = 0; position != text.length; position++) {
    selection.modify('move', 'right', 'character');
  }
}

HTMLSpanElement.prototype.insertAtCaret = insertTextAtCursor;

并且由于触发按键事件的元素是 <div>,然后保存 <span> 元素,然后保存具有实际输入的文本节点,我找到最深的 <span> 元素并对该元素执行 insertAtCaret:

function findDeepestChild(parent) {
  var result = { depth: 0, element: parent };

  [].slice.call(parent.childNodes).forEach(function (child) {
    var childResult = findDeepestChild(child);
    if (childResult.depth + 1 > result.depth) {
      result = {
        depth: 1 + childResult.depth,
        element: childResult.element,
        parent: childResult.element.parentNode,
      };
    }
  });

  return result;
}

onKeyDown(e){
  ...
  targetElement = findDeepestChild(e.target).parent; // deepest child is a text node
  target.insertAtCaret(charToInsert);
  ...
}

上面的代码可以成功插入字符,但是当 Facebook 的幕后框架尝试处理新值时,奇怪的事情发生了。我尝试了各种技巧,重新定位光标并插入 <span> 元素,类似于 Facebook 在插入时操纵 dom 时似乎发生的情况,但最终,所有这些都以某种方式失败。我想这是因为输入区域的状态被保存在某个地方并且与我的修改不同步。

您认为可以可靠地做到这一点吗?如果可以,怎么做?理想情况下,答案不会特定于 Facebook,但也适用于使用其他元素而不是 HTMLInputElement 作为输入字段的其他页面,但我知道这可能是不可能的。

我在使用 Whatsapp 网页时遇到了类似的问题
尝试调度输入事件

target.dispatchEvent(new InputEvent('input', {bubbles: true}));

我制作了一个 Firefox extension 可以将上下文菜单中记住的注释粘贴到输入字段中。然而,Facebook Messenger 字段是大量脚本化的 div 和 span,而不是输入字段。我一直在努力让它们工作,并按照 @user9977151 的建议派发一个事件帮助了我!

然而,它需要从特定元素发送,并且您还需要检查您的 Facebook Messenger 输入字段是否为空。

空字段看起来像这样:

<div class="" data-block="true" data-editor="e1m9r" data-offset-key="6hbkl-0-0">
    <div data-offset-key="6hbkl-0-0" class="_1mf _1mj">
        <span data-offset-key="6hbkl-0-0">
            <br data-text="true">
        </span>
    </div>
</div>

而不是那样空虚

<div class="" data-block="true" data-editor="e1m9r" data-offset-key="6hbkl-0-0">
    <div data-offset-key="6hbkl-0-0" class="_1mf _1mj">
        <span data-offset-key="6hbkl-0-0">
            <span data-text="true">
                Some input
            </span>
        </span>
    </div>
</div>

事件需要从

发送
<span data-offset-key="6hbkl-0-0">

向非空字段添加内容很简单 - 您只需更改 innerText 并分派事件。

空字段比较棘手。通常,当用户输入内容时,<br data-text="true"> 会随着用户的输入而变为 <span data-text="true">。 我试过以编程方式执行此操作(添加带有 innerText 的跨度,删除 br)但它破坏了 Messenger 输入。对我有用的是添加一个跨度,分派事件然后删除它!在那之后,Facebook 像往常一样删除了 br,并在我的输入中添加了 span

Facebook 似乎以某种方式将用户按键存储在它的内存中,然后自己输入它们。

我的代码是

if(document.body.parentElement.id == "facebook"){
    var dc = getDeepestChild(actEl);
    var elementToDispatchEventFrom = dc.parentElement;
    let newEl;
    if(dc.nodeName.toLowerCase() == "br"){
        // attempt to paste into empty messenger field
        // by creating new element and setting it's value
        newEl = document.createElement("span");
        newEl.setAttribute("data-text", "true");
        dc.parentElement.appendChild(newEl);
        newEl.innerText = message.content;
    }else{
        // attempt to paste into not empty messenger field
        // by changing existing content
        let sel = document.getSelection();
        selStart = sel.anchorOffset;
        selStartCopy = selStart;
        selEnd = sel.focusOffset;

        intendedValue = dc.textContent.slice(0,selStart) + message.content + dc.textContent.slice(selEnd);
        dc.textContent = intendedValue;
        elementToDispatchEventFrom = elementToDispatchEventFrom.parentElement;
    }
    // simulate user's input
    elementToDispatchEventFrom.dispatchEvent(new InputEvent('input', {bubbles: true}));
    // remove new element if it exists
    // otherwise there will be two of them after
    // Facebook adds it itself!
    if (newEl) newEl.remove();
}else ...

哪里

function getDeepestChild(element){
  if(element.lastChild){
    return getDeepestChild(element.lastChild)
  }else{
    return element;
  }
}

message.content 是我想粘贴到 Messenger 字段中的字符串。

此解决方案可以更改 Messenger 字段的内容,但会将光标移动到字段的开头 - 我不确定是否可以保持光标的位置不变(因为没有 selectionStart 和 selectionEnd 可以被更改)。

and 的答案大部分对我有用。 但是,我遇到过它不起作用的情况。

我的扩展程序显示了一个文本列表,当用户单击某个项目时,这些文本将被插入到输入中。直到我在修改内容和调度事件之前专注于该字段,它才正常工作。

const myNewText = 'whatever text I get a click on my list';
elementToDispatchEventFrom.focus();  // <- I had to add this line, focus the field before editing (`elementToDispatchEventFrom` is from raandremsil's answer)
deepestChild.contextText = myNewText; // Edit the text
elementToDispatchEventFrom.dispatchEvent(new InputEvent('input', {bubbles: true})); // Dispatch event to simulate user input

2022 年 2 月 27 日更新:

由于某种原因它停止工作了。这是我现在正在做的,以替换输入中的文本,假设它就在光标位置之前。

const selection = window.getSelection()!;
const cursorPosition = selection.focusOffset;

const focusNode = selection.focusNode!;
const doc = focusNode.ownerDocument!;

const range = new Range();
range.setStart(focusNode, cursorPosition - textToReplace.length);
range.setEnd(focusNode, cursorPosition);

selection.removeAllRanges();
selection.addRange(range);

doc.execCommand('insertText', false, myNewText); // Careful, for some reason, this does not work if `myNewText` ends with the space.