/** * This module implements context menu to be shown on contextmenu event (most commonly associated * with right+click, but could varies slightly depending on platform, ie: mac support ctrl+click as * well). * * To prevent the default context menu to show everywhere else (including on the top of your custom * context menu) dont forget to prevent it by including below line at the root of the dom: * `dom.on('contextmenu', ev => ev.preventDefault())` */ import { Disposable, dom, DomArg, DomContents, Holder } from "grainjs"; import { cssMenuElem, registerMenuOpen } from 'app/client/ui2018/menus'; import { IOpenController, Menu } from 'popweasel'; export type IContextMenuContentFunc = (ctx: ContextMenuController) => DomContents; class ContextMenuController extends Disposable implements IOpenController { private _content: HTMLElement; constructor(private _event: MouseEvent, contentFunc: IContextMenuContentFunc) { super(); setTimeout(() => this._updatePosition(), 0); // Create content and add to the dom but keep hidden until menu gets positioned const menu = Menu.create(null, this, [contentFunc(this)], { menuCssClass: cssMenuElem.className + ' grist-floating-menu' }); const content = this._content = menu.content; content.style.visibility = 'hidden'; document.body.appendChild(content); // Prevents arrow to move the cursor while menu is open. dom.onKeyElem(content, 'keydown', { ArrowLeft: (ev) => ev.stopPropagation(), ArrowRight: (ev) => ev.stopPropagation() // UP and DOWN are already handle by the menu to navigate the menu) }); // On click anywhere on the page (outside popup content), close it. const onClick = (evt: MouseEvent) => { const target: Node|null = evt.target as Node; if (target && !content.contains(target)) { this.close(); } }; this.autoDispose(dom.onElem(document, 'contextmenu', onClick, {useCapture: true})); this.autoDispose(dom.onElem(document, 'click', onClick, {useCapture: true})); // Cleanup involves removing the element. this.onDispose(() => { dom.domDispose(content); content.remove(); }); registerMenuOpen(this); } public close() { this.dispose(); } public setOpenClass() {} // IOpenController expects a trigger elem but context menu has no trigger. Let's return body for // now. As of time of writing the trigger elem is only used by popweasel when certain options are // enabled, ie: strectToSelector, parentSelectoToMark. // TODO: make a PR on popweasel to support using Menu with no trigger element. public getTriggerElem() { return document.body; } public update() {} private _updatePosition() { const content = this._content; const ev = this._event; const rect = content.getBoundingClientRect(); // position menu on the right of the cursor if it can fit, on the left otherwise content.style.left = ((ev.pageX + rect.width < window.innerWidth) ? ev.pageX : ev.pageX - rect.width) + 'px'; // position menu below the cursor if it can fit, otherwise fit at the bottom of the screen content.style.bottom = Math.max(window.innerHeight - (ev.pageY + rect.height), 0) + 'px'; // show content content.style.visibility = ''; } } /** * Show a context menu on contextmenu. */ export function contextMenu(contentFunc: IContextMenuContentFunc): DomArg { return (elem) => { const holder = Holder.create(null); dom.autoDisposeElem(elem, holder); dom.onElem(elem, 'contextmenu', (ev) => { ev.preventDefault(); ev.stopPropagation(); ContextMenuController.create(holder, ev, contentFunc); }); }; }