@@ -35,6 +35,7 @@ import {
3535 $createTextNode ,
3636 KEY_DOWN_COMMAND ,
3737 COMMAND_PRIORITY_NORMAL ,
38+ type NodeKey ,
3839} from "lexical" ;
3940import { LinkNode } from "@lexical/link" ;
4041import { LexicalComposer } from "@lexical/react/LexicalComposer" ;
@@ -43,6 +44,7 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
4344import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary" ;
4445import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin" ;
4546import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin" ;
47+
4648import { nanoid } from "nanoid" ;
4749import { createRegularStyleSheet } from "@webstudio-is/css-engine" ;
4850import type { Instance , Instances } from "@webstudio-is/sdk" ;
@@ -71,7 +73,11 @@ import {
7173} from "~/shared/dom-utils" ;
7274import deepEqual from "fast-deep-equal" ;
7375import { setDataCollapsed } from "~/canvas/collapsed" ;
74- import { $selectedPage , selectInstance } from "~/shared/awareness" ;
76+ import {
77+ $selectedPage ,
78+ addTemporaryInstance ,
79+ selectInstance ,
80+ } from "~/shared/awareness" ;
7581import { shallowEqual } from "shallow-equal" ;
7682
7783const BindInstanceToNodePlugin = ( {
@@ -176,7 +182,21 @@ const OnChangeOnBlurPlugin = ({
176182 return null ;
177183} ;
178184
179- const LinkSelectionPlugin = ( ) => {
185+ const getNodeKeyFromDOMNode = (
186+ dom : Node ,
187+ editor : LexicalEditor
188+ ) : NodeKey | undefined => {
189+ const prop = `__lexicalKey_${ editor . _key } ` ;
190+ return ( dom as Node & Record < typeof prop , NodeKey | undefined > ) [ prop ] ;
191+ } ;
192+
193+ const LinkSelectionPlugin = ( {
194+ rootInstanceSelector,
195+ registerNewLink,
196+ } : {
197+ rootInstanceSelector : InstanceSelector ;
198+ registerNewLink : ( key : NodeKey , instanceId : string ) => void ;
199+ } ) => {
180200 const [ editor ] = useLexicalComposerContext ( ) ;
181201 const [ preservedSelection ] = useState ( $selectedInstanceSelector . get ( ) ) ;
182202
@@ -189,14 +209,43 @@ const LinkSelectionPlugin = () => {
189209 ( { editorState } ) => {
190210 editorState . read ( ( ) => {
191211 const selection = $getSelection ( ) ;
192- // console.log(selection);
193212 if ( ! $isRangeSelection ( selection ) ) {
194213 return false ;
195214 }
196215 const key = selection . anchor . getNode ( ) . getKey ( ) ;
197216
198217 const elt = editor . getElementByKey ( key ) ;
199- const link = elt ?. closest ( `a[${ selectorIdAttribute } ]` ) ;
218+ let link = elt ?. closest ( `a[${ selectorIdAttribute } ]` ) ;
219+ const newLink = elt ?. closest ( `a` ) ;
220+
221+ while ( newLink != null && link == null ) {
222+ // new link detected
223+
224+ // https://github.com/facebook/lexical/blob/b7fa4cf673869dac0c2e0c1fe667e71e72ff6adb/packages/lexical/src/LexicalUtils.ts#L465
225+ const key = getNodeKeyFromDOMNode ( newLink , editor ) ;
226+ if ( key === undefined ) {
227+ console . error ( "Key not found for node" , newLink ) ;
228+ break ;
229+ }
230+
231+ // Register new link
232+ const instanceId = nanoid ( ) ;
233+
234+ newLink . setAttribute ( idAttribute , instanceId ) ;
235+ // We set id + root selector here, for simplicity
236+ // This solves hover behavior during mouseMove for editable child outline
237+ // @todo : A normal selector must be used, but it would require significantly more code to detect the tree structure.
238+ newLink . setAttribute (
239+ selectorIdAttribute ,
240+ [ instanceId , ...rootInstanceSelector ] . join ( "," )
241+ ) ;
242+
243+ registerNewLink ( key , instanceId ) ;
244+
245+ link = newLink ;
246+
247+ break ;
248+ }
200249
201250 if ( link == null ) {
202251 if (
@@ -232,7 +281,7 @@ const LinkSelectionPlugin = () => {
232281 return ( ) => {
233282 removeUpdateListener ( ) ;
234283 } ;
235- } , [ editor , preservedSelection ] ) ;
284+ } , [ editor , preservedSelection , registerNewLink , rootInstanceSelector ] ) ;
236285
237286 return null ;
238287} ;
@@ -950,6 +999,7 @@ export const TextEditor = ({
950999 const [ paragraphClassName ] = useState ( ( ) => `a${ nanoid ( ) } ` ) ;
9511000 const [ italicClassName ] = useState ( ( ) => `a${ nanoid ( ) } ` ) ;
9521001 const lastSavedStateJsonRef = useRef < SerializedEditorState | null > ( null ) ;
1002+ const [ newLinkKeyToInstanceId ] = useState ( ( ) => new Map ( ) ) ;
9531003
9541004 const handleChange = useEffectEvent ( ( editorState : EditorState ) => {
9551005 editorState . read ( ( ) => {
@@ -961,7 +1011,10 @@ export const TextEditor = ({
9611011 return ;
9621012 }
9631013
964- onChange ( $convertToUpdates ( treeRootInstance , refs ) ) ;
1014+ onChange (
1015+ $convertToUpdates ( treeRootInstance , refs , newLinkKeyToInstanceId )
1016+ ) ;
1017+ newLinkKeyToInstanceId . clear ( ) ;
9651018 lastSavedStateJsonRef . current = jsonState ;
9661019 }
9671020
@@ -1113,6 +1166,19 @@ export const TextEditor = ({
11131166 $hoveredInstanceSelector . set ( undefined ) ;
11141167 } , [ ] ) ;
11151168
1169+ const registerNewLink = useCallback (
1170+ ( key : NodeKey , instanceId : string ) => {
1171+ newLinkKeyToInstanceId . set ( key , instanceId ) ;
1172+ addTemporaryInstance ( {
1173+ id : instanceId ,
1174+ component : "RichTextLink" ,
1175+ type : "instance" ,
1176+ children : [ ] ,
1177+ } ) ;
1178+ } ,
1179+ [ newLinkKeyToInstanceId ]
1180+ ) ;
1181+
11161182 return (
11171183 < LexicalComposer initialConfig = { initialConfig } >
11181184 < RemoveParagaphsPlugin />
@@ -1142,7 +1208,10 @@ export const TextEditor = ({
11421208 < SwitchBlockPlugin onNext = { handleNext } />
11431209 < OnChangeOnBlurPlugin onChange = { handleChange } />
11441210 < InitCursorPlugin />
1145- < LinkSelectionPlugin />
1211+ < LinkSelectionPlugin
1212+ rootInstanceSelector = { rootInstanceSelector }
1213+ registerNewLink = { registerNewLink }
1214+ />
11461215 < AnyKeyDownPlugin onKeyDown = { handleAnyKeydown } />
11471216 < InitialJSONStatePlugin
11481217 onInitialState = { ( json ) => {
0 commit comments