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