UNPKG

@jupyterlab/launcher

Version:
269 lines 9.94 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { showErrorMessage } from '@jupyterlab/apputils'; import { nullTranslator } from '@jupyterlab/translation'; import { classes, LabIcon, VDomModel, VDomRenderer } from '@jupyterlab/ui-components'; import { ArrayExt, map } from '@lumino/algorithm'; import { DisposableDelegate } from '@lumino/disposable'; import { AttachedProperty } from '@lumino/properties'; import { Widget } from '@lumino/widgets'; import * as React from 'react'; /** * The class name added to Launcher instances. */ const LAUNCHER_CLASS = 'jp-Launcher'; /** * LauncherModel keeps track of the path to working directory and has a list of * LauncherItems, which the Launcher will render. */ export class LauncherModel extends VDomModel { constructor() { super(...arguments); this.itemsList = []; } /** * Add a command item to the launcher, and trigger re-render event for parent * widget. * * @param options - The specification options for a launcher item. * * @returns A disposable that will remove the item from Launcher, and trigger * re-render event for parent widget. * */ add(options) { // Create a copy of the options to circumvent mutations to the original. const item = Private.createItem(options); this.itemsList.push(item); this.stateChanged.emit(void 0); return new DisposableDelegate(() => { ArrayExt.removeFirstOf(this.itemsList, item); this.stateChanged.emit(void 0); }); } /** * Return an iterator of launcher items. */ items() { return this.itemsList[Symbol.iterator](); } } /** * A virtual-DOM-based widget for the Launcher. */ export class Launcher extends VDomRenderer { /** * Construct a new launcher widget. */ constructor(options) { super(options.model); this._pending = false; this._cwd = ''; this._cwd = options.cwd; this.translator = options.translator || nullTranslator; this._trans = this.translator.load('jupyterlab'); this._callback = options.callback; this._commands = options.commands; this.addClass(LAUNCHER_CLASS); } /** * The cwd of the launcher. */ get cwd() { return this._cwd; } set cwd(value) { this._cwd = value; this.update(); } /** * Whether there is a pending item being launched. */ get pending() { return this._pending; } set pending(value) { this._pending = value; } /** * Render the launcher to virtual DOM nodes. */ render() { // Bail if there is no model. if (!this.model) { return null; } const knownCategories = [ this._trans.__('Notebook'), this._trans.__('Console'), this._trans.__('Other') ]; const kernelCategories = [ this._trans.__('Notebook'), this._trans.__('Console') ]; // First group-by categories const categories = Object.create(null); for (const item of this.model.items()) { const cat = item.category || this._trans.__('Other'); if (!(cat in categories)) { categories[cat] = []; } categories[cat].push(item); } // Within each category sort by rank for (const cat in categories) { categories[cat] = categories[cat].sort((a, b) => { return Private.sortCmp(a, b, this._cwd, this._commands); }); } // Variable to help create sections const sections = []; let section; // Assemble the final ordered list of categories, beginning with // KNOWN_CATEGORIES. const orderedCategories = []; for (const cat of knownCategories) { orderedCategories.push(cat); } for (const cat in categories) { if (knownCategories.indexOf(cat) === -1) { orderedCategories.push(cat); } } // Now create the sections for each category orderedCategories.forEach(cat => { if (!categories[cat]) { return; } const item = categories[cat][0]; const args = { ...item.args, cwd: this.cwd }; const kernel = kernelCategories.indexOf(cat) > -1; const iconClass = this._commands.iconClass(item.command, args); const icon = this._commands.icon(item.command, args); if (cat in categories) { section = (React.createElement("div", { className: "jp-Launcher-section", key: cat }, React.createElement("div", { className: "jp-Launcher-sectionHeader" }, React.createElement(LabIcon.resolveReact, { icon: icon, iconClass: classes(iconClass, 'jp-Icon-cover'), stylesheet: "launcherSection", "aria-hidden": "true" }), React.createElement("h2", { className: "jp-Launcher-sectionTitle" }, cat)), React.createElement("div", { className: "jp-Launcher-cardContainer" }, Array.from(map(categories[cat], (item) => { return Card(kernel, item, this, this._commands, this._trans, this._callback); }))))); sections.push(section); } }); // Wrap the sections in body and content divs. return (React.createElement("div", { className: "jp-Launcher-body" }, React.createElement("div", { className: "jp-Launcher-content" }, React.createElement("div", { className: "jp-Launcher-cwd" }, React.createElement("h3", null, this.cwd)), sections))); } } /** * A pure tsx component for a launcher card. * * @param kernel - whether the item takes uses a kernel. * * @param item - the launcher item to render. * * @param launcher - the Launcher instance to which this is added. * * @param commands - the command registry holding the command of item. * * @param trans - the translation bundle. * * @returns a vdom `VirtualElement` for the launcher card. */ function Card(kernel, item, launcher, commands, trans, launcherCallback) { // Get some properties of the command const command = item.command; const args = { ...item.args, cwd: launcher.cwd }; const caption = commands.caption(command, args); const label = commands.label(command, args); const title = kernel ? label : caption || label; // Build the onclick handler. const onclick = () => { // If an item has already been launched, // don't try to launch another. if (launcher.pending === true) { return; } launcher.pending = true; void commands .execute(command, { ...item.args, cwd: launcher.cwd }) .then(value => { launcher.pending = false; if (value instanceof Widget) { launcherCallback(value); } }) .catch(err => { console.error(err); launcher.pending = false; void showErrorMessage(trans._p('Error', 'Launcher Error'), err); }); }; // With tabindex working, you can now pick a kernel by tabbing around and // pressing Enter. const onkeypress = (event) => { if (event.key === 'Enter') { onclick(); } }; const iconClass = commands.iconClass(command, args); const icon = commands.icon(command, args); // Return the VDOM element. return (React.createElement("div", { className: "jp-LauncherCard", title: title, onClick: onclick, onKeyPress: onkeypress, tabIndex: 0, "data-category": item.category || trans.__('Other'), key: Private.keyProperty.get(item) }, React.createElement("div", { className: "jp-LauncherCard-icon" }, kernel ? (item.kernelIconUrl ? (React.createElement("img", { src: item.kernelIconUrl, className: "jp-Launcher-kernelIcon", alt: title })) : (React.createElement("div", { className: "jp-LauncherCard-noKernelIcon" }, label[0].toUpperCase()))) : (React.createElement(LabIcon.resolveReact, { icon: icon, iconClass: classes(iconClass, 'jp-Icon-cover'), stylesheet: "launcherCard" }))), React.createElement("div", { className: "jp-LauncherCard-label", title: title }, React.createElement("p", null, label)))); } /** * The namespace for module private data. */ var Private; (function (Private) { /** * An incrementing counter for keys. */ let id = 0; /** * An attached property for an item's key. */ Private.keyProperty = new AttachedProperty({ name: 'key', create: () => id++ }); /** * Create a fully specified item given item options. */ function createItem(options) { return { ...options, category: options.category || '', rank: options.rank !== undefined ? options.rank : Infinity }; } Private.createItem = createItem; /** * A sort comparison function for a launcher item. */ function sortCmp(a, b, cwd, commands) { // First, compare by rank. const r1 = a.rank; const r2 = b.rank; if (r1 !== r2 && r1 !== undefined && r2 !== undefined) { return r1 < r2 ? -1 : 1; // Infinity safe } // Finally, compare by display name. const aLabel = commands.label(a.command, { ...a.args, cwd }); const bLabel = commands.label(b.command, { ...b.args, cwd }); return aLabel.localeCompare(bLabel); } Private.sortCmp = sortCmp; })(Private || (Private = {})); //# sourceMappingURL=widget.js.map