1+ import { useEffect } from 'react' ;
2+ import type { BlockNoteEditor , InlineContentFromConfig } from '@blocknote/core' ;
3+ import { wpBridge , makeInstanceId } from '../../lib' ;
4+ import type { InlineWpSize } from '../../lib' ;
5+
6+ //Uses `any` generics this hook is schema-agnostic by design.
7+ type AnyEditor = BlockNoteEditor < any , any , any > ;
8+ type AnyInlineNode = InlineContentFromConfig < any , any > ;
9+
10+ interface InlineWpNode {
11+ type :'inlineWorkPackage' ;
12+ props :{
13+ wpid :string ;
14+ // instanceId MUST be globally unique per document
15+ instanceId :string ;
16+ size :InlineWpSize ;
17+ } ;
18+ content :unknown [ ] ;
19+ }
20+
21+ // Must match InlineWpSize union update both if sizes change
22+ const VALID_SIZES :Set < InlineWpSize > = new Set ( [ 'xxs' , 'xs' , 's' , 'm' ] ) ;
23+
24+ function isInlineWpNode ( node :unknown ) : node is InlineWpNode {
25+ if ( typeof node !== 'object' || node === null ) return false ;
26+ const n = node as Record < string , unknown > ;
27+ if ( n [ 'type' ] !== 'inlineWorkPackage' ) return false ;
28+
29+ const props = n [ 'props' ] ;
30+ if ( typeof props !== 'object' || props === null ) return false ;
31+
32+ const p = props as Record < string , unknown > ;
33+ return (
34+ typeof p [ 'instanceId' ] === 'string' &&
35+ typeof p [ 'wpid' ] === 'string' &&
36+ typeof p [ 'size' ] === 'string' && VALID_SIZES . has ( p [ 'size' ] as InlineWpSize )
37+ ) ;
38+ }
39+
40+ function asInlineNode ( node :InlineWpNode ) :AnyInlineNode {
41+ return node as unknown as AnyInlineNode ;
42+ }
43+
44+ interface FoundInlineBlock {
45+ blockId :string ;
46+ content :AnyInlineNode [ ] ;
47+ chip :InlineWpNode ;
48+ }
49+
50+ function findInlineChip ( editor :AnyEditor , instanceId :string ) :FoundInlineBlock | null {
51+ let found : FoundInlineBlock | null = null ;
52+
53+ editor . forEachBlock ( ( block ) => {
54+ if ( found ) return false ;
55+
56+ const content = ( block . content ?? [ ] ) as AnyInlineNode [ ] ;
57+ const chip = content . find (
58+ ( node ) :node is AnyInlineNode & InlineWpNode =>
59+ isInlineWpNode ( node ) && node . props . instanceId === instanceId
60+ ) ;
61+
62+ if ( chip ) {
63+ found = { blockId :block . id , content, chip } ;
64+ return false ;
65+ }
66+
67+ return true ;
68+ } ) ;
69+
70+ return found ;
71+ }
72+
73+ // The updater returns the updated node, or null to remove it.
74+ // Returns found so the caller can use it without a second traversal.
75+ function updateInlineChip (
76+ editor :AnyEditor ,
77+ instanceId :string ,
78+ updater :( chip :InlineWpNode ) => InlineWpNode | null
79+ ) : FoundInlineBlock | null {
80+ const found = findInlineChip ( editor , instanceId ) ;
81+ if ( ! found ) return null ;
82+
83+ const updatedContent = found . content . reduce < AnyInlineNode [ ] > ( ( acc , node ) => {
84+ if ( ! isInlineWpNode ( node ) || node . props . instanceId !== instanceId ) {
85+ acc . push ( node ) ;
86+ return acc ;
87+ }
88+ const updated = updater ( node ) ;
89+ if ( updated !== null ) acc . push ( asInlineNode ( updated ) ) ;
90+ return acc ;
91+ } , [ ] ) ;
92+
93+ editor . updateBlock ( found . blockId , { content :updatedContent } as any ) ;
94+ return found ;
95+ }
96+
97+ function moveCursorAfter ( editor :AnyEditor , blockId :string ) :void {
98+ requestAnimationFrame ( ( ) => {
99+ editor . focus ( ) ;
100+ editor . setTextCursorPosition ( blockId , 'end' ) ;
101+
102+ const cursor = editor . getTextCursorPosition ( ) ;
103+ if ( ! cursor ?. nextBlock && cursor ?. block ) {
104+ editor . insertBlocks ( [ { type : 'paragraph' , content :[ ] } ] , cursor . block . id , 'after' ) ;
105+ }
106+
107+ const updated = editor . getTextCursorPosition ( ) ;
108+ if ( updated ?. nextBlock ) {
109+ editor . setTextCursorPosition ( updated . nextBlock . id , 'start' ) ;
110+ }
111+ } ) ;
112+ }
113+
114+ function handleResize ( editor :AnyEditor , instanceId :string , size :InlineWpSize ) :void {
115+ if ( size === 'm' ) {
116+ handlePromoteToBlock ( editor , instanceId ) ;
117+ return ;
118+ }
119+ updateInlineChip ( editor , instanceId , ( chip ) => ( {
120+ ...chip ,
121+ props :{ ...chip . props , size } ,
122+ } ) ) ;
123+ }
124+
125+ function handleDelete ( editor :AnyEditor , instanceId :string ) :void {
126+ updateInlineChip ( editor , instanceId , ( ) => null ) ;
127+ }
128+
129+ function handlePromoteToBlock ( editor :AnyEditor , instanceId :string ) :void {
130+ const found = findInlineChip ( editor , instanceId ) ;
131+ if ( ! found ) return ;
132+
133+ // wpid must be a positive integer
134+ const wpid = Number ( found . chip . props . wpid ) ;
135+ if ( Number . isNaN ( wpid ) || wpid <= 0 ) return ;
136+
137+ updateInlineChip ( editor , instanceId , ( ) => null ) ;
138+
139+ const [ insertedBlock ] = editor . insertBlocks (
140+ [ { type :'openProjectWorkPackage' , props : { wpid, initialized :true } } as any ] ,
141+ found . blockId ,
142+ 'after'
143+ ) ;
144+
145+ if ( insertedBlock ?. id ) {
146+ moveCursorAfter ( editor , insertedBlock . id ) ;
147+ }
148+ }
149+
150+ function handleConvertToInline ( editor : AnyEditor , wpid : number , size : InlineWpSize , blockId : string ) : void {
151+ const block = editor . getBlock ( blockId ) ;
152+ if ( ! block ) return ;
153+
154+ const instanceId = makeInstanceId ( ) ;
155+
156+ const [ insertedParagraph ] = editor . insertBlocks (
157+ [
158+ {
159+ type : 'paragraph' ,
160+ content : [
161+ { type : 'inlineWorkPackage' , props : { wpid : String ( wpid ) , instanceId, size } } ,
162+ ] ,
163+ } as any ,
164+ ] ,
165+ blockId ,
166+ 'before'
167+ ) ;
168+
169+ editor . removeBlocks ( [ blockId ] ) ;
170+
171+ requestAnimationFrame ( ( ) => {
172+ if ( ! insertedParagraph ?. id ) return ;
173+ editor . focus ( ) ;
174+ editor . setTextCursorPosition ( insertedParagraph . id , 'end' ) ;
175+ } ) ;
176+ }
177+
178+ // editor instance is stable for the lifetime of the component re-subscription only on editor replacement
179+ export function useInlineWpEvents ( editor : AnyEditor ) :void {
180+ useEffect ( ( ) => {
181+ const offResize = wpBridge . onResize ( ( { instanceId, size } ) => handleResize ( editor , instanceId , size ) ) ;
182+ const offDelete = wpBridge . onDelete ( ( { instanceId } ) => handleDelete ( editor , instanceId ) ) ;
183+ const offToInline = wpBridge . onConvertToInline ( ( { wpid, size, blockId } ) =>
184+ handleConvertToInline ( editor , wpid , size , blockId )
185+ ) ;
186+ return ( ) => {
187+ offResize ( ) ;
188+ offDelete ( ) ;
189+ offToInline ( ) ;
190+ } ;
191+ } , [ editor ] ) ;
192+ }
0 commit comments