Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to not group context menu item by target #206

Merged
merged 1 commit into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions packages/widgets/src/contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand All @@ -105,6 +111,7 @@ export class ContextMenu {
return true;
}

private _groupByTarget: boolean = true;
private _idTick = 0;
private _items: Private.IItem[] = [];
private _sortBySelector: boolean = true;
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -289,6 +310,10 @@ namespace Private {
target = target.parentElement;
}

if (!groupByTarget) {
result.sort(sortBySelector ? itemCmp : itemCmpRank);
}

// Return the matched and sorted results.
return result;
}
Expand All @@ -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)) {
Expand Down Expand Up @@ -334,7 +359,7 @@ namespace Private {
if (s1 !== s2) {
return s2 - s1;
}

// If specificities are equal
return itemCmpRank(a, b);
}
Expand Down
125 changes: 94 additions & 31 deletions packages/widgets/tests/src/contextmenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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');
});
});
});
Expand Down