javascriptinputtextevent-handlingcustom-events

How to access just the changed data of an 'input' event regardless of text deletion or insertion like pasted or typed or restored text for the latter?


I have a textarea and would like to return the most recent change to the text for use in avia JavaScript function. That would include:

For example:

I'm still new to this and have no real idea how to call that added/removed text for use in a JavaScript function.


Solution

  • Reading about InputEvent, InputEvent.inputType and InputEvent.data might be a good starting point. In combination with selectionStart / selectionEnd as with e.g. HTMLInputElements and/or HTMLTextAreaElements one pretty much could cover/solve the OP's task.

    A possible approach which detects (text) changes within the input data/value might ...

    On top of the new custom 'input:datachange' event type, with all the additional information one does neither receive by 'input' nor by 'change' events, the OP now will be able to directly access the changed value whether deleted and/or inserted (pasted or typed or restored for the latter).

    // the node reference based storage of most recent element values.
    const mostRecentValueStorage = new WeakMap;
    
    function getDataChangeFromDeleteOrPaste({ currentTarget, data }) {
      const recentValue = mostRecentValueStorage.get(currentTarget);
      const { value: currentValue, selectionEnd } = currentTarget;
    
      let deletionStart = selectionEnd;
      let leadingValue = currentValue.slice(0, deletionStart);
    
      while ((leadingValue !== '') && !recentValue.startsWith(leadingValue)) {
        leadingValue = leadingValue.slice(0, --deletionStart);
      }
      const deletionLength =
        (selectionEnd - deletionStart) + (recentValue.length - currentValue.length);
    
      const deleted = recentValue.slice(deletionStart, (deletionStart + deletionLength));
    
      const insertLength = currentValue.length + deleted.length - recentValue.length;
      const insertStart = selectionEnd - insertLength;
    
      const inserted = (insertStart < selectionEnd)
        && currentValue.slice(insertStart, selectionEnd)
        || null;
    
      return {
        currentValue,
        recentValue,
        deleted: (deleted === '') ? null : deleted,
        deletionStart: (deleted === '') ? null : deletionStart,
        deletionLength: (deleted === '') ? null : deletionLength,
        inserted,
        insertStart: (inserted === null) ? null : insertStart,
        insertLength: (inserted === null) ? null : insertLength,
      };
    }
    function getDataChangeFromInsertText({ currentTarget, data }) {
      const recentValue = mostRecentValueStorage.get(currentTarget);
      const { value: currentValue, selectionEnd } = currentTarget;
    
      const insertLength = data.length;
    
      const deletionStart = (selectionEnd - insertLength);
      const deletionLength = (recentValue.length - currentValue.length + insertLength);
    
      const deleted = recentValue.slice(deletionStart, (deletionStart + deletionLength));
    
      return {
        currentValue,
        recentValue,
        deleted: (deleted === '') ? null : deleted,
        deletionStart: (deleted === '') ? null : deletionStart,
        deletionLength: (deleted === '') ? null : deletionLength,
        inserted: data,
        insertStart: (selectionEnd - insertLength),
        insertLength,
      };
    }
    
    function handleCustomInputDataChange(evt) {
      const { currentTarget } = evt;
    
      if (currentTarget.value !== mostRecentValueStorage.get(currentTarget)) {
        const { data } = evt;
    
        const dataChange = (
          (typeof data === 'string') && getDataChangeFromInsertText(evt)
        ) || (
          (data === null) && getDataChangeFromDeleteOrPaste(evt)
        ) || null;
    
        // put the most recent element value into an object based storage.
        mostRecentValueStorage.set(currentTarget, currentTarget.value);
    
        currentTarget
          .dispatchEvent(
            new CustomEvent('input:datachange', {
              bubbles: true,
              detail: {
                dataChange,
                inputEvent: evt,
              },
            })        
          );
      }
    }
    
    function enableCustomInputDataChangeHandling(elmNode) {
      // put the initial element value into an object based storage.
      mostRecentValueStorage.set(elmNode, elmNode.defaultValue);
    
      elmNode
        .addEventListener('input', handleCustomInputDataChange);
    }
    function initializeCustomInputDataChangeHandling() {
      document
        // for every element which features a
        // `data-handle-input-data-change` attribute ...
        .querySelectorAll('[data-handle-input-data-change]')
    
        // ... enable the handling of a custom
        // 'input:datachange' event.
        .forEach(enableCustomInputDataChangeHandling);
    }
    
    function main() {
      initializeCustomInputDataChangeHandling();
    
      document
        // subscribe to the custom 'input:datachange' event wherever it is needed. 
        .addEventListener(
          'input:datachange',
          ({ target, detail: { dataChange } }) => console.log({ /*target, */dataChange })
        );
    }
    main();
    body { margin: 0; }
    .as-console-wrapper { left: auto!important; width: 76%; min-height: 100%!important; }
    <textarea
      data-handle-input-data-change
      cols="16"
      rows="12"
    >The quick brown fox jumps over the lazy dog ... edit text in whichever way.</textarea>