@@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
22import keyboardActionService , { getActionSync } from "../services/keyboard_actions.js" ;
33import note_tooltip from "../services/note_tooltip.js" ;
44import utils from "../services/utils.js" ;
5- import { should } from "vitest " ;
5+ import { h , JSX , render } from "preact " ;
66
77export interface ContextMenuOptions < T > {
88 x : number ;
@@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
1515 onHide ?: ( ) => void ;
1616}
1717
18+ export interface CustomMenuItem {
19+ kind : "custom" ,
20+ componentFn : ( ) => JSX . Element | null ;
21+ }
22+
1823export interface MenuSeparatorItem {
1924 kind : "separator" ;
2025}
@@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
5156 columns ?: number ;
5257}
5358
54- export type MenuItem < T > = MenuCommandItem < T > | MenuSeparatorItem | MenuHeader ;
59+ export type MenuItem < T > = MenuCommandItem < T > | CustomMenuItem | MenuSeparatorItem | MenuHeader ;
5560export type MenuHandler < T > = ( item : MenuCommandItem < T > , e : JQuery . MouseDownEvent < HTMLElement , undefined , HTMLElement , HTMLElement > ) => void ;
5661export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery . ContextMenuEvent ;
5762
@@ -202,126 +207,142 @@ class ContextMenu {
202207 $group . append ( $ ( "<h6>" ) . addClass ( "dropdown-header" ) . text ( item . title ) ) ;
203208 shouldResetGroup = true ;
204209 } else {
205- const $icon = $ ( "<span>" ) ;
206-
207- if ( "uiIcon" in item || "checked" in item ) {
208- const icon = ( item . checked ? "bx bx-check" : item . uiIcon ) ;
209- if ( icon ) {
210- $icon . addClass ( icon ) ;
211- } else {
212- $icon . append ( " " ) ;
213- }
210+ if ( "kind" in item && item . kind === "custom" ) {
211+ // Custom menu item
212+ $group . append ( this . createCustomMenuItem ( item ) ) ;
213+ } else {
214+ // Standard menu item
215+ $group . append ( this . createMenuItem ( item ) ) ;
214216 }
215217
216- const $link = $ ( "<span>" )
217- . append ( $icon )
218- . append ( " " ) // some space between icon and text
219- . append ( item . title ) ;
218+ // After adding a menu item, if the previous item was a separator or header,
219+ // reset the group so that the next item will be appended directly to the parent.
220+ if ( shouldResetGroup ) {
221+ $group = $parent ;
222+ shouldResetGroup = false ;
223+ } ;
224+ }
225+ }
226+ }
227+
228+ private createCustomMenuItem ( item : CustomMenuItem ) {
229+ const element = document . createElement ( "li" ) ;
230+ element . classList . add ( "dropdown-custom-item" ) ;
231+ render ( h ( item . componentFn , { } ) , element ) ;
232+ return element ;
233+ }
234+
235+ private createMenuItem ( item : MenuCommandItem < any > ) {
236+ const $icon = $ ( "<span>" ) ;
220237
221- if ( "badges" in item && item . badges ) {
222- for ( let badge of item . badges ) {
223- const badgeElement = $ ( `<span class="badge">` ) . text ( badge . title ) ;
238+ if ( "uiIcon" in item || "checked" in item ) {
239+ const icon = ( item . checked ? "bx bx-check" : item . uiIcon ) ;
240+ if ( icon ) {
241+ $icon . addClass ( icon ) ;
242+ } else {
243+ $icon . append ( " " ) ;
244+ }
245+ }
224246
225- if ( badge . className ) {
226- badgeElement . addClass ( badge . className ) ;
227- }
247+ const $link = $ ( "<span>" )
248+ . append ( $icon )
249+ . append ( " " ) // some space between icon and text
250+ . append ( item . title ) ;
228251
229- $link . append ( badgeElement ) ;
230- }
252+ if ( "badges" in item && item . badges ) {
253+ for ( let badge of item . badges ) {
254+ const badgeElement = $ ( `<span class="badge">` ) . text ( badge . title ) ;
255+
256+ if ( badge . className ) {
257+ badgeElement . addClass ( badge . className ) ;
231258 }
232259
233- if ( "keyboardShortcut" in item && item . keyboardShortcut ) {
234- const shortcuts = getActionSync ( item . keyboardShortcut ) . effectiveShortcuts ;
235- if ( shortcuts ) {
236- const allShortcuts : string [ ] = [ ] ;
237- for ( const effectiveShortcut of shortcuts ) {
238- allShortcuts . push ( effectiveShortcut . split ( "+" )
239- . map ( key => `<kbd>${ key } </kbd>` )
240- . join ( "+" ) ) ;
241- }
242-
243- if ( allShortcuts . length ) {
244- const container = $ ( "<span>" ) . addClass ( "keyboard-shortcut" ) ;
245- container . append ( $ ( allShortcuts . join ( "," ) ) ) ;
246- $link . append ( container ) ;
247- }
248- }
249- } else if ( "shortcut" in item && item . shortcut ) {
250- $link . append ( $ ( "<kbd>" ) . text ( item . shortcut ) ) ;
260+ $link . append ( badgeElement ) ;
261+ }
262+ }
263+
264+ if ( "keyboardShortcut" in item && item . keyboardShortcut ) {
265+ const shortcuts = getActionSync ( item . keyboardShortcut ) . effectiveShortcuts ;
266+ if ( shortcuts ) {
267+ const allShortcuts : string [ ] = [ ] ;
268+ for ( const effectiveShortcut of shortcuts ) {
269+ allShortcuts . push ( effectiveShortcut . split ( "+" )
270+ . map ( key => `<kbd>${ key } </kbd>` )
271+ . join ( "+" ) ) ;
251272 }
252273
253- const $item = $ ( "<li>" )
254- . addClass ( "dropdown-item" )
255- . append ( $link )
256- . on ( "contextmenu" , ( e ) => false )
257- // important to use mousedown instead of click since the former does not change focus
258- // (especially important for focused text for spell check)
259- . on ( "mousedown" , ( e ) => {
260- e . stopPropagation ( ) ;
261-
262- if ( e . which !== 1 ) {
263- // only left click triggers menu items
264- return false ;
265- }
266-
267- if ( this . isMobile && "items" in item && item . items ) {
268- const $item = $ ( e . target ) . closest ( ".dropdown-item" ) ;
269-
270- $item . toggleClass ( "submenu-open" ) ;
271- $item . find ( "ul.dropdown-menu" ) . toggleClass ( "show" ) ;
272- return false ;
273- }
274-
275- if ( "handler" in item && item . handler ) {
276- item . handler ( item , e ) ;
277- }
278-
279- this . options ?. selectMenuItemHandler ( item , e ) ;
280-
281- // it's important to stop the propagation especially for sub-menus, otherwise the event
282- // might be handled again by top-level menu
283- return false ;
284- } ) ;
285-
286- $item . on ( "mouseup" , ( e ) => {
287- // Prevent submenu from failing to expand on mobile
288- if ( ! this . isMobile || ! ( "items" in item && item . items ) ) {
289- e . stopPropagation ( ) ;
290- // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
291- this . hide ( ) ;
292- return false ;
293- }
294- } ) ;
295-
296- if ( "enabled" in item && item . enabled !== undefined && ! item . enabled ) {
297- $item . addClass ( "disabled" ) ;
274+ if ( allShortcuts . length ) {
275+ const container = $ ( "<span>" ) . addClass ( "keyboard-shortcut" ) ;
276+ container . append ( $ ( allShortcuts . join ( "," ) ) ) ;
277+ $link . append ( container ) ;
298278 }
279+ }
280+ } else if ( "shortcut" in item && item . shortcut ) {
281+ $link . append ( $ ( "<kbd>" ) . text ( item . shortcut ) ) ;
282+ }
299283
300- if ( "items" in item && item . items ) {
301- $item . addClass ( "dropdown-submenu" ) ;
302- $link . addClass ( "dropdown-toggle" ) ;
284+ const $item = $ ( "<li>" )
285+ . addClass ( "dropdown-item" )
286+ . append ( $link )
287+ . on ( "contextmenu" , ( e ) => false )
288+ // important to use mousedown instead of click since the former does not change focus
289+ // (especially important for focused text for spell check)
290+ . on ( "mousedown" , ( e ) => {
291+ e . stopPropagation ( ) ;
292+
293+ if ( e . which !== 1 ) {
294+ // only left click triggers menu items
295+ return false ;
296+ }
303297
304- const $subMenu = $ ( "<ul>" ) . addClass ( "dropdown-menu" ) ;
305- const hasColumns = ! ! item . columns && item . columns > 1 ;
306- if ( ! this . isMobile && hasColumns ) {
307- $subMenu . css ( "column-count" , item . columns ! ) ;
308- }
298+ if ( this . isMobile && "items" in item && item . items ) {
299+ const $item = $ ( e . target ) . closest ( ".dropdown-item" ) ;
309300
310- this . addItems ( $subMenu , item . items , hasColumns ) ;
301+ $item . toggleClass ( "submenu-open" ) ;
302+ $item . find ( "ul.dropdown-menu" ) . toggleClass ( "show" ) ;
303+ return false ;
304+ }
311305
312- $item . append ( $subMenu ) ;
306+ if ( "handler" in item && item . handler ) {
307+ item . handler ( item , e ) ;
313308 }
314309
315- $group . append ( $ item) ;
310+ this . options ?. selectMenuItemHandler ( item , e ) ;
316311
317- // After adding a menu item, if the previous item was a separator or header,
318- // reset the group so that the next item will be appended directly to the parent.
319- if ( shouldResetGroup ) {
320- $group = $parent ;
321- shouldResetGroup = false ;
322- } ;
312+ // it's important to stop the propagation especially for sub-menus, otherwise the event
313+ // might be handled again by top-level menu
314+ return false ;
315+ } ) ;
316+
317+ $item . on ( "mouseup" , ( e ) => {
318+ // Prevent submenu from failing to expand on mobile
319+ if ( ! this . isMobile || ! ( "items" in item && item . items ) ) {
320+ e . stopPropagation ( ) ;
321+ // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
322+ this . hide ( ) ;
323+ return false ;
324+ }
325+ } ) ;
326+
327+ if ( "enabled" in item && item . enabled !== undefined && ! item . enabled ) {
328+ $item . addClass ( "disabled" ) ;
329+ }
330+
331+ if ( "items" in item && item . items ) {
332+ $item . addClass ( "dropdown-submenu" ) ;
333+ $link . addClass ( "dropdown-toggle" ) ;
334+
335+ const $subMenu = $ ( "<ul>" ) . addClass ( "dropdown-menu" ) ;
336+ const hasColumns = ! ! item . columns && item . columns > 1 ;
337+ if ( ! this . isMobile && hasColumns ) {
338+ $subMenu . css ( "column-count" , item . columns ! ) ;
323339 }
340+
341+ this . addItems ( $subMenu , item . items , hasColumns ) ;
342+
343+ $item . append ( $subMenu ) ;
324344 }
345+ return $item ;
325346 }
326347
327348 async hide ( ) {
0 commit comments