@jupyterlab/apputils
Version:
JupyterLab - Application Utilities
329 lines • 14.3 kB
JavaScript
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
import { ObservableList } from '@jupyterlab/observables';
import { SettingRegistry } from '@jupyterlab/settingregistry';
import { findIndex } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { Dialog, showDialog } from '../dialog';
/**
* Default toolbar item rank
*
* #### Notes
* This will place item just before the white spacer item in the notebook toolbar.
*/
const DEFAULT_TOOLBAR_ITEM_RANK = 50;
const TOOLBAR_KEY = 'jupyter.lab.toolbars';
/**
* Display warning when the toolbar definition have been modified.
*
* @param trans Translation bundle
*/
async function displayInformation(trans) {
const result = await showDialog({
title: trans.__('Information'),
body: trans.__('Toolbar customization has changed. You will need to reload JupyterLab to see the changes.'),
buttons: [
Dialog.cancelButton(),
Dialog.okButton({ label: trans.__('Reload') })
]
});
if (result.button.accept) {
location.reload();
}
}
/**
* Set the toolbar definition by accumulating all settings definition.
*
* The list will be populated only with the enabled items.
*
* @param toolbarItems Observable list to populate
* @param registry Application settings registry
* @param factoryName Widget factory name that needs a toolbar
* @param pluginId Settings plugin id
* @param translator Translator object
* @param propertyId Property holding the toolbar definition in the settings; default 'toolbar'
* @returns List of toolbar items
*/
async function setToolbarItems(toolbarItems, registry, factoryName, pluginId, translator, propertyId = 'toolbar') {
var _a;
const trans = translator.load('jupyterlab');
let canonical = null;
let loaded = {};
let listenPlugin = true;
try {
/**
* Populate the plugin's schema defaults.
*
* We keep track of disabled entries in case the plugin is loaded
* after the toolbar initialization.
*/
function populate(schema) {
var _a, _b;
loaded = {};
const pluginDefaults = Object.keys(registry.plugins)
// Filter out the current plugin (will be listed when reloading)
// because we control its addition after the mapping step
.filter(plugin => plugin !== pluginId)
.map(plugin => {
var _a, _b;
const items = (_b = ((_a = registry.plugins[plugin].schema[TOOLBAR_KEY]) !== null && _a !== void 0 ? _a : {})[factoryName]) !== null && _b !== void 0 ? _b : [];
loaded[plugin] = items;
return items;
})
.concat([(_b = ((_a = schema[TOOLBAR_KEY]) !== null && _a !== void 0 ? _a : {})[factoryName]) !== null && _b !== void 0 ? _b : []])
.reduceRight((acc, val) => SettingRegistry.reconcileToolbarItems(acc, val, true), []);
// Apply default value as last step to take into account overrides.json
// The standard toolbars default is [] as the plugin must use
// `jupyter.lab.toolbars.<factory>` to define its default value.
schema.properties[propertyId].default =
SettingRegistry.reconcileToolbarItems(pluginDefaults, schema.properties[propertyId].default, true).sort((a, b) => {
var _a, _b;
return ((_a = a.rank) !== null && _a !== void 0 ? _a : DEFAULT_TOOLBAR_ITEM_RANK) -
((_b = b.rank) !== null && _b !== void 0 ? _b : DEFAULT_TOOLBAR_ITEM_RANK);
});
}
// Transform the plugin object to return different schema than the default.
registry.transform(pluginId, {
compose: plugin => {
var _a, _b, _c, _d, _e;
// Only override the canonical schema the first time.
if (!canonical) {
canonical = JSONExt.deepCopy(plugin.schema);
populate(canonical);
}
const defaults = (_c = ((_b = ((_a = canonical.properties) !== null && _a !== void 0 ? _a : {})[propertyId]) !== null && _b !== void 0 ? _b : {}).default) !== null && _c !== void 0 ? _c : [];
// Initialize the settings
const user = plugin.data.user;
const composite = plugin.data.composite;
// Overrides the value with using the aggregated default for the toolbar property
user[propertyId] =
(_d = plugin.data.user[propertyId]) !== null && _d !== void 0 ? _d : [];
composite[propertyId] = ((_e = SettingRegistry.reconcileToolbarItems(defaults, user[propertyId], false)) !== null && _e !== void 0 ? _e : []).sort((a, b) => {
var _a, _b;
return ((_a = a.rank) !== null && _a !== void 0 ? _a : DEFAULT_TOOLBAR_ITEM_RANK) -
((_b = b.rank) !== null && _b !== void 0 ? _b : DEFAULT_TOOLBAR_ITEM_RANK);
});
plugin.data = { composite, user };
return plugin;
},
fetch: plugin => {
// Only override the canonical schema the first time.
if (!canonical) {
canonical = JSONExt.deepCopy(plugin.schema);
populate(canonical);
}
return {
data: plugin.data,
id: plugin.id,
raw: plugin.raw,
schema: canonical,
version: plugin.version
};
}
});
}
catch (error) {
if (error.name === 'TransformError') {
// Assume the existing transformer is the toolbar builder transformer
// from another factory set up.
listenPlugin = false;
}
else {
throw error;
}
}
// Repopulate the canonical variable after the setting registry has
// preloaded all initial plugins.
const settings = await registry.load(pluginId);
// React to customization by the user
settings.changed.connect(() => {
var _a;
const newItems = (_a = settings.composite[propertyId]) !== null && _a !== void 0 ? _a : [];
transferSettings(newItems);
});
const transferSettings = (newItems) => {
// This is not optimal but safer because a toolbar item with the same
// name cannot be inserted (it will be a no-op). But that could happen
// if the settings are changing the items order.
toolbarItems.clear();
toolbarItems.pushAll(newItems.filter(item => !item.disabled));
};
// Initialize the toolbar
transferSettings((_a = settings.composite[propertyId]) !== null && _a !== void 0 ? _a : []);
// React to plugin changes if no other transformer exists, otherwise bail.
if (!listenPlugin) {
return;
}
registry.pluginChanged.connect(async (sender, plugin) => {
var _a, _b, _c;
// Since the plugin storing the toolbar definition is transformed above,
// if it has changed, it means that a request to reload was triggered.
// Hence the toolbar definitions from the other plugins have been
// automatically reset during the transform step.
if (plugin === pluginId) {
return;
}
// If a plugin changed its toolbar items
const oldItems = (_a = loaded[plugin]) !== null && _a !== void 0 ? _a : [];
const newItems = (_c = ((_b = registry.plugins[plugin].schema[TOOLBAR_KEY]) !== null && _b !== void 0 ? _b : {})[factoryName]) !== null && _c !== void 0 ? _c : [];
if (!JSONExt.deepEqual(oldItems, newItems)) {
if (loaded[plugin]) {
// The plugin has changed, request the user to reload the UI
await displayInformation(trans);
}
else {
if (newItems.length > 0) {
// Empty the default values to avoid toolbar settings collisions.
canonical = null;
const schema = registry.plugins[pluginId].schema;
schema.properties.toolbar.default = [];
// Run again the transformations.
await registry.load(pluginId, true);
}
}
}
});
}
/**
* Create the toolbar factory for a given container widget based
* on a data description stored in settings
*
* @param toolbarRegistry Toolbar widgets registry
* @param settingsRegistry Settings registry
* @param factoryName Toolbar container factory name
* @param pluginId Settings plugin id
* @param translator Translator
* @param propertyId Toolbar definition key in the settings plugin
* @returns List of toolbar widgets factory
*/
export function createToolbarFactory(toolbarRegistry, settingsRegistry, factoryName, pluginId, translator, propertyId = 'toolbar') {
const items = new ObservableList({
itemCmp: (a, b) => JSONExt.deepEqual(a, b)
});
// Get toolbar definition from the settings
setToolbarItems(items, settingsRegistry, factoryName, pluginId, translator, propertyId).catch(reason => {
console.error(`Failed to load toolbar items for factory ${factoryName} from ${pluginId}`, reason);
});
return (widget) => {
const updateToolbar = (list, change) => {
switch (change.type) {
case 'move':
toolbar.move(change.oldIndex, change.newIndex);
break;
case 'add':
change.newValues.forEach(item => toolbar.push({
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
}));
break;
case 'remove':
change.oldValues.forEach(() => toolbar.remove(change.oldIndex));
break;
case 'set':
change.newValues.forEach(item => toolbar.set(change.newIndex, {
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
}));
break;
}
};
const updateWidget = (registry, itemName) => {
const itemIndex = Array.from(items).findIndex(item => item.name === itemName);
if (itemIndex >= 0) {
toolbar.set(itemIndex, {
name: itemName,
widget: toolbarRegistry.createWidget(factoryName, widget, items.get(itemIndex))
});
}
};
const toolbar = new ObservableList({
values: Array.from(items).map(item => {
return {
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
};
})
});
// Re-render the widget if a new factory has been added.
toolbarRegistry.factoryAdded.connect(updateWidget);
items.changed.connect(updateToolbar);
widget.disposed.connect(() => {
items.changed.disconnect(updateToolbar);
toolbarRegistry.factoryAdded.disconnect(updateWidget);
});
return toolbar;
};
}
/**
* Set the toolbar items of a widget from a factory
*
* @param widget Widget with the toolbar to set
* @param factory Toolbar items factory
* @param toolbar Separated toolbar if widget is a raw widget
*/
export function setToolbar(widget, factory, toolbar) {
var _a;
// @ts-expect-error Widget has no toolbar
if (!widget.toolbar && !toolbar) {
console.log(`Widget ${widget.id} has no 'toolbar' and no explicit toolbar was provided.`);
return;
}
// @ts-expect-error Widget has no toolbar
const toolbar_ = (_a = widget.toolbar) !== null && _a !== void 0 ? _a : toolbar;
const items = factory(widget);
if (Array.isArray(items)) {
items.forEach(({ name, widget: item }) => {
toolbar_.addItem(name, item);
});
}
else {
const updateToolbar = (list, changes) => {
switch (changes.type) {
case 'add':
changes.newValues.forEach((item, index) => {
toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
});
break;
case 'move':
changes.oldValues.forEach(item => {
item.widget.parent = null;
});
changes.newValues.forEach((item, index) => {
toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
});
break;
case 'remove':
changes.oldValues.forEach(item => {
item.widget.parent = null;
});
break;
case 'set':
changes.oldValues.forEach(item => {
item.widget.parent = null;
});
changes.newValues.forEach((item, index) => {
const existingIndex = findIndex(toolbar_.names(), name => item.name === name);
if (existingIndex >= 0) {
Array.from(toolbar_.children())[existingIndex].parent = null;
}
toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
});
break;
}
};
updateToolbar(items, {
newIndex: 0,
newValues: Array.from(items),
oldIndex: 0,
oldValues: [],
type: 'add'
});
items.changed.connect(updateToolbar);
widget.disposed.connect(() => {
items.changed.disconnect(updateToolbar);
});
}
}
//# sourceMappingURL=factory.js.map