diff --git a/packages/widgets/src/contextmenu.ts b/packages/widgets/src/contextmenu.ts index f7009488e..1f08c6472 100644 --- a/packages/widgets/src/contextmenu.ts +++ b/packages/widgets/src/contextmenu.ts @@ -7,15 +7,15 @@ | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ -import { ArrayExt, each } from "@lumino/algorithm"; +import { ArrayExt, each } from '@lumino/algorithm'; -import { CommandRegistry } from "@lumino/commands"; +import { CommandRegistry } from '@lumino/commands'; -import { DisposableDelegate, IDisposable } from "@lumino/disposable"; +import { DisposableDelegate, IDisposable } from '@lumino/disposable'; -import { Selector } from "@lumino/domutils"; +import { Selector } from '@lumino/domutils'; -import { Menu } from "./menu"; +import { Menu } from './menu'; /** * An object which implements a universal context menu. @@ -33,8 +33,9 @@ export class ContextMenu { * @param options - The options for initializing the menu. */ constructor(options: ContextMenu.IOptions) { - const { sortBySelector, ...others } = options; + const { groupByTarget, sortBySelector, ...others } = options; this.menu = new Menu(others); + this._groupByTarget = groupByTarget !== false; this._sortBySelector = sortBySelector !== false; } @@ -86,7 +87,12 @@ export class ContextMenu { } // Find the matching items for the event. - let items = Private.matchItems(this._items, event, this._sortBySelector); + let items = Private.matchItems( + this._items, + event, + this._groupByTarget, + this._sortBySelector + ); // Bail if there are no matching items. if (!items || items.length === 0) { @@ -105,6 +111,7 @@ export class ContextMenu { return true; } + private _groupByTarget: boolean = true; private _idTick = 0; private _items: Private.IItem[] = []; private _sortBySelector: boolean = true; @@ -134,6 +141,17 @@ export namespace ContextMenu { * Default true. */ sortBySelector?: boolean; + + /** + * Whether to group items following the DOM hierarchy. + * + * Default true. + * + * #### Note + * If true, when the mouse event occurs on element `span` within `div.top`, + * the items matching `div.top` will be shown before the ones matching `body`. + */ + groupByTarget?: boolean; } /** @@ -212,7 +230,8 @@ namespace Private { export function matchItems( items: IItem[], event: MouseEvent, - sortBySelector: boolean, + groupByTarget: boolean, + sortBySelector: boolean ): IItem[] | null { // Look up the target of the event. let target = event.target as Element | null; @@ -276,7 +295,9 @@ namespace Private { // Sort the matches for this level and add them to the results. if (matches.length !== 0) { - matches.sort(sortBySelector ? itemCmp : itemCmpRank); + if (groupByTarget) { + matches.sort(sortBySelector ? itemCmp : itemCmpRank); + } result.push(...matches); } @@ -289,6 +310,10 @@ namespace Private { target = target.parentElement; } + if (!groupByTarget) { + result.sort(sortBySelector ? itemCmp : itemCmpRank); + } + // Return the matched and sorted results. return result; } @@ -300,7 +325,7 @@ namespace Private { * invalid or contains commas. */ function validateSelector(selector: string): string { - if (selector.indexOf(",") !== -1) { + if (selector.indexOf(',') !== -1) { throw new Error(`Selector cannot contain commas: ${selector}`); } if (!Selector.isValid(selector)) { @@ -334,7 +359,7 @@ namespace Private { if (s1 !== s2) { return s2 - s1; } - + // If specificities are equal return itemCmpRank(a, b); } diff --git a/packages/widgets/tests/src/contextmenu.spec.ts b/packages/widgets/tests/src/contextmenu.spec.ts index 125f145ca..29dccf217 100644 --- a/packages/widgets/tests/src/contextmenu.spec.ts +++ b/packages/widgets/tests/src/contextmenu.spec.ts @@ -7,72 +7,77 @@ | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ -import { expect } from "chai"; +import { expect } from 'chai'; -// import { simulate } from "simulate-event"; +// import { simulate } from 'simulate-event'; -import { CommandRegistry } from "@lumino/commands"; +import { CommandRegistry } from '@lumino/commands'; -import { JSONObject } from "@lumino/coreutils"; +import { JSONObject } from '@lumino/coreutils'; -import { ContextMenu } from "@lumino/widgets"; +import { ContextMenu } from '@lumino/widgets'; -describe("@lumino/widgets", () => { +describe('@lumino/widgets', () => { let commands = new CommandRegistry(); before(() => { - commands.addCommand("test-1", { + commands.addCommand('test-1', { execute: (args: JSONObject) => {}, - label: "Test 1 Label", + label: 'Test 1 Label', }); - commands.addCommand("test-2", { + commands.addCommand('test-2', { execute: (args: JSONObject) => {}, - label: "Test 2 Label", + label: 'Test 2 Label', }); - commands.addCommand("test-3", { + commands.addCommand('test-3', { execute: (args: JSONObject) => {}, - label: "Test 3 Label", + label: 'Test 3 Label', }); - commands.addCommand("test-4", { + commands.addCommand('test-4', { execute: (args: JSONObject) => {}, - label: "Test 4 Label", + label: 'Test 4 Label', }); }); - describe("ContextMenu", () => { - describe("#open", () => { + describe('ContextMenu', () => { + describe('#open', () => { let menu: ContextMenu; - const CLASSNAME = "menu-1"; - + const CLASSNAME = 'menu-1'; + function addItems(menu: ContextMenu) { menu.addItem({ - command: "test-1", + command: 'test-1', selector: `.${CLASSNAME}`, rank: 20, }); menu.addItem({ - command: "test-2", + command: 'test-2', selector: `.${CLASSNAME}`, rank: 10, }); menu.addItem({ - command: "test-3", + command: 'test-3', selector: `div.${CLASSNAME}`, rank: 30, }); menu.addItem({ - command: "test-4", - selector: ".menu-2", + command: 'test-4', + selector: '.menu-2', rank: 1, }); + menu.addItem({ + command: 'test-5', + selector: 'body', + rank: 15, + }); } afterEach(() => { menu && menu.menu.dispose(); }); - it("should show items matching selector and order by selector and rank", () => { - const target = document.createElement("div"); + it('should show items matching selector, grouped and ordered by selector and rank', () => { + const target = document.createElement('div'); target.className = CLASSNAME; document.body.appendChild(target); @@ -87,15 +92,16 @@ describe("@lumino/widgets", () => { clientX: bb.x, clientY: bb.y, } as any); - - expect(menu.menu.items).to.have.length(3); + + expect(menu.menu.items).to.have.length(4); expect(menu.menu.items[0].command).to.equal('test-3'); expect(menu.menu.items[1].command).to.equal('test-2'); expect(menu.menu.items[2].command).to.equal('test-1'); + expect(menu.menu.items[3].command).to.equal('test-5'); }); - it("should show items matching selector and order only by rank", () => { - const target = document.createElement("div"); + it('should show items matching selector, grouped and ordered only by rank', () => { + const target = document.createElement('div'); target.className = CLASSNAME; document.body.appendChild(target); @@ -110,11 +116,68 @@ describe("@lumino/widgets", () => { clientX: bb.x, clientY: bb.y, } as any); - - expect(menu.menu.items).to.have.length(3); + + expect(menu.menu.items).to.have.length(4); expect(menu.menu.items[0].command).to.equal('test-2'); expect(menu.menu.items[1].command).to.equal('test-1'); expect(menu.menu.items[2].command).to.equal('test-3'); + expect(menu.menu.items[3].command).to.equal('test-5'); + }); + + it('should show items matching selector, ungrouped and ordered by selector and rank', () => { + const target = document.createElement('div'); + target.className = CLASSNAME; + document.body.appendChild(target); + + menu = new ContextMenu({ + commands, + groupByTarget: false, + sortBySelector: false, + }); + addItems(menu); + + const bb = target.getBoundingClientRect() as DOMRect; + + menu.open({ + target, + currentTarget: document.body, + clientX: bb.x, + clientY: bb.y, + } as any); + + expect(menu.menu.items).to.have.length(4); + expect(menu.menu.items[1].command).to.equal('test-5'); + expect(menu.menu.items[0].command).to.equal('test-2'); + expect(menu.menu.items[2].command).to.equal('test-1'); + expect(menu.menu.items[3].command).to.equal('test-3'); + }); + + it('should show items matching selector, ungrouped and ordered only by rank', () => { + const target = document.createElement('div'); + target.className = CLASSNAME; + document.body.appendChild(target); + + menu = new ContextMenu({ + commands, + groupByTarget: false, + sortBySelector: false, + }); + addItems(menu); + + const bb = target.getBoundingClientRect() as DOMRect; + + menu.open({ + target, + currentTarget: document.body, + clientX: bb.x, + clientY: bb.y, + } as any); + + expect(menu.menu.items).to.have.length(4); + expect(menu.menu.items[0].command).to.equal('test-2'); + expect(menu.menu.items[1].command).to.equal('test-5'); + expect(menu.menu.items[2].command).to.equal('test-1'); + expect(menu.menu.items[3].command).to.equal('test-3'); }); }); });