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