diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 03ec0f330..806e7a993 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -81,6 +81,72 @@ class CommandRegistry { return this._keyBindings; } + get contextEvent(): MouseEvent | null { + return this._contextEvent; + } + + set contextEvent(event: MouseEvent | null) { + if (event === null) { + this._contextEvent = null; + this._contextEventCurrentTarget = null; + } + else { + this._contextEvent = event; + // event.currentTarget is nulled when the next event occurs, so record for later + this._contextEventCurrentTarget = event.currentTarget as (Element | null); + } + } + + contextEventTarget(selector: string): Element | null { + // Validate the selector + if (selector.indexOf(',') !== -1) { + throw new Error(`Selector cannot contain commas: ${selector}`); + } + if (!Selector.isValid(selector)) { + throw new Error(`Invalid selector: ${selector}`); + } + + let event = this._contextEvent; + let currentTarget = this._contextEventCurrentTarget; + if (!event || !currentTarget) { + return null; + } + + // Look up the target of the event. + let target = event.target as (Element | null); + + // Bail if there is no target. + if (!target) { + return null; + } + + // There are some third party libraries that cause the `target` to + // be detached from the DOM before Phosphor can process the event. + if (!currentTarget.contains(target)) { + target = document.elementFromPoint(event.clientX, event.clientY); + if (!target || !currentTarget.contains(target)) { + return null; + } + } + + while (target !== null) { + // Return the first Element that matches the selector + if (Selector.matches(target, selector)) { + return target; + } + + // Stop searching at the limits of the DOM range. + if (target === currentTarget) { + return null; + } + + // Step to the parent DOM level. + target = target.parentElement; + } + + return null; + } + /** * List the ids of the registered commands. * @@ -565,6 +631,8 @@ class CommandRegistry { private _commandChanged = new Signal(this); private _commandExecuted = new Signal(this); private _keyBindingChanged = new Signal(this); + private _contextEvent: MouseEvent | null = null; + private _contextEventCurrentTarget: Element | null = null; } diff --git a/packages/widgets/src/contextmenu.ts b/packages/widgets/src/contextmenu.ts index fb9b57f3e..df5c64b48 100644 --- a/packages/widgets/src/contextmenu.ts +++ b/packages/widgets/src/contextmenu.ts @@ -25,6 +25,32 @@ import { Menu } from './menu'; +import { + Message +} from '@phosphor/messaging'; + +export +class MenuForContextMenu extends Menu { + triggerActiveItem(): void { + this._doCleanupContextEvent = false; + super.triggerActiveItem(); + this._cleanupContextEvent(); + this._doCleanupContextEvent = true; + } + + protected onCloseRequest(msg: Message): void { + super.onCloseRequest(msg); + if (this._doCleanupContextEvent) { + this._cleanupContextEvent(); + } + } + + private _cleanupContextEvent(): void { + this.commands.contextEvent = null; + } + + private _doCleanupContextEvent: boolean = true; +} /** * An object which implements a universal context menu. @@ -43,13 +69,13 @@ class ContextMenu { * @param options - The options for initializing the menu. */ constructor(options: ContextMenu.IOptions) { - this.menu = new Menu(options); + this.menu = new MenuForContextMenu(options); } /** * The menu widget which displays the matched context items. */ - readonly menu: Menu; + readonly menu: MenuForContextMenu; /** * Add an item to the context menu. @@ -101,6 +127,9 @@ class ContextMenu { return false; } + // Record the initiating event for use in commands + this.menu.commands.contextEvent = event; + // Add the filtered items to the menu. each(items, item => { this.menu.addItem(item); });