11import {
2+ BlockNoteEditor ,
23 BlockSchema ,
34 DefaultBlockSchema ,
45 DefaultInlineContentSchema ,
56 DefaultStyleSchema ,
67 InlineContentSchema ,
78 StyleSchema ,
89} from "@blocknote/core" ;
9- import { UseFloatingOptions , flip , offset } from "@floating-ui/react" ;
10- import { FC } from "react" ;
10+ import {
11+ UseFloatingOptions ,
12+ autoUpdate ,
13+ flip ,
14+ offset ,
15+ useDismiss ,
16+ useFloating ,
17+ useInteractions ,
18+ useTransitionStyles ,
19+ } from "@floating-ui/react" ;
20+ import { FC , useEffect , useState } from "react" ;
1121
1222import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js" ;
13- import { useUIElementPositioning } from "../../hooks/useUIElementPositioning.js" ;
14- import { useUIPluginState } from "../../hooks/useUIPluginState.js" ;
1523import { LinkToolbar } from "./LinkToolbar.js" ;
1624import { LinkToolbarProps } from "./LinkToolbarProps.js" ;
25+ import { getMarkRange , posToDOMRect } from "@tiptap/core" ;
26+ import { isEventTargetWithin } from "@floating-ui/react/utils" ;
27+ import { useEditorState } from "../../hooks/useEditorState.js" ;
28+ import { useElementHover } from "../../hooks/useElementHover.js" ;
29+
30+ function getLinkElementAtPos (
31+ editor : BlockNoteEditor < any , any , any > ,
32+ pos : number ,
33+ ) {
34+ let currentNode = editor . prosemirrorView . nodeDOM ( pos ) ;
35+ while ( currentNode && currentNode . parentElement ) {
36+ if ( currentNode . nodeName === "A" ) {
37+ return currentNode as HTMLAnchorElement ;
38+ }
39+ currentNode = currentNode . parentElement ;
40+ }
41+ return null ;
42+ }
43+
44+ function getLinkAtElement (
45+ editor : BlockNoteEditor < any , any , any > ,
46+ element : HTMLElement ,
47+ ) {
48+ return editor . transact ( ( ) => {
49+ const posAtElement = editor . prosemirrorView . posAtDOM ( element , 0 ) + 1 ;
50+ return getMarkAtPos ( editor , posAtElement , "link" ) ;
51+ } ) ;
52+ }
53+
54+ function getLinkAtSelection ( editor : BlockNoteEditor < any , any , any > ) {
55+ return editor . transact ( ( tr ) => {
56+ const selection = tr . selection ;
57+ return getMarkAtPos ( editor , selection . anchor , "link" ) ;
58+ } ) ;
59+ }
60+
61+ function getMarkAtPos (
62+ editor : BlockNoteEditor < any , any , any > ,
63+ pos : number ,
64+ markType : string ,
65+ ) {
66+ return editor . transact ( ( tr ) => {
67+ const resolvedPos = tr . doc . resolve ( pos ) ;
68+ const mark = resolvedPos
69+ . marks ( )
70+ . find ( ( mark ) => mark . type . name === markType ) ;
71+
72+ if ( ! mark ) {
73+ return ;
74+ }
75+
76+ const markRange = getMarkRange ( resolvedPos , mark . type ) ;
77+ if ( ! markRange ) {
78+ return ;
79+ }
80+
81+ return {
82+ range : markRange ,
83+ mark,
84+ get text ( ) {
85+ return tr . doc . textBetween ( markRange . from , markRange . to ) ;
86+ } ,
87+ get position ( ) {
88+ // toJSON is always a new reference, so we remove it
89+ const { toJSON, ...position } = posToDOMRect (
90+ editor . prosemirrorView ,
91+ markRange . from ,
92+ markRange . to ,
93+ ) ;
94+ return position ;
95+ } ,
96+ } ;
97+ } ) ;
98+ }
99+
100+ function isWithinEditor (
101+ editor : BlockNoteEditor ,
102+ element : HTMLElement | EventTarget ,
103+ ) {
104+ const editorWrapper = editor . prosemirrorView . dom . parentElement ;
105+ if ( ! editorWrapper ) {
106+ return false ;
107+ }
108+
109+ return (
110+ editorWrapper === ( element as Node ) ||
111+ editorWrapper . contains ( element as Node )
112+ ) ;
113+ }
17114
18115export const LinkToolbarController = <
19116 BSchema extends BlockSchema = DefaultBlockSchema ,
@@ -24,45 +121,163 @@ export const LinkToolbarController = <
24121 floatingOptions ?: Partial < UseFloatingOptions > ;
25122} ) => {
26123 const editor = useBlockNoteEditor < BSchema , I , S > ( ) ;
124+ const linkAtSelection = useEditorState ( {
125+ editor,
126+ selector : ( { editor } ) => {
127+ return getLinkAtSelection ( editor ) ;
128+ // if (!linkAtSelection) {
129+ // return;
130+ // }
131+ // const { range, text, mark, position } = linkAtSelection;
132+ // console.log(position);
133+ // return { range, text, mark };
134+ } ,
135+ } ) ;
27136
28137 const callbacks = {
29138 deleteLink : editor . linkToolbar . deleteLink ,
30139 editLink : editor . linkToolbar . editLink ,
31140 startHideTimer : editor . linkToolbar . startHideTimer ,
32141 stopHideTimer : editor . linkToolbar . stopHideTimer ,
33142 } ;
143+ const [ show , setShow ] = useState ( false ) ;
34144
35- const state = useUIPluginState (
36- editor . linkToolbar . onUpdate . bind ( editor . linkToolbar ) ,
37- ) ;
38- const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning (
39- state ?. show || false ,
40- state ?. referencePos || null ,
41- 4000 ,
42- {
43- placement : "top-start" ,
44- middleware : [ offset ( 10 ) , flip ( ) ] ,
45- onOpenChange : ( open ) => {
46- if ( ! open ) {
47- editor . linkToolbar . closeMenu ( ) ;
48- editor . focus ( ) ;
49- }
50- } ,
51- ...props . floatingOptions ,
145+ const {
146+ refs : { setFloating : ref , setReference } ,
147+ context,
148+ floatingStyles,
149+ } = useFloating ( {
150+ open : show ,
151+ placement : "top-start" ,
152+ middleware : [ offset ( 10 ) , flip ( ) ] ,
153+ onOpenChange : ( open , event , reason ) => {
154+ console . log ( "openChange" , open , event , reason ) ;
155+ setShow ( open ) ;
52156 } ,
53- ) ;
157+ whileElementsMounted : autoUpdate ,
158+ ...props . floatingOptions ,
159+ } ) ;
160+
161+ const { isMounted, styles } = useTransitionStyles ( context ) ;
54162
55- if ( ! isMounted || ! state ) {
163+ // handle "escape" and other dismiss events, these will add some listeners to
164+ // getFloatingProps which need to be attached to the floating element
165+ const dismiss = useDismiss ( context , {
166+ outsidePress : ( e ) =>
167+ ! isEventTargetWithin ( e , editor . prosemirrorView . dom . parentElement ) ,
168+ } ) ;
169+
170+ console . log ( linkAtSelection ) ;
171+ // Create element hover hook for link detection
172+ const elementHover = useElementHover ( context , {
173+ enabled : ! linkAtSelection ,
174+ attachTo ( ) {
175+ return editor . prosemirrorView . dom ;
176+ } ,
177+ delay : { open : 250 , close : 0 } ,
178+ restMs : 0 ,
179+ mouseOnly : true ,
180+ getElementAtHover : ( target ) => {
181+ // Check if there's a link at the current selection first
182+ if ( getLinkAtSelection ( editor ) ) {
183+ return null ; // Disable hover when link is selected
184+ }
185+
186+ // Check for link at the hovered element
187+ const linkAtElement = getLinkAtElement ( editor , target as HTMLElement ) ;
188+ if ( linkAtElement ) {
189+ return getLinkElementAtPos ( editor , linkAtElement . range . from ) ;
190+ }
191+
192+ return null ;
193+ } ,
194+ onHover : ( element ) => {
195+ if ( element ) {
196+ setReference ( element ) ;
197+ setShow ( true ) ;
198+ } else {
199+ setReference ( null ) ;
200+ setShow ( false ) ;
201+ }
202+ } ,
203+ } ) ;
204+
205+ const { getReferenceProps, getFloatingProps } = useInteractions ( [
206+ dismiss ,
207+ elementHover ,
208+ ] ) ;
209+
210+ useEffect ( ( ) => {
211+ const abortController = new AbortController ( ) ;
212+ const props = getReferenceProps ( ) ;
213+
214+ for ( const [ key , eventListener ] of Object . entries ( props ) ) {
215+ if ( typeof eventListener === "function" && key . startsWith ( "on" ) ) {
216+ editor . prosemirrorView . dom . addEventListener (
217+ // e.g. "onKeyDown" -> "keydown"
218+ key . slice ( 2 ) . toLowerCase ( ) as keyof HTMLElementEventMap ,
219+ eventListener as ( e : Event ) => void ,
220+ {
221+ signal : abortController . signal ,
222+ } ,
223+ ) ;
224+ }
225+ }
226+
227+ return ( ) => {
228+ abortController . abort ( ) ;
229+ } ;
230+ } , [ editor , getReferenceProps ] ) ;
231+
232+ useEffect ( ( ) => {
233+ if ( ! linkAtSelection ) {
234+ setReference ( null ) ;
235+ return ;
236+ }
237+
238+ setReference ( getLinkElementAtPos ( editor , linkAtSelection . range . from ) ) ;
239+ setShow ( true ) ;
240+ } , [ editor , linkAtSelection , setReference ] ) ;
241+ const style = {
242+ display : "flex" ,
243+ ...styles ,
244+ ...floatingStyles ,
245+ zIndex : 4000 ,
246+ } ;
247+ // const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning(
248+ // state?.show || false,
249+ // state?.referencePos || null,
250+ // 4000,
251+ // // {
252+ // // placement: "top-start",
253+ // // middleware: [offset(10), flip()],
254+ // // onOpenChange: (open) => {
255+ // // if (!open) {
256+ // // editor.linkToolbar.closeMenu();
257+ // // editor.focus();
258+ // // }
259+ // // },
260+ // // ...props.floatingOptions,
261+ // // },
262+ // );
263+ // console.log(show, linkAtSelection);
264+ if ( ! isMounted ) {
56265 return null ;
57266 }
58267
59- const { show, referencePos, ...data } = state ;
60-
61268 const Component = props . linkToolbar || LinkToolbar ;
62269
270+ console . log ( "showing" ) ;
63271 return (
64272 < div ref = { ref } style = { style } { ...getFloatingProps ( ) } >
65- < Component { ...data } { ...callbacks } />
273+ < Component
274+ url = { String ( linkAtSelection ?. mark . attrs . href || "" ) }
275+ text = { linkAtSelection ?. text || "" }
276+ deleteLink = { callbacks . deleteLink }
277+ editLink = { callbacks . editLink }
278+ startHideTimer = { callbacks . startHideTimer }
279+ stopHideTimer = { callbacks . stopHideTimer }
280+ />
66281 </ div >
67282 ) ;
68283} ;
0 commit comments