ianstormtaylor / slate

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

Formatting type 'string' can't be used to index type 'Node' #5075

Open neilfranci opened 2 years ago

neilfranci commented 2 years ago

Description I'm following the Hovering Toolbar example and when comes to this checking formatting function. The hovering toolbar is still able to get the right matches to show the correct color of the active format.

Maybe I'm wrong somewhere or does this actually happen to all people?

const isFormatActive = (editor: BaseEditor & ReactEditor, format: string) => {
   const [match] = Editor.nodes(editor, {
      match: (n) => {
         return n[format] === true
      },
      mode: 'all'
   })

   return !!match
}

I got the following error saying

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Node'.
  No index signature with a parameter of type 'string' was found on type 'Node'.ts(7053)

Recording image

Environment

Context


type CustomElement = { type: 'paragraph'; children: CustomText[] }
type CustomText = { text: string; bold?: boolean; italic?: boolean }

declare module 'slate' {
   interface CustomTypes {
      Editor: BaseEditor & ReactEditor
      Element: CustomElement
      Text: CustomText
   }
}

function ChatEditor({ channel }: { channel: string }) {
   const [editor] = useState(() => withReact(createEditor()))

   const initialValue: Descendant[] = [
      {
         type: 'paragraph',
         children: [
            {
               text: 'This example shows how you can make a hovering menu appear above your content, which you can use to make text '
            },
            { text: 'bold', bold: true },
            { text: ', ' },
            { text: 'italic', italic: true },
            { text: ', or anything else you might want to do!' }
         ]
      },
      {
         type: 'paragraph',
         children: [
            { text: 'Try it out yourself! Just ' },
            {
               text: 'select any piece of text and the menu will appear',
               bold: true
            },
            { text: '.' }
         ]
      }
   ]

   return (
      <div
         className={cx(`flex flex-shrink-0 w-full my-2 py-1 bg-[#40444b]`, {
            [`h-[${height}]`]: true
         })}>
         <BsPlusCircleFill
            color="#b9bbbe"
            className="mx-1 min-h-[24px] min-w-[24px] cursor-pointer"
         />
         <div
            className={cx(
               `bg-[#40444b] flex items-center w-[90%] min-h-[24px] max-h-[50vh] rounded-lg
                overflow-auto`,
               [styles.scrollbar]
            )}>
            <div className="absolute select-none text-[#53555d]">
               Message ${channel}
            </div>
            <div className="w-full">
               <Slate editor={editor} value={initialValue}>
                  <HoveringToolbar />
                  <Editable
                     spellCheck={false}
                     className="break-word caret-[#b9bbbe] text-[#dcddde]"
                     onKeyDown={(e) => changeHeight(e)}
                  />
               </Slate>
            </div>
         </div>
         <BsEmojiSmileFill
            color="#b9bbbe"
            className="mx-1 min-h-[24px] min-w-[24px] cursor-pointer"
         />
      </div>
   )
}
const HoveringToolbar = () => {
   const ref = useRef<HTMLDivElement | null>(null)
   const editor = useSlate()
   const inFocus = useFocused()

   useEffect(() => {
      const el = ref.current
      const { selection } = editor

      if (!el) {
         return
      }

      if (
         !selection ||
         !inFocus ||
         Range.isCollapsed(selection) ||
         Editor.string(editor, selection) === ''
      ) {
         el.removeAttribute('style')
         return
      }
      const domSelection = window.getSelection()
      if (domSelection !== null && domSelection.rangeCount > 0) {
         const domRange = domSelection.getRangeAt(0)
         const rect = domRange.getBoundingClientRect()
         el.style.opacity = '1'
         el.style.top = `${rect.top + window.scrollY - el.offsetHeight - 5}px`
         el.style.left = `${
            rect.left + window.scrollX - el.offsetWidth / 2 + rect.width / 2
         }px`
      }
   })

   return (
      <Portal>
         <Menu
            ref={ref}
            className={cx(`
            pr-1 pt-[0.5rem] pb-[0.2rem] absolute z-[1] -top-[10000px] -left-[10000px]
            -mr-[6px] opacity-0 bg-[#202020] rounded-[4px]
            transition-opacity duration-75 
         `)}>
            <FormatButton format="bold" icon="format_bold" />
            <FormatButton format="italic" icon="format_italic" />
            {/* <FormatButton format="underlined" icon="format_underlined" /> */}
         </Menu>
      </Portal>
   )
}
export const Portal = ({ children }: { children: React.ReactNode }) => {
   return typeof document === 'object'
      ? ReactDOM.createPortal(children, document.body)
      : null
}
const FormatButton = ({ format, icon }: { format: string; icon: string }) => {
   const editor = useSlate()

   const format_icon = () => {
      switch (icon) {
         case 'format_bold':
            return <AiOutlineBold />
         case 'format_italic':
            return <AiOutlineItalic />
         default:
            break
      }
   }

   return (
      <Button
         active={isFormatActive(editor, format)}
         className={cx('cursor-pointer inline-block ml-1')}>
         {format_icon()}
      </Button>
   )
}
// eslint-disable-next-line react/display-name
const Menu = React.forwardRef(
   (
      { className, ...props }: PropsWithChildren<{ className: string }>,
      ref: Ref<HTMLDivElement>
   ) => <div {...props} ref={ref} className={className} />
)

const isFormatActive = (editor: BaseEditor & ReactEditor, format: string) => {
   const [match] = Editor.nodes(editor, {
      match: (n) => {
         return n[format] === true
      },
      mode: 'all'
   })

   return !!match
}

// eslint-disable-next-line react/display-name
const Button = React.forwardRef(
   (
      {
         className,
         active,
         ...props
      }: PropsWithChildren<{
         className: string
         active?: boolean
      }>,
      ref: Ref<HTMLSpanElement>
   ) => (
      <span
         className={cx(
            className,
            { 'text-[#fff]': active },
            { 'text-[#c04b4b]': !active }
         )}
         {...props}
         ref={ref}
      />
   )
)
PangYiMing commented 2 years ago

please try add any type.

    const curNodeEntry = Editor.nodes(editor, {
      at: [],
      match: (n: any) => n?.xxx=== 'xxxxxx',
    }).next().value;