@jupyterlab/ui-components
Version:
JupyterLab - UI components written in React
434 lines (384 loc) • 10.3 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/* global WeakRef */
import { ArrayExt } from '@lumino/algorithm';
import { CommandRegistry } from '@lumino/commands';
import { ReadonlyJSONObject } from '@lumino/coreutils';
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
import { Signal } from '@lumino/signaling';
import { VirtualElement } from '@lumino/virtualdom';
import { Menu } from '@lumino/widgets';
/**
* Interface for disposable item menu
*/
export interface IDisposableMenuItem extends IDisposable, Menu.IItem {}
/**
* A common interface for extensible JupyterLab application menus.
*
* Plugins are still free to define their own menus in any way
* they like. However, JupyterLab defines a few top-level
* application menus that may be extended by plugins as well,
* such as "Edit" and "View"
*/
export interface IRankedMenu extends IDisposable {
/**
* Add a group of menu items specific to a particular
* plugin.
*
* The rank can be set for all items in the group using the
* function argument or per item.
*
* @param items - the list of menu items to add.
* @param rank - the default rank in the menu in which to insert the group.
* @returns Disposable of the group
*/
addGroup(items: Menu.IItemOptions[], rank?: number): IDisposable;
/**
* Add a menu item to the end of the menu.
*
* @param options - The options for creating the menu item.
*
* @returns The disposable menu item added to the menu.
*/
addItem(options: IRankedMenu.IItemOptions): IDisposable;
/**
* A read-only array of the menu items in the menu.
*/
readonly items: ReadonlyArray<Menu.IItem>;
/**
* Menu rank
*/
readonly rank?: number;
}
/**
* Namespace for JupyterLabMenu interfaces
*/
export namespace IRankedMenu {
/**
* Default menu item rank
*/
export const DEFAULT_RANK = 100;
/**
* An options object for creating a menu item.
*/
export interface IItemOptions extends Menu.IItemOptions {
/**
* Menu item rank
*/
rank?: number;
}
/**
* An options object for creating a JupyterLab menu.
*/
export interface IOptions extends Menu.IOptions {
/**
* Whether to include separators between the
* groups that are added to the menu.
*
* Default: true
*/
includeSeparators?: boolean;
/**
* Menu rank
*/
rank?: number;
}
}
/**
* An extensible menu for JupyterLab application menus.
*/
export class RankedMenu extends Menu implements IRankedMenu {
/**
* Construct a new menu.
*
* @param options - Options for the lumino menu.
*/
constructor(options: IRankedMenu.IOptions) {
super(options);
this.addClass('jp-ThemedContainer');
this._rank = options.rank;
this._includeSeparators = options.includeSeparators ?? true;
}
/**
* Menu rank.
*/
get rank(): number | undefined {
return this._rank;
}
/**
* Add a group of menu items specific to a particular
* plugin.
*
* The rank can be set for all items in the group using the
* function argument or per item.
*
* @param items - the list of menu items to add.
* @param rank - the default rank in the menu in which to insert the group.
* @returns Disposable of the group
*/
addGroup(items: IRankedMenu.IItemOptions[], rank?: number): IDisposable {
if (items.length === 0) {
return new DisposableDelegate(() => void 0);
}
const defaultRank = rank ?? IRankedMenu.DEFAULT_RANK;
const sortedItems = items
.map(item => {
return { ...item, rank: item.rank ?? defaultRank };
})
.sort((a, b) => a.rank - b.rank);
// Insert the plugin group into the menu.
let insertIndex = this._ranks.findIndex(rank => sortedItems[0].rank < rank);
if (insertIndex < 0) {
insertIndex = this._ranks.length; // Insert at the end of the menu
}
// Keep an array of the menu items that have been created.
const added: IDisposableMenuItem[] = [];
// Insert a separator before the group.
// Lumino takes care of superfluous leading,
// trailing, and duplicate separators.
if (this._includeSeparators) {
added.push(
this.insertItem(insertIndex++, { type: 'separator', rank: defaultRank })
);
}
// Insert the group.
added.push(
...sortedItems.map(item => {
return this.insertItem(insertIndex++, item);
})
);
// Insert a separator after the group.
if (this._includeSeparators) {
added.push(
this.insertItem(insertIndex++, { type: 'separator', rank: defaultRank })
);
}
return new DisposableDelegate(() => {
added.forEach(i => i.dispose());
});
}
/**
* Add a menu item to the end of the menu.
*
* @param options - The options for creating the menu item.
*
* @returns The menu item added to the menu.
*/
addItem(options: IRankedMenu.IItemOptions): IDisposableMenuItem {
let insertIndex = -1;
if (options.rank) {
insertIndex = this._ranks.findIndex(rank => options.rank! < rank);
}
if (insertIndex < 0) {
insertIndex = this._ranks.length; // Insert at the end of the menu
}
return this.insertItem(insertIndex, options);
}
/**
* Remove all menu items from the menu.
*/
clearItems(): void {
this._ranks.length = 0;
super.clearItems();
}
/**
* Dispose of the resources held by the menu.
*/
dispose(): void {
this._ranks.length = 0;
super.dispose();
}
/**
* Get the rank of the item at index.
*
* @param index Item index.
* @returns Rank of the item.
*/
getRankAt(index: number): number {
return this._ranks[index];
}
/**
* Insert a menu item into the menu at the specified index.
*
* @param index - The index at which to insert the item.
*
* @param options - The options for creating the menu item.
*
* @returns The menu item added to the menu.
*
* #### Notes
* The index will be clamped to the bounds of the items.
*/
insertItem(
index: number,
options: IRankedMenu.IItemOptions
): IDisposableMenuItem {
const clampedIndex = Math.max(0, Math.min(index, this._ranks.length));
ArrayExt.insert(
this._ranks,
clampedIndex,
options.rank ??
Math.max(
IRankedMenu.DEFAULT_RANK,
this._ranks[this._ranks.length - 1] ?? IRankedMenu.DEFAULT_RANK
)
);
const item = super.insertItem(clampedIndex, options);
return new DisposableMenuItem(item, this);
}
/**
* Remove the item at a given index from the menu.
*
* @param index - The index of the item to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
removeItemAt(index: number): void {
ArrayExt.removeAt(this._ranks, index);
super.removeItemAt(index);
}
private _includeSeparators: boolean;
private _rank: number | undefined;
private _ranks: number[] = [];
}
/**
* Disposable Menu Item
*/
class DisposableMenuItem implements IDisposableMenuItem {
/**
* Create a disposable menu item from an item and the menu it belongs to
*
* @param item Menu item
* @param menu Menu
*/
constructor(item: Menu.IItem, menu: Menu) {
this._item = new WeakRef(item);
this._menu = menu;
// dispose this item if the parent menu is disposed
const dispose = (menu: Menu): void => {
menu.disposed.disconnect(dispose, this);
this.dispose();
};
this._menu.disposed.connect(dispose, this);
}
/**
* Whether the menu item is disposed or not.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* The type of the menu item.
*/
get type(): Menu.ItemType {
return this._item.deref()!.type;
}
/**
* The command to execute when the item is triggered.
*/
get command(): string {
return this._item.deref()!.command;
}
/**
* The arguments for the command.
*/
get args(): ReadonlyJSONObject {
return this._item.deref()!.args;
}
/**
* The submenu for a `'submenu'` type item.
*/
get submenu(): Menu | null {
return this._item.deref()!.submenu;
}
/**
* The display label for the menu item.
*/
get label(): string {
return this._item.deref()!.label;
}
/**
* The mnemonic index for the menu item.
*/
get mnemonic(): number {
return this._item.deref()!.mnemonic;
}
/**
* The icon renderer for the menu item.
*/
get icon(): VirtualElement.IRenderer | undefined {
return this._item.deref()!.icon;
}
/**
* The icon class for the menu item.
*/
get iconClass(): string {
return this._item.deref()!.iconClass;
}
/**
* The icon label for the menu item.
*/
get iconLabel(): string {
return this._item.deref()!.iconLabel;
}
/**
* The display caption for the menu item.
*/
get caption(): string {
return this._item.deref()!.caption;
}
/**
* The extra class name for the menu item.
*/
get className(): string {
return this._item.deref()!.className;
}
/**
* The dataset for the menu item.
*/
get dataset(): CommandRegistry.Dataset {
return this._item.deref()!.dataset;
}
/**
* Whether the menu item is enabled.
*/
get isEnabled(): boolean {
return this._item.deref()!.isEnabled;
}
/**
* Whether the menu item is toggled.
*/
get isToggled(): boolean {
return this._item.deref()!.isToggled;
}
/**
* Whether the menu item is visible.
*/
get isVisible(): boolean {
return this._item.deref()!.isVisible;
}
/**
* The key binding for the menu item.
*/
get keyBinding(): CommandRegistry.IKeyBinding | null {
return this._item.deref()!.keyBinding;
}
/**
* Dispose the menu item by removing it from its menu.
*/
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
const item = this._item.deref();
if (item && !this._menu.isDisposed) {
this._menu.removeItem(item);
}
Signal.clearData(this);
}
private _isDisposed: boolean;
private _item: WeakRef<Menu.IItem>;
private _menu: Menu;
}