Cool stuff first always! Try it before I walk you through why I put this demo together.
Below is a text editor that supports some text formatting - the standard bold and italic and a custom KEBAB! formatting. KEBAB! formatting underlines a span of text, and when hovered over, renders a tooltip and allows to accept or dismiss a suggestion that turns the text into Kebab!.
Also note KEBAB! formatting, accepting or rejecting it won't interfere with the undo/redo history of the text editor.
Now you have seen the demo, let's talk about what this demo shows and why these small seemingly small features are significant.
I worked at a startup that offers an AI detection tool. The main product is a text editor that allows you to write or paste in text and then shows the AI detection result on the right side - very straightforward.
One of the main initiative we had was to turn this "copy paste then scan" heavy product into a writing space - we want users to start or stay in the text editor to write and scan.
There were a lot of features we put together to push forward in this direction and one major theme is to provide writing feedback to users while they write. And in this push of providing writing feedback, the first type of feedback we want to provide is grammar related.
So now you can understand why we put together this demo of KEBAB! formatting that allows users to accept or dismiss - in our product, these are the grammar suggestions that users can pick and choose (think of grammarly).
Our text editor is basically a QuillJS editor. And it's not exactly easy to work with Quill to render all different types of advanced in text highlighting while maintaining a great writing experience.
Now, I am going to walk you through some technical challenges I overcame.
The first challenge I navigated through was building the tooltip that renders when the text was hovered.

Here I learned that QuillJS only cares about what's on the surface of an element in the editor and will never look at shadow DOM (plus it can't anyways). I figured out how to implement custom Quill highlighting to turn a span of text into custom element AKA web component, then the tooltip is rendered within the shadow DOM. (See above screenshot)
You might want to ask - is Web Component absolutely necessary? Maybe not, but one of the biggest obstacle was actually from how we are getting our grammar suggestion data. Our grammar suggestion data is generated based on a snapshot of editor text and it tells you the starting and ending index of a span of text and its suggestion. This means tracking how text is moved around is almost impossible. So I thought by formatting the text into a web component instead of a regular HTML span or other type of default element would allow me to encapsulate much more information and logic into the span and thus making the custom element span self-govern.
Because tooltip is rendered within the shadow DOM itself, the accept and dismiss button can just alter the element itself - accepting means changing the inner text to suggested text, dismissing means do nothing, then both action turns the custom element itself into a regular text node. Then Quill's optimization/consolidation logic will automatically merge the text node back into the span ahead and after.
This seems like a super elegent solution until we run into another issue. Keep reading!
The next challenge we encountered was related to the undo/redo history of the text editor.
As I just mentioned above, I thought self mutating on accepting or rejecting would be a clean solution but it turned out it actually is destructive to the Quill editor internal undo/redo history stack.
Quill editor maintains a internal state of all actions taken for performing undo/redo. But if anything directly modifies the DOM elements within the editor and triggers the optimization/consolidation process to run, the undo/redo stack is no longer accurate and it drifts away from what's in the editor. This can cause undo/redo shortcut having weird behavior.
Lesson learned here. So the fix contains below changes
One funny thing I found is that the silent source in QuillJS doesn't seem to work properly or I may have understood it wrong. But it doesn't really stay away from the undo/redo history. So I had this little maneuver to get around it so some actions do NOT touch undo/redo history. For example, in the demo above, formating, accepting or rejecting KEBAB! formatting are all undo/redo history orthogonal.
const silentlyExecute = (quill: Quill, fn: Function) => {
const history = quill.getModule('history');
// Save the old record function
const oldRecord = history.record;
try {
// Temporarily disable recording by stubbing in a no-op function
history.record = () => {};
fn();
} finally {
// Restore normal history behavior
history.record = oldRecord;
}
}
There are so many things to consider to build or even just use a editor in browser. Building a writing space in browser is difficult, especially when you trying to sprinkle some fancy highlighting on top of it.
With QuillJS specifcally, this combination of custom Blot in junction with web component does seems pretty slick but with that ONE CATCH - never self mutate, always go through QuillJS when you need to mutate text or really any mutation.