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

Mark Toolbar: Cannot resolve a DOM node from Slate node #2674

Closed
zbeyens opened this issue Oct 4, 2023 Discussed in #2651 · 8 comments
Closed

Mark Toolbar: Cannot resolve a DOM node from Slate node #2674

zbeyens opened this issue Oct 4, 2023 Discussed in #2651 · 8 comments
Labels

Comments

@zbeyens
Copy link
Member

zbeyens commented Oct 4, 2023

Discussed in #2651

Originally posted by kieranm September 27, 2023
I've followed the docs on implementing a basic fixed toolbar, but I am running into a strange issue that's difficult to debug. For this demo I've removed all buttons other than Bold.

The issue is the following:

  • Selecting a whole block/row of text works fine with both Cmd+B and the button
  • Selecting part of a block works fine with Cmd+B
  • Selecting part of a block and using the button results in an exception
Uncaught Error: Cannot resolve a DOM node from Slate node: {"text":"k brown ","bold":true}

A video to illustrate the behaviour I'm seeing:
https://github.com/udecode/plate/assets/232283/c5833fd5-2ab7-495f-9002-4e763e468d77

I would be grateful for any pointers. I've copied the main parts of my code below...

Editor.tsx

import React from "react"
import {
  Plate,
  PlateContent,
  PlateContentProps,
  PlateEditor,
  PlateProps,
  PlateRenderElementProps,
  RenderAfterEditable,
  TElement,
  Value,
  createPlateEditor,
  createPlugins,
  deserializeHtml,
} from "@udecode/plate-common"
import { createComboboxPlugin } from "@udecode/plate-combobox"
import { createEmojiPlugin } from "@udecode/plate-emoji"
import { serializeHtml } from "@udecode/plate-serializer-html"
import {
  MARK_BOLD,
  createBoldPlugin,
  createItalicPlugin,
  createStrikethroughPlugin,
  createUnderlinePlugin,
} from "@udecode/plate-basic-marks"
import { createParagraphPlugin } from "@udecode/plate-paragraph"
import { EmojiCombobox } from "./EmojiCombobox"
import { EditorToolbar } from "./toolbar/EditorToolbar"
import { EditorMarkToolbarButton } from "./toolbar/EditorMarkToolbarButton"

export type EditorValue = Value

type EditorProps = Omit<PlateContentProps, "onChange" | "value"> &
  React.RefAttributes<HTMLDivElement> &
  Pick<PlateProps, "value" | "onChange">

const plugins = createPlugins([
  createComboboxPlugin(),
  createEmojiPlugin({
    renderAfterEditable: EmojiCombobox as RenderAfterEditable,
  }),

  // Marks
  createBoldPlugin({
    component: ({
      attributes,
      nodeProps,
      children,
    }: PlateRenderElementProps) => (
      <b {...attributes} {...nodeProps}>
        {children}
      </b>
    ),
  }),

  // Basic Elements
  createParagraphPlugin(),
])

const EditorFn = React.forwardRef(
  (props: EditorProps, ref: React.ForwardedRef<HTMLDivElement>) => {
    const { className, value, onChange, ...rest } = props

    return (
      <Plate value={value} plugins={plugins} onChange={onChange}>
        <EditorToolbar>
          <EditorMarkToolbarButton nodeType={MARK_BOLD} icon="Bold" />
        </EditorToolbar>
        <PlateContent
          className={className}
          ref={ref}
          placeholder="Type..."
          {...rest}
        />
      </Plate>
    )
  },
)
EditorFn.displayName = "Editor"

export const Editor = React.memo(
  EditorFn,
) as React.ForwardRefExoticComponent<EditorProps>

EditorToolbar.tsx

"use client"

import React from "react"
import { Root, ToolbarProps } from "@radix-ui/react-toolbar"
import { cva, VariantProps } from "class-variance-authority"
import { cn } from "@daybridge/cn"
import { useEditorReadOnly } from "@udecode/plate-common"

type EditorToolbarProps = React.HTMLAttributes<HTMLDivElement> &
  ToolbarProps &
  VariantProps<typeof editorToolbar> & {
    // Additional props here...
  }

const editorToolbar = cva("", {
  variants: {
    theme: {
      primary: [],
    },
  },
  defaultVariants: {
    theme: "primary",
  },
})

const EditorToolbarFn = React.forwardRef(
  (props: EditorToolbarProps, ref: React.ForwardedRef<HTMLDivElement>) => {
    const { children, className, style, theme, ...rest } = props
    const readOnly = useEditorReadOnly()

    if (readOnly) return null

    return (
      <Root
        ref={ref}
        contentEditable={false}
        style={{ userSelect: "none", ...style }}
        className={cn(editorToolbar({ theme, className }))}
        {...rest}
      >
        {children}
      </Root>
    )
  },
)
EditorToolbarFn.displayName = "EditorToolbar"

export const EditorToolbar = React.memo(
  EditorToolbarFn,
) as typeof EditorToolbarFn

