|
| 1 | +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ |
| 2 | +/** |
| 3 | + * @typedef {import('mdast-util-mdx')} |
| 4 | + */ |
| 5 | +import React from 'react' |
| 6 | +import { |
| 7 | + $getRoot, |
| 8 | + $getSelection, |
| 9 | + $isRangeSelection, |
| 10 | + COMMAND_PRIORITY_LOW, |
| 11 | + ElementNode, |
| 12 | + LexicalEditor, |
| 13 | + LexicalNode, |
| 14 | + RangeSelection, |
| 15 | + SELECTION_CHANGE_COMMAND, |
| 16 | + TextNode, |
| 17 | +} from 'lexical' |
| 18 | +import { mergeRegister } from '@lexical/utils' |
| 19 | + |
| 20 | +import { LexicalComposer } from '@lexical/react/LexicalComposer' |
| 21 | +import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin' |
| 22 | +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' |
| 23 | +import { ContentEditable } from '@lexical/react/LexicalContentEditable' |
| 24 | +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' |
| 25 | +import { ListPlugin } from '@lexical/react/LexicalListPlugin' |
| 26 | +import { $isAtNodeEnd } from '@lexical/selection' |
| 27 | +import { importMarkdownToLexical, UsedLexicalNodes } from '@virtuoso.dev/lexical-mdx-import-export' |
| 28 | +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' |
| 29 | +import { $isLinkNode } from '@lexical/link' |
| 30 | + |
| 31 | +const initialMarkdown = ` |
| 32 | +[A link](https://google.com/ "Link To Google") |
| 33 | +
|
| 34 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 35 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 36 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 37 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 38 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 39 | +In commodo tempor lorem, id lobortis purus pharetra nec. Morbi sagittis ultricies lectus ut placerat. Praesent vestibulum ligula non sapien efficitur, sit amet viverra est imperdiet. Nam rutrum massa quam, sit amet convallis erat viverra nec. Donec pharetra urna urna, non malesuada orci consequat cursus. Nulla blandit ligula ac leo dictum fermentum. Fusce augue purus, dignissim ut posuere gravida, laoreet eget quam. Nunc porttitor leo sem, eget posuere magna congue ut. Nunc tempus mi quis efficitur accumsan. Ut efficitur, felis eu lacinia finibus, nisl quam ultrices eros, eget scelerisque dolor diam nec turpis. Nam ultricies, sapien auctor condimentum bibendum, metus est maximus neque, id ornare metus eros quis libero. Nulla facilisi. |
| 40 | +
|
| 41 | +[A link](https://google.com/ "Link To Google") |
| 42 | +` |
| 43 | + |
| 44 | +const theme = { |
| 45 | + text: { |
| 46 | + bold: 'PlaygroundEditorTheme__textBold', |
| 47 | + code: 'PlaygroundEditorTheme__textCode', |
| 48 | + italic: 'PlaygroundEditorTheme__textItalic', |
| 49 | + strikethrough: 'PlaygroundEditorTheme__textStrikethrough', |
| 50 | + subscript: 'PlaygroundEditorTheme__textSubscript', |
| 51 | + superscript: 'PlaygroundEditorTheme__textSuperscript', |
| 52 | + underline: 'PlaygroundEditorTheme__textUnderline', |
| 53 | + underlineStrikethrough: 'PlaygroundEditorTheme__textUnderlineStrikethrough', |
| 54 | + }, |
| 55 | + |
| 56 | + list: { |
| 57 | + nested: { |
| 58 | + listitem: 'PlaygroundEditorTheme__nestedListItem', |
| 59 | + }, |
| 60 | + }, |
| 61 | +} |
| 62 | + |
| 63 | +function onError(error: Error) { |
| 64 | + console.error(error) |
| 65 | +} |
| 66 | + |
| 67 | +function getSelectedNode(selection: RangeSelection): TextNode | ElementNode { |
| 68 | + const anchor = selection.anchor |
| 69 | + const focus = selection.focus |
| 70 | + const anchorNode = selection.anchor.getNode() |
| 71 | + const focusNode = selection.focus.getNode() |
| 72 | + if (anchorNode === focusNode) { |
| 73 | + return anchorNode |
| 74 | + } |
| 75 | + const isBackward = selection.isBackward() |
| 76 | + if (isBackward) { |
| 77 | + return $isAtNodeEnd(focus) ? anchorNode : focusNode |
| 78 | + } else { |
| 79 | + return $isAtNodeEnd(anchor) ? anchorNode : focusNode |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +function getSelectionRectangle(editor: LexicalEditor) { |
| 84 | + const selection = $getSelection() |
| 85 | + const nativeSelection = window.getSelection() |
| 86 | + const activeElement = document.activeElement |
| 87 | + |
| 88 | + const rootElement = editor.getRootElement() |
| 89 | + |
| 90 | + if ( |
| 91 | + selection !== null && |
| 92 | + nativeSelection !== null && |
| 93 | + rootElement !== null && |
| 94 | + rootElement.contains(nativeSelection.anchorNode) && |
| 95 | + editor.isEditable() |
| 96 | + ) { |
| 97 | + const domRange = nativeSelection.getRangeAt(0) |
| 98 | + let rect |
| 99 | + if (nativeSelection.anchorNode === rootElement) { |
| 100 | + let inner = rootElement |
| 101 | + while (inner.firstElementChild != null) { |
| 102 | + inner = inner.firstElementChild as HTMLElement |
| 103 | + } |
| 104 | + rect = inner.getBoundingClientRect() |
| 105 | + } else { |
| 106 | + rect = domRange.getBoundingClientRect() |
| 107 | + } |
| 108 | + |
| 109 | + return rect |
| 110 | + } else if (!activeElement || activeElement.className !== 'link-input') { |
| 111 | + return null |
| 112 | + } |
| 113 | + return null |
| 114 | +} |
| 115 | + |
| 116 | +function useLexicalSelection() { |
| 117 | + const [selectionRectangle, setSelectionRectangle] = React.useState<DOMRect | null>(null) |
| 118 | + const [selectedNode, setSelectedNode] = React.useState<LexicalNode | null>(null) |
| 119 | + const [editor] = useLexicalComposerContext() |
| 120 | + |
| 121 | + const reportSelection = React.useCallback(() => { |
| 122 | + const selection = $getSelection() |
| 123 | + if ($isRangeSelection(selection)) { |
| 124 | + setSelectionRectangle(getSelectionRectangle(editor)) |
| 125 | + setSelectedNode(getSelectedNode(selection)) |
| 126 | + } |
| 127 | + }, [editor]) |
| 128 | + |
| 129 | + React.useEffect(() => { |
| 130 | + const update = () => { |
| 131 | + editor.getEditorState().read(() => { |
| 132 | + reportSelection() |
| 133 | + }) |
| 134 | + } |
| 135 | + |
| 136 | + update() |
| 137 | + window.addEventListener('resize', update) |
| 138 | + // TODO: get the right scroller |
| 139 | + window.addEventListener('scroll', update) |
| 140 | + |
| 141 | + return () => { |
| 142 | + window.removeEventListener('resize', update) |
| 143 | + window.removeEventListener('scroll', update) |
| 144 | + } |
| 145 | + }, [editor, reportSelection]) |
| 146 | + |
| 147 | + React.useEffect(() => { |
| 148 | + return mergeRegister( |
| 149 | + editor.registerUpdateListener(({ editorState }) => { |
| 150 | + editorState.read(() => { |
| 151 | + reportSelection() |
| 152 | + }) |
| 153 | + }), |
| 154 | + |
| 155 | + editor.registerCommand( |
| 156 | + SELECTION_CHANGE_COMMAND, |
| 157 | + () => { |
| 158 | + reportSelection() |
| 159 | + return true |
| 160 | + }, |
| 161 | + COMMAND_PRIORITY_LOW |
| 162 | + ) |
| 163 | + ) |
| 164 | + }, [editor, reportSelection]) |
| 165 | + |
| 166 | + return { selectionRectangle, selectedNode } |
| 167 | +} |
| 168 | + |
| 169 | +function SelectionRectanglePlugin() { |
| 170 | + const { selectionRectangle } = useLexicalSelection() |
| 171 | + if (selectionRectangle?.width === 0) { |
| 172 | + return |
| 173 | + } |
| 174 | + return ( |
| 175 | + <div |
| 176 | + style={{ |
| 177 | + position: 'absolute', |
| 178 | + top: selectionRectangle?.top, |
| 179 | + left: selectionRectangle?.left, |
| 180 | + width: selectionRectangle?.width, |
| 181 | + height: selectionRectangle?.height, |
| 182 | + opacity: 0.5, |
| 183 | + backgroundColor: 'red', |
| 184 | + transform: 'translatey(-30px)', |
| 185 | + }} |
| 186 | + > |
| 187 | + Selection |
| 188 | + </div> |
| 189 | + ) |
| 190 | +} |
| 191 | + |
| 192 | +export function BasicSetup() { |
| 193 | + const initialConfig = { |
| 194 | + editorState: () => { |
| 195 | + importMarkdownToLexical($getRoot(), initialMarkdown) |
| 196 | + }, |
| 197 | + namespace: 'MyEditor', |
| 198 | + theme, |
| 199 | + nodes: UsedLexicalNodes, |
| 200 | + onError, |
| 201 | + } |
| 202 | + |
| 203 | + return ( |
| 204 | + <LexicalComposer initialConfig={initialConfig}> |
| 205 | + <RichTextPlugin contentEditable={<ContentEditable />} placeholder={<div></div>} ErrorBoundary={LexicalErrorBoundary} /> |
| 206 | + <LexicalLinkPlugin /> |
| 207 | + <ListPlugin /> |
| 208 | + <SelectionRectanglePlugin /> |
| 209 | + </LexicalComposer> |
| 210 | + ) |
| 211 | +} |
0 commit comments