mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	chore(client/ts): port menus/context_menu
This commit is contained in:
		
							parent
							
								
									38752f0006
								
							
						
					
					
						commit
						5f0ace2886
					
				
							
								
								
									
										214
									
								
								src/public/app/menus/context_menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/public/app/menus/context_menu.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,214 @@
 | 
				
			|||||||
 | 
					import keyboardActionService from '../services/keyboard_actions.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ContextMenuOptions {
 | 
				
			||||||
 | 
					    x: number;
 | 
				
			||||||
 | 
					    y: number;
 | 
				
			||||||
 | 
					    orientation?: "left";
 | 
				
			||||||
 | 
					    selectMenuItemHandler: MenuHandler;
 | 
				
			||||||
 | 
					    items: MenuItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MenuSeparatorItem {
 | 
				
			||||||
 | 
					    title: "----"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MenuCommandItem {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    command?: string;
 | 
				
			||||||
 | 
					    type: string;
 | 
				
			||||||
 | 
					    uiIcon: string;
 | 
				
			||||||
 | 
					    templateNoteId?: string;
 | 
				
			||||||
 | 
					    enabled?: boolean;
 | 
				
			||||||
 | 
					    handler?: MenuHandler;
 | 
				
			||||||
 | 
					    items?: MenuItem[];
 | 
				
			||||||
 | 
					    shortcut?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type MenuItem = MenuCommandItem | MenuSeparatorItem;
 | 
				
			||||||
 | 
					export type MenuHandler = (item: MenuItem, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ContextMenu {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $widget!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private dateContextMenuOpenedMs: number;
 | 
				
			||||||
 | 
					    private options?: ContextMenuOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        this.$widget = $("#context-menu-container");
 | 
				
			||||||
 | 
					        this.$widget.addClass("dropend");
 | 
				
			||||||
 | 
					        this.dateContextMenuOpenedMs = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $(document).on('click', () => this.hide());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async show(options: ContextMenuOptions) {
 | 
				
			||||||
 | 
					        this.options = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.$widget.hasClass("show")) {
 | 
				
			||||||
 | 
					            // The menu is already visible. Hide the menu then open it again
 | 
				
			||||||
 | 
					            // at the new location to re-trigger the opening animation.
 | 
				
			||||||
 | 
					            await this.hide();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$widget.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.addItems(this.$widget, options.items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        keyboardActionService.updateDisplayedShortcuts(this.$widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.positionMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.dateContextMenuOpenedMs = Date.now();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    positionMenu() {
 | 
				
			||||||
 | 
					        if (!this.options) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // the code below tries to detect when dropdown would overflow from page
 | 
				
			||||||
 | 
					        // in such case we'll position it above click coordinates, so it will fit into the client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const CONTEXT_MENU_PADDING = 5; // How many pixels to pad the context menu from edge of screen
 | 
				
			||||||
 | 
					        const CONTEXT_MENU_OFFSET = 0; // How many pixels to offset the context menu by relative to cursor, see #3157
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const clientHeight = document.documentElement.clientHeight;
 | 
				
			||||||
 | 
					        const clientWidth = document.documentElement.clientWidth;
 | 
				
			||||||
 | 
					        const contextMenuHeight = this.$widget.outerHeight();
 | 
				
			||||||
 | 
					        const contextMenuWidth = this.$widget.outerWidth();
 | 
				
			||||||
 | 
					        let top, left;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (contextMenuHeight && this.options.y + contextMenuHeight - CONTEXT_MENU_OFFSET > clientHeight - CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					            // Overflow: bottom
 | 
				
			||||||
 | 
					            top = clientHeight - contextMenuHeight - CONTEXT_MENU_PADDING;
 | 
				
			||||||
 | 
					        } else if (this.options.y - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					            // Overflow: top
 | 
				
			||||||
 | 
					            top = CONTEXT_MENU_PADDING;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            top = this.options.y - CONTEXT_MENU_OFFSET;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.options.orientation === 'left' && contextMenuWidth) {
 | 
				
			||||||
 | 
					            if (this.options.x + CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					                // Overflow: right
 | 
				
			||||||
 | 
					                left = clientWidth - contextMenuWidth - CONTEXT_MENU_OFFSET;
 | 
				
			||||||
 | 
					            } else if (this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					                // Overflow: left
 | 
				
			||||||
 | 
					                left = CONTEXT_MENU_PADDING;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					                // Overflow: right
 | 
				
			||||||
 | 
					                left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
 | 
				
			||||||
 | 
					            } else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
 | 
				
			||||||
 | 
					                // Overflow: left
 | 
				
			||||||
 | 
					                left = CONTEXT_MENU_PADDING;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                left = this.options.x - CONTEXT_MENU_OFFSET;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$widget.css({
 | 
				
			||||||
 | 
					            display: "block",
 | 
				
			||||||
 | 
					            top: top,
 | 
				
			||||||
 | 
					            left: left
 | 
				
			||||||
 | 
					        }).addClass("show");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addItems($parent: JQuery<HTMLElement>, items: MenuItem[]) {
 | 
				
			||||||
 | 
					        for (const item of items) {
 | 
				
			||||||
 | 
					            if (!item) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (item.title === '----') {
 | 
				
			||||||
 | 
					                $parent.append($("<div>").addClass("dropdown-divider"));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const $icon = $("<span>");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if ("uiIcon" in item && item.uiIcon) {
 | 
				
			||||||
 | 
					                    $icon.addClass(item.uiIcon);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $icon.append(" ");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const $link = $("<span>")
 | 
				
			||||||
 | 
					                    .append($icon)
 | 
				
			||||||
 | 
					                    .append("   ") // some space between icon and text
 | 
				
			||||||
 | 
					                    .append(item.title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if ("shortcut" in item && item.shortcut) {
 | 
				
			||||||
 | 
					                    $link.append($("<kbd>").text(item.shortcut));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const $item = $("<li>")
 | 
				
			||||||
 | 
					                    .addClass("dropdown-item")
 | 
				
			||||||
 | 
					                    .append($link)
 | 
				
			||||||
 | 
					                    .on('contextmenu', e => false)
 | 
				
			||||||
 | 
					                    // important to use mousedown instead of click since the former does not change focus
 | 
				
			||||||
 | 
					                    // (especially important for focused text for spell check)
 | 
				
			||||||
 | 
					                    .on('mousedown', e => {
 | 
				
			||||||
 | 
					                        e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (e.which !== 1) { // only left click triggers menu items
 | 
				
			||||||
 | 
					                            return false;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        this.hide();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if ("handler" in item && item.handler) {
 | 
				
			||||||
 | 
					                            item.handler(item, e);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        this.options?.selectMenuItemHandler(item, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // it's important to stop the propagation especially for sub-menus, otherwise the event
 | 
				
			||||||
 | 
					                        // might be handled again by top-level menu
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
 | 
				
			||||||
 | 
					                    $item.addClass("disabled");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if ("items" in item && item.items) {
 | 
				
			||||||
 | 
					                    $item.addClass("dropdown-submenu");
 | 
				
			||||||
 | 
					                    $link.addClass("dropdown-toggle");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const $subMenu = $("<ul>").addClass("dropdown-menu");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    this.addItems($subMenu, item.items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $item.append($subMenu);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $parent.append($item);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async hide() {
 | 
				
			||||||
 | 
					        // this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468
 | 
				
			||||||
 | 
					        // "contextmenu" event also triggers "click" event which depending on the timing can close the just opened context menu
 | 
				
			||||||
 | 
					        // we might filter out right clicks, but then it's better if even right clicks close the context menu
 | 
				
			||||||
 | 
					        if (Date.now() - this.dateContextMenuOpenedMs > 300) {
 | 
				
			||||||
 | 
					            // seems like if we hide the menu immediately, some clicks can get propagated to the underlying component
 | 
				
			||||||
 | 
					            // see https://github.com/zadam/trilium/pull/3805 for details
 | 
				
			||||||
 | 
					            await timeout(100);
 | 
				
			||||||
 | 
					            this.$widget.removeClass("show");
 | 
				
			||||||
 | 
					            this.$widget.hide()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function timeout(ms: number) {
 | 
				
			||||||
 | 
					    return new Promise((accept, reject) => {
 | 
				
			||||||
 | 
					        setTimeout(accept, ms);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const contextMenu = new ContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default contextMenu;
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user