ueberdosis / tiptap

The headless rich text editor framework for web artisans.
https://tiptap.dev
MIT License
27.12k stars 2.26k forks source link

[PRO]: When the scrollParent is different than window, onUpdate callback of TableOfContents extension is not called on scroll #5349

Closed gizemagden closed 2 months ago

gizemagden commented 3 months ago

Affected Packages

@tiptap-pro/extension-table-of-contents

Version(s)

2.9.2

Description of the Bug

I am using TableOfContents extension. I set a specific scrollParent element other than window. onUpdate callback of the extension is not called when I scroll the scrollable parent div. So, I cannot access updated isScrolledOver and isActive props of the anchors.

I see when the whole page is scrollable, with the default setting (window), onUpdate is called.

Browser Used

Chrome

Code Example (Preferred)

No response

Expected Behavior

onUpdate callback of the TableOfContents extension should be called properly when a specific scrollParent element is set.

Additional Context (Optional)

Code:

body {
  margin: 0;

  padding: 0;

  overflow: hidden;

  height: 100%;
}

#root, html {
  height: 100%;
}

.scrollableParent {
  @include customScrollbar;
  display: flex;
  flex-direction: column;
  width: 100%;
  overflow-x: hidden;
  overflow-y: auto;
}

.parentDivOfScrollableParent {
  display: flex;

  height: calc(100% - 293px);
  flex:  1;
}
export const TiptapExtensionsKit = [
  StarterKit.configure({
    heading: {
      levels: [1, 2, 3],
    },
    horizontalRule: false,
    codeBlock: false,
    strike: false,
    code: false,
  }),
  Placeholder.configure({
    placeholder: "Type / to browse options",
  }),
  Highlight,
  Subscript,
  Superscript,
  Image,
  Table.configure({
    resizable: true,
  }),
  TableCell,
  TableHeader,
  TableRow,
  TableOfContents,
];
const editor = useEditor({
    extensions: ExtensionsKit,
    content: "",
    autofocus: true,
    injectCSS: false,
    editable: isEditOn,
  });
const debouncedOnContentChangeAsync = useMemo(
    () => debounce(onContentChangeAsync, GeneralConstants.DEFAULT_MS_DELAY),
    [onContentChangeAsync]
  );

  const onTransactionHandlerAsync = useCallback(
    async ({
      editor: updatedEditor,
      transaction,
    }: {
      editor: TiptapEditor;
      transaction: Transaction;
    }) => {
      if (transaction.steps.length > 0) {
        await debouncedOnContentChangeAsync(
          JSON.stringify(updatedEditor.getJSON())
        );
      }
    },
    [debouncedOnContentChangeAsync]
  );

  const onUpdateHandler = useCallback(
    async ({
      editor: updatedEditor
    }: {
      editor: TiptapEditor;
    }) => {
      setTableOfContents(updatedEditor.extensionStorage.tableOfContents);
    },
    [setTableOfContents]
  );

  useEffect(() => {
    if (!editor) return;
    editor.on("update", onUpdateHandler);
    editor.on("transaction", onTransactionHandlerAsync);

    return () => {
      editor.off("update", onUpdateHandler);
      editor.off("transaction", onTransactionHandlerAsync);
    };
  }, [editor, onTransactionHandlerAsync, onUpdateHandler]);
useEffect(() => {
    if (scrollingDivElement) {
      initializeContent(content, scrollingDivElement);
    }
}, [scrollingDivElement, initializeContent]);
  const initializeContent = useCallback((newContent: string, scrollParent: HTMLDivElement) => {
    if (!editor) return;

    const tableOfContentsExtension = editor.extensionManager.extensions.find(currentExt => currentExt.name === TableOfContents.name);

    if (tableOfContentsExtension) {
      (tableOfContentsExtension as Extension<TableOfContentsOptions, TableOfContentsStorage>).options.scrollParent = scrollParent;
      tableOfContentsExtension.options.onUpdate = (data: any, isCreate: boolean) => {
        console.log("Updated data", data, isCreate); // this is not called on scroll of scroll parent.
      };
    }

    setContent(newContent);
  }, [editor]);

Dependency Updates

bdbch commented 3 months ago

I have a PR open for this. Passing through the scrollParent will be required to use callback functions in the future:

TableOfContents.configure({
  // ...
  scrollParent: () => scrollRef.current
})
gizemagden commented 3 months ago

Thank you, but unfortunately it still does not work correctly for me.

I noticed I needed to pass my scrollable div to the dependency array of the useEditor hook since it is set later. By doing that, I am able to see the onUpdate callback of the extension is called on scroll. But when I look at the isScrolledOver and isActive values of the headers, they seem to be set based on the viewport not on my scrollable parent element. How can I make sure that they are not calculated based on the viewport?

  const editor = useEditor({
    extensions: [...ExtensionsKit, TableOfContents.configure({
      scrollParent: () => { console.log("Editor scrolling parent", editorScrollParent); return editorScrollParent as HTMLElement;},
      onUpdate: (data: TableOfContentData, isCreate?: boolean) => {
        console.log("Updated data", data, isCreate); //  isScrolledOver and isActive values in here are not correct.
      },
    })],
    content: "",
    autofocus: true,
    injectCSS: false,
    editable: isEditOn,
  }, [editorScrollParent]);
borstessi commented 6 days ago

how did you fix it @gizemagden ?