ianstormtaylor / slate

A completely customizable framework for building rich text editors. (Currently in beta.)
http://slatejs.org
MIT License
29.33k stars 3.22k forks source link

Changing the text direction breaks the editor #5628

Open azvoncov-smartling opened 3 months ago

azvoncov-smartling commented 3 months ago

Description The bug is reproduced in Chromium; when changing the direction of the text, the browser wraps the elements in an extra div, which breaks the editor

I didn't find the specification, but it looks like all inline blocks are up to the nearest contenteditable="false"

That is, if you use inline-void elements and change the direction of the text -> the editor breaks

Recording

https://github.com/ianstormtaylor/slate/assets/87305102/2cb11beb-86ab-474f-bc76-d214d58d3450

Sandbox https://www.slatejs.org/examples/mentions

Steps To reproduce the behavior:

  1. Go to https://www.slatejs.org/examples/mentions
  2. Click on the text after mentioned user (inline-void element)
  3. Сall context menu -> Writing Direction -> Right to Left
  4. See extra div and errors in the console

Expectation Ideal option: when changing the direction of the text, the dir attribute is set to the entire editor Ok option: intercept this event and restore the previous state of DOM

Environment

Context In lexical this problem is solved (try Insert -> Image -> Sample) https://playground.lexical.dev/

azvoncov-smartling commented 3 months ago

Also, if you apply a change of direction to the selected text close to the Void Element - Chrome removes the span nodes and after any changes (Ctrl+z for example) the editor crashes

https://github.com/ianstormtaylor/slate/assets/87305102/b7eb2827-49ac-45b2-a2ba-2d28abf46837

azvoncov-smartling commented 3 months ago

btw, I have a raw version of the hook that fixes this problem, you can also save a new direction locally and change it in the editor if desired


const config = { attributes: true, childList: true, subtree: true };

function isElementNode(node: Node | null): node is Element {
    return !!node && node.nodeType === Node.ELEMENT_NODE;
}

export function useChangingDirection(editor: SlateEditor) {
    useLayoutEffect(() => {
        const textbox = ReactEditor.toDOMNode(editor, editor);

        const callback: MutationCallback = (mutationsList) => {
            const changedDirection = mutationsList.some(mutation => {
                const { type, attributeName, target } = mutation;

                if (type === "attributes" && target.nodeName === "DIV") {
                    if (attributeName === "style"
                        && isElementNode(target)
                    ) {
                        const styleAttribute = target.getAttribute("style");

                        return styleAttribute && styleAttribute.includes("direction");
                    }

                    return attributeName === "dir";
                }

                return false;
            });

            if (changedDirection) {
                const childListMutations = mutationsList
                    .filter(mutation => mutation.type === "childList")
                    .reverse();

                childListMutations.forEach(currentMutation => {
                    currentMutation.addedNodes.forEach(node => {
                        node.parentNode?.removeChild(node);
                    });

                    currentMutation.removedNodes.forEach(node => {
                        currentMutation.target.insertBefore(node, currentMutation.nextSibling);
                    });
                });
            }
        };

        const observer = new MutationObserver(callback);
        observer.observe(textbox, config);

        return () => observer.disconnect();
    }, [editor]);
}