Monday 30 November 2020

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

I'm trying to write a Chrome Extension that needs to be able to insert a character at the cursor location in an input field.

It's very easy when the input is an actual HTMLInputElement (insertAtCaretInput borrowed from another stack answer):

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);
  ...
}

But the moment an input is actually represented differently in the HTML structure (e.g. Facebook having a <div> with <span> elements showing up and consolidating at weird times) I can't figure out how to do it reliably. The new character disappears or changes position or the cursor jumps to unpredictable places the moment I start interacting with the input.

Example HTML structure for Facebook's (Chrome desktop page, new post or message input fields) editable <div> containing string 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>

Here's my most successful attempt so far. I extend the span element like so (insertTextAtCursor also borrowed from another answer):

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;

And since the element triggering key press events is a <div> that then holds <span> elements which then hold the text nodes with the actual input, I find the deepest <span> element and perform insertAtCaret on that element:

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);
  ...
}

The code above can successfully insert the character but then strange things happen when Facebook's behind-the-scenes framework tries to process the new value. I tried all kinds of tricks with repositioning the cursors and inserting <span> elements similar to what seems to be happening when Facebook manipulates the dom on inserts but in the end, all of it fails one way or another. I imagine it's because the state of the input area is held somewhere and is not synchronized with my modifications.

Do you think it's possible to do this reliably and if so, how? Ideally, the answer wouldn't be specific to Facebook but would also work on other pages that use other elements instead of HTMLInputElement as input fields but I understand that it might not be possible.



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

No comments:

Post a Comment