EditorToolbarToggleItem.tsx

import { ToggleItem, ToolbarToggleItemProps } from "@radix-ui/react-toolbar"
import { VariantProps, cva } from "class-variance-authority"
import React from "react"

import { cn } from "@daybridge/cn"

type EditorToolbarToggleItemProps = React.HTMLAttributes<HTMLButtonElement> &
  ToolbarToggleItemProps &
  VariantProps<typeof editorToolbarToggleItem> & {
    // Additional props here...
  }

const editorToolbarToggleItem = cva("", {
  variants: {
    theme: {
      primary: [],
    },
    enabled: {
      true: [],
      false: [],
    },
  },
  defaultVariants: {
    theme: "primary",
  },
})

const EditorToolbarToggleItemFn = React.forwardRef(
  (
    props: EditorToolbarToggleItemProps,
    ref: React.ForwardedRef<HTMLButtonElement>,
  ) => {
    const { children, className, theme, enabled, ...rest } = props

    return (
      <ToggleItem
        ref={ref}
        className={cn(editorToolbarToggleItem({ theme, enabled }), className)}
        {...rest}
      >
        {children}
      </ToggleItem>
    )
  },
)
EditorToolbarToggleItemFn.displayName = "EditorToolbarToggleItem"

export const EditorToolbarToggleItem = React.memo(
  EditorToolbarToggleItemFn,
) as typeof EditorToolbarToggleItemFn

EditorMarkToolbarButton.tsx

"use client"

import React, { ComponentProps } from "react"
import { cn } from "@daybridge/cn"
import {
  useMarkToolbarButton,
  useMarkToolbarButtonState,
} from "@udecode/plate-utils"
import { Button } from "../../button/Button"
import { EditorToolbarToggleGroup } from "./EditorToolbarToggleGroup"
import { EditorToolbarToggleItem } from "./EditorToolbarToggleItem"

type EditorMarkToolbarButtonProps = Omit<
  ComponentProps<typeof EditorToolbarToggleItem>,
  "value"
> &
  Omit<React.HTMLAttributes<HTMLButtonElement>, "children"> & {
    clear?: string
    nodeType: string
    icon: string
  }

const EditorMarkToolbarButtonFn = React.forwardRef(
  (
    props: EditorMarkToolbarButtonProps,
    ref: React.ForwardedRef<HTMLDivElement>,
  ) => {
    const { className, clear, nodeType, icon, ...rest } = props
    const state = useMarkToolbarButtonState({ clear, nodeType })
    const { props: buttonProps } = useMarkToolbarButton(state)

    return (
      <EditorToolbarToggleGroup type="single" ref={ref} asChild>
        <EditorToolbarToggleItem
          value="single"
          className={cn(className)}
          enabled={buttonProps.pressed}
          onClick={buttonProps.onClick}
          {...rest}
          asChild
        >
          <Button icon={icon} className="w-5" />
        </EditorToolbarToggleItem>
      </EditorToolbarToggleGroup>
    )
  },
)
EditorMarkToolbarButtonFn.displayName = "EditorMarkToolbarButton"

export const EditorMarkToolbarButton = React.memo(
  EditorMarkToolbarButtonFn,
) as typeof EditorMarkToolbarButtonFn

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar
@zbeyens
Copy link
Member Author

zbeyens commented Oct 4, 2023

@zbeyens
Copy link
Member Author

zbeyens commented Oct 4, 2023

Waiting for feedback ianstormtaylor/slate#5516

In the meantime, use slate-react < 0.99.0

@zbeyens
Copy link
Member Author

zbeyens commented Oct 4, 2023

Fixed in #2676

@zbeyens zbeyens closed this as completed Oct 4, 2023
@zbeyens zbeyens added the slate label Oct 4, 2023
@mstelz
Copy link

mstelz commented Oct 6, 2023

is this still functionally working with insert?

@zbeyens
Copy link
Member Author

zbeyens commented Oct 6, 2023

If not, use it in setTimeout

@12joan
Copy link
Collaborator

12joan commented Oct 6, 2023

I can reproduce it on the playground.

recording.mp4

Getting the Slate change reverted or fixed might be the best course of action here; even if we prevent the mousedown events on the dropdown trigger and dropdown item buttons, there will still be cases where manually focusing the editor is necessary for making keyboard accessible buttons and menus. (CC @josephmr, @dylans)

We could just surpress the error if the DOM node can't be resolved. Adding a try/catch block to Slate would be the safest way of doing this to avoid catching more than we intend.

@AndreyMarchuk
Copy link
Contributor

AndreyMarchuk commented Oct 17, 2023

Also can reproduce this issue with a link on floating toolbar with latest plate/slate:

  • select text
  • click "link" on a toolbar
  • enter some url and click enter -> exception

@AndreyMarchuk
Copy link
Contributor

Another setTimeout() fix?
#2705

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

No branches or pull requests

4 participants