All files / roosterjs-content-model-plugins/lib/contextMenuBase ContextMenuPluginBase.ts

19.51% Statements 8/41
0% Branches 0/34
12.5% Functions 1/8
17.95% Lines 7/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113                                                              1x                           1x               1x             1x                           1x                                 1x                                       1x  
import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';
 
/**
 * Context Menu options for ContextMenu plugin
 */
export interface ContextMenuOptions<T> {
    /**
     * Render function for the context menu
     * @param container The container HTML element, it will be located at the mouse click position,
     * so the callback just need to render menu content into this container
     * @param onDismiss The onDismiss callback, some menu render need to know this callback so that
     * it can handle the dismiss event
     */
    render: (container: HTMLElement, items: (T | null)[], onDismiss: () => void) => void;
 
    /**
     * Dismiss function for the context menu, it will be called when user wants to dismiss this context menu
     * e.g. user click away so the menu should be dismissed
     * @param container The container HTML element
     */
    dismiss?: (container: HTMLElement) => void;
 
    /**
     * Whether the default context menu is allowed. @default false
     */
    allowDefaultMenu?: boolean;
}
 
/**
 * An editor plugin that support showing a context menu using render() function from options parameter
 */
export class ContextMenuPluginBase<T> implements EditorPlugin {
    private container: HTMLElement | null = null;
    private editor: IEditor | null = null;
    private isMenuShowing: boolean = false;
 
    /**
     * Create a new instance of ContextMenu class
     * @param options An options object to determine how to show/hide the context menu
     */
    constructor(private options: ContextMenuOptions<T>) {}
 
    /**
     * Get a friendly name of  this plugin
     */
    getName() {
        return 'ContextMenu';
    }
 
    /**
     * Initialize this plugin
     * @param editor The editor instance
     */
    initialize(editor: IEditor) {
        this.editor = editor;
    }
 
    /**
     * Dispose this plugin
     */
    dispose() {
        this.onDismiss();
 
        if (this.container?.parentNode) {
            this.container.parentNode.removeChild(this.container);
            this.container = null;
        }
        this.editor = null;
    }
 
    /**
     * Handle events triggered from editor
     * @param event PluginEvent object
     */
    onPluginEvent(event: PluginEvent) {
        if (event.eventType == 'contextMenu' && event.items.length > 0) {
            const { rawEvent, items } = event;
 
            this.onDismiss();
 
            if (!this.options.allowDefaultMenu) {
                rawEvent.preventDefault();
            }
 
            if (this.initContainer(rawEvent.pageX, rawEvent.pageY)) {
                this.options.render(this.container!, items as T[], this.onDismiss);
                this.isMenuShowing = true;
            }
        }
    }
 
    private initContainer(x: number, y: number) {
        if (!this.container && this.editor) {
            this.container = this.editor.getDocument().createElement('div');
 
            this.container.style.position = 'fixed';
            this.container.style.width = '0';
            this.container.style.height = '0';
            this.editor.getDocument().body.appendChild(this.container);
        }
        this.container?.style.setProperty('left', x + 'px');
        this.container?.style.setProperty('top', y + 'px');
        return !!this.container;
    }
 
    private onDismiss = () => {
        if (this.container && this.isMenuShowing) {
            this.options.dismiss?.(this.container);
            this.isMenuShowing = false;
        }
    };
}