Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the best way to modify text on a text-change event in Quill? #2558

Open
alecgibson opened this issue Apr 2, 2019 · 9 comments
Open

Comments

@alecgibson
Copy link
Contributor

We're trying to build track changes into Quill, which includes checking for user input, and then modifying the document. For example, if a user deletes some text, we reverse the deletion, and then apply a track-deletion attribute. Here is a simplified example of what we do:

quill.on('text-change', (delta, oldContents, source) => {
  if (source !== 'user') return;

  const trackChangesDelta = delta.invert(oldContents);

  trackChangesDelta.ops.forEach((op) => {
    if (op.insert) op.attributes['track-deletion'] = true;
    // Also handle tracked insertions, etc...
  });

  quill.updateContents(trackChangesDelta);
});

This approach mostly works, but runs into some slightly odd cases:

  • If I have the text foo, and I highlight the text, and then type the letter o, I'd expect the entire word to be deleted, followed by an o insertion. Instead, Parchment appears to "optimise" the op, and instead do a pair of deletions either side of the middle o

  • If I highlight and replace text at the start of a paragraph, then the inserted letter goes to the end of the deletion, but my cursor stays in the wrong place

I think both of these issues are symptoms of the fact that we're performing a text-change within a text-change, and I was wondering if there was a better (more synchronous?) way of hooking into the Quill document life-cycle.

For example, the second issue we have is resolved by using a setTimeout to let the Quill document finish processing the text-change before we apply our track changes, but that then results in some other jittery behaviour.

Ideally, I'd like to catch the text-change before it's even been applied to the document, but as far as I can tell, neither Quill nor Parchment offers a suitable hook?

@alecgibson
Copy link
Contributor Author

One other place this is juddery is if you hold down the backspace key in Safari. It would be really nice if we could update the contents before the editor is redrawn.
Apr-10-2019 10-28-52

@DmitrySkripkin
Copy link

You actually can't do that. Quill uses contenteditable and DOM mutation listeners (as far as I know). So DOM updates first and mutation events trigger Delta model updates.
In some cases you can use keyboard bindings to prevent DOM changes and change Delta first.

I think we need pretty complex input interceptor.

@Jaimies
Copy link

Jaimies commented Apr 30, 2020

@alecgibson +1, it would be really great to have a feature like that

@phillipb
Copy link

phillipb commented Aug 8, 2020

This is also problematic when dealing with deltas. If you make a call to updateContents from text-change event, that delta appears before the actual user's change. So delta.compse for those changes results in an incorrect delta.

@gordoncl
Copy link

gordoncl commented Mar 14, 2022

This is also problematic when dealing with deltas. If you make a call to updateContents from text-change event, that delta appears before the actual user's change. So delta.compse for those changes results in an incorrect delta.

I made a sandbox which illustrates that subsequent handlers will receive the changes out of order. You can view the console to see the assertion fails.

In my case, I'm trying to build a convert to emoji tool which works in collaborative editing and want to be able to handle the cases where arbitrary changes are made (ex if a deletion corrects a misspelling in an emoji token the transformation is made). I've noticed other modules use this approach but they likely don't need to support collaborative editing, where it's imperative the deltas are delivered in the correct order.

The documentation also doesn't really say whether performing edits inside of text-change is bad practice or not. But having support for edits in response to edits would be nice. Or, at least have documentation on what is best practice.

@littlecharva
Copy link

+1 for wanting to modify text on a text-change event. I'd like to be able to add a width and class attribute to an img after it's been added. In my case I can't even use the invert method on the Delta objects in the text-change event, as the method is missing. It seems to have every method EXCEPT invert. I'm using Quill from the CDN (https://cdn.quilljs.com/1.3.7/quill.js).

@salmin89
Copy link

salmin89 commented Sep 21, 2023

@luin maybe a solution to this is to introduce a "before-text-change" -event, that allows us to modify the delta before rendering?

@luin
Copy link
Member

luin commented Sep 22, 2023

Before Quill officially supports this, a workaround could be perform the changes with queueMicrotask:

quill.on(Quill.events.TEXT_CHANGE, () => {
  queueMicrotask(() => {
    quill.updateContents(/* your changes */);
  });
});

@salmin89
Copy link

salmin89 commented Sep 24, 2023

@luin could explaining the difference between running quill.updateContents immediately vs running it in a queueMicrotask?

microtask works very similar and it doesn't change the need to "reverse" the delta operations if you need to filter out some content.

My current solution looks like this (I need to filter out content dynamically based on a regex-option):

  onTextChange(delta: DeltaStatic, old: DeltaStatic, source: Sources) {
    if (delta.ops === undefined) return;
    if (source === 'silent') return;

    if (delta.ops && delta.ops.some((op) => op.insert && typeof op.insert === 'string' && op.insert.match(this.regExp))) {
      const reversed: any[] = [];

      delta.ops.forEach((op) => {
        if (op.retain) {
          reversed.push({
            retain: op.retain,
          });
        }

        if (op.insert) {
          reversed.push({
            delete: op.insert.length,
          });

          reversed.push({
            insert: op.insert.replace(this.regExp, ''),
          });
        }
      });

      queueMicrotask(() => {
        this.quill.updateContents(reversed as any as DeltaStatic, 'silent');
      });
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants