@@ -96,7 +96,7 @@ export type EditorToolbarSlots<
9696 </script >
9797
9898<script setup lang="ts" generic =" T extends ArrayOrNested <EditorToolbarItem >" >
99- import { computed , inject } from ' vue'
99+ import { computed , inject , onBeforeUnmount , shallowRef , watch } from ' vue'
100100import { Primitive , Separator , useForwardProps } from ' reka-ui'
101101import { defu } from ' defu'
102102import { BubbleMenu , FloatingMenu } from ' @tiptap/vue-3/menus'
@@ -154,59 +154,118 @@ const groups = computed(() =>
154154 : []
155155)
156156
157- function isActive(item : EditorToolbarItem ): boolean {
158- if (! props .editor ?.isEditable ) {
159- return false
157+ type ToolbarItemState = {
158+ active: boolean
159+ disabled: boolean
160+ }
161+
162+ const stateVersion = shallowRef (0 )
163+ const itemStateCache = new WeakMap <object , { version: number , state: ToolbarItemState }>()
164+ const dropdownItemsCache = new WeakMap <object , { version: number , items: DropdownMenuItem [][] }>()
165+ let refreshFrameId: number | null = null
166+
167+ function scheduleStateRefresh() {
168+ if (refreshFrameId !== null ) {
169+ return
160170 }
161171
162- // Check for dropdown (has items property)
163- if ((' items' in item ) && item .items ?.length ) {
164- return item .items ?.some ((item ): boolean => isActive (item as EditorToolbarItem )) || false
172+ if (typeof requestAnimationFrame === ' function' ) {
173+ refreshFrameId = requestAnimationFrame (() => {
174+ refreshFrameId = null
175+ stateVersion .value ++
176+ })
177+ return
165178 }
166179
167- // Check for plain button (no kind property)
168- if (! (' kind' in item )) {
169- return item .active ?? false
180+ stateVersion .value ++
181+ }
182+
183+ function cancelStateRefresh() {
184+ if (refreshFrameId !== null && typeof cancelAnimationFrame === ' function' ) {
185+ cancelAnimationFrame (refreshFrameId )
170186 }
171187
172- // Check for editor item (has kind property)
173- const handler = handlers ?.value ?.[item .kind ]
174- return handler ?.isActive (props .editor , item as any ) || false
188+ refreshFrameId = null
175189}
176190
177- function isDisabled(item : EditorToolbarItem ): boolean {
191+ function getItemState(item : EditorToolbarItem ): ToolbarItemState {
192+ const currentVersion = stateVersion .value
193+ const key = item as object
194+ const cached = itemStateCache .get (key )
195+
196+ if (cached && cached .version === currentVersion ) {
197+ return cached .state
198+ }
199+
200+ const state = resolveItemState (item )
201+ itemStateCache .set (key , {
202+ version: currentVersion ,
203+ state
204+ })
205+
206+ return state
207+ }
208+
209+ function resolveItemState(item : EditorToolbarItem ): ToolbarItemState {
178210 if (! props .editor ?.isEditable ) {
179- return true
211+ return {
212+ active: false ,
213+ disabled: true
214+ }
180215 }
181216
182217 if (' items' in item && item .items ?.length ) {
183218 const items = isArrayOfArray (item .items ) ? item .items .flat () : item .items
184- // Filter out structural elements (separators, labels)
185- const actionableItems = items .filter ((item : any ) => item .type !== ' separator' && item .type !== ' label' )
219+ let hasActionableItems = false
220+ let active = false
221+ let allActionableDisabled = true
222+
223+ for (const child of items as EditorToolbarDropdownChildItem []) {
224+ const state = getItemState (child as EditorToolbarItem )
225+ const type = (child as DropdownMenuItem ).type
226+ const isStructural = type === ' separator' || type === ' label'
227+
228+ active = active || state .active
186229
187- if (actionableItems .length === 0 ) {
188- return true
230+ if (! isStructural ) {
231+ hasActionableItems = true
232+ allActionableDisabled = allActionableDisabled && state .disabled
233+ }
189234 }
190235
191- return actionableItems .every ((item : any ) => isDisabled (item ))
236+ return {
237+ active ,
238+ disabled: ! hasActionableItems || allActionableDisabled
239+ }
192240 }
193241
194242 if (! (' kind' in item )) {
195- return item .disabled ?? false
243+ return {
244+ active: item .active ?? false ,
245+ disabled: item .disabled ?? false
246+ }
196247 }
197248
198249 const handler = handlers ?.value ?.[item .kind ]
199250 if (! handler ) {
200- return false
251+ return {
252+ active: false ,
253+ disabled: false
254+ }
201255 }
202256
203- // Check item-specific disabled state
204- if ( handler .isDisabled ?. (props .editor , item )) {
205- return true
257+ return {
258+ active: handler .isActive (props .editor , item as any ) || false ,
259+ disabled: !! handler . isDisabled ?.( props . editor , item ) || ! handler . canExecute ( props . editor , item )
206260 }
261+ }
207262
208- // Check if item can be executed
209- return ! handler .canExecute (props .editor , item )
263+ function isActive(item : EditorToolbarItem ): boolean {
264+ return getItemState (item ).active
265+ }
266+
267+ function isDisabled(item : EditorToolbarItem ): boolean {
268+ return getItemState (item ).disabled
210269}
211270
212271function onClick(e : MouseEvent , item : EditorToolbarItem ) {
@@ -240,7 +299,7 @@ function getActiveChildItem(item: EditorToolbarDropdownItem): EditorToolbarItem
240299 if (! (' kind' in childItem )) {
241300 return false
242301 }
243- return isActive (childItem as EditorToolbarItem )
302+ return getItemState (childItem as EditorToolbarItem ). active
244303 }) as EditorToolbarItem | undefined
245304}
246305
@@ -298,14 +357,61 @@ function mapDropdownItem(item: EditorToolbarDropdownChildItem): DropdownMenuItem
298357}
299358
300359function getDropdownItems(item : EditorToolbarDropdownItem ) {
360+ const currentVersion = stateVersion .value
361+ const key = item as object
362+ const cached = dropdownItemsCache .get (key )
363+
364+ if (cached && cached .version === currentVersion ) {
365+ return cached .items
366+ }
367+
301368 if (! item .items ) {
302369 return []
303370 }
304371
305- return isArrayOfArray (item .items )
372+ const mappedItems = isArrayOfArray (item .items )
306373 ? item .items .map ((group : any ) => group .map (mapDropdownItem ))
307374 : [item .items .map (mapDropdownItem )]
375+
376+ dropdownItemsCache .set (key , {
377+ version: currentVersion ,
378+ items: mappedItems
379+ })
380+
381+ return mappedItems
308382}
383+
384+ watch (() => handlers .value , () => {
385+ scheduleStateRefresh ()
386+ }, { deep: true })
387+
388+ watch (() => props .editor , (editor , _ , onCleanup ) => {
389+ scheduleStateRefresh ()
390+
391+ if (typeof (editor as any )?.on !== ' function' || typeof (editor as any )?.off !== ' function' ) {
392+ return
393+ }
394+
395+ const onStateChange = () => {
396+ scheduleStateRefresh ()
397+ }
398+
399+ editor .on (' selectionUpdate' , onStateChange )
400+ editor .on (' focus' , onStateChange )
401+ editor .on (' blur' , onStateChange )
402+ editor .on (' transaction' , onStateChange )
403+
404+ onCleanup (() => {
405+ editor .off (' selectionUpdate' , onStateChange )
406+ editor .off (' focus' , onStateChange )
407+ editor .off (' blur' , onStateChange )
408+ editor .off (' transaction' , onStateChange )
409+ })
410+ }, { immediate: true })
411+
412+ onBeforeUnmount (() => {
413+ cancelStateRefresh ()
414+ })
309415 </script >
310416
311417<template >
0 commit comments