UNPKG

14.3 kBJavaScriptView Raw
1/*
2 * Copyright (c) Jupyter Development Team.
3 * Distributed under the terms of the Modified BSD License.
4 */
5import { ObservableList } from '@jupyterlab/observables';
6import { SettingRegistry } from '@jupyterlab/settingregistry';
7import { findIndex } from '@lumino/algorithm';
8import { JSONExt } from '@lumino/coreutils';
9import { Dialog, showDialog } from '../dialog';
10/**
11 * Default toolbar item rank
12 *
13 * #### Notes
14 * This will place item just before the white spacer item in the notebook toolbar.
15 */
16const DEFAULT_TOOLBAR_ITEM_RANK = 50;
17const TOOLBAR_KEY = 'jupyter.lab.toolbars';
18/**
19 * Display warning when the toolbar definition have been modified.
20 *
21 * @param trans Translation bundle
22 */
23async function displayInformation(trans) {
24 const result = await showDialog({
25 title: trans.__('Information'),
26 body: trans.__('Toolbar customization has changed. You will need to reload JupyterLab to see the changes.'),
27 buttons: [
28 Dialog.cancelButton(),
29 Dialog.okButton({ label: trans.__('Reload') })
30 ]
31 });
32 if (result.button.accept) {
33 location.reload();
34 }
35}
36/**
37 * Set the toolbar definition by accumulating all settings definition.
38 *
39 * The list will be populated only with the enabled items.
40 *
41 * @param toolbarItems Observable list to populate
42 * @param registry Application settings registry
43 * @param factoryName Widget factory name that needs a toolbar
44 * @param pluginId Settings plugin id
45 * @param translator Translator object
46 * @param propertyId Property holding the toolbar definition in the settings; default 'toolbar'
47 * @returns List of toolbar items
48 */
49async function setToolbarItems(toolbarItems, registry, factoryName, pluginId, translator, propertyId = 'toolbar') {
50 var _a;
51 const trans = translator.load('jupyterlab');
52 let canonical = null;
53 let loaded = {};
54 let listenPlugin = true;
55 try {
56 /**
57 * Populate the plugin's schema defaults.
58 *
59 * We keep track of disabled entries in case the plugin is loaded
60 * after the toolbar initialization.
61 */
62 function populate(schema) {
63 var _a, _b;
64 loaded = {};
65 const pluginDefaults = Object.keys(registry.plugins)
66 // Filter out the current plugin (will be listed when reloading)
67 // because we control its addition after the mapping step
68 .filter(plugin => plugin !== pluginId)
69 .map(plugin => {
70 var _a, _b;
71 const items = (_b = ((_a = registry.plugins[plugin].schema[TOOLBAR_KEY]) !== null && _a !== void 0 ? _a : {})[factoryName]) !== null && _b !== void 0 ? _b : [];
72 loaded[plugin] = items;
73 return items;
74 })
75 .concat([(_b = ((_a = schema[TOOLBAR_KEY]) !== null && _a !== void 0 ? _a : {})[factoryName]) !== null && _b !== void 0 ? _b : []])
76 .reduceRight((acc, val) => SettingRegistry.reconcileToolbarItems(acc, val, true), []);
77 // Apply default value as last step to take into account overrides.json
78 // The standard toolbars default is [] as the plugin must use
79 // `jupyter.lab.toolbars.<factory>` to define its default value.
80 schema.properties[propertyId].default =
81 SettingRegistry.reconcileToolbarItems(pluginDefaults, schema.properties[propertyId].default, true).sort((a, b) => {
82 var _a, _b;
83 return ((_a = a.rank) !== null && _a !== void 0 ? _a : DEFAULT_TOOLBAR_ITEM_RANK) -
84 ((_b = b.rank) !== null && _b !== void 0 ? _b : DEFAULT_TOOLBAR_ITEM_RANK);
85 });
86 }
87 // Transform the plugin object to return different schema than the default.
88 registry.transform(pluginId, {
89 compose: plugin => {
90 var _a, _b, _c, _d, _e;
91 // Only override the canonical schema the first time.
92 if (!canonical) {
93 canonical = JSONExt.deepCopy(plugin.schema);
94 populate(canonical);
95 }
96 const defaults = (_c = ((_b = ((_a = canonical.properties) !== null && _a !== void 0 ? _a : {})[propertyId]) !== null && _b !== void 0 ? _b : {}).default) !== null && _c !== void 0 ? _c : [];
97 // Initialize the settings
98 const user = plugin.data.user;
99 const composite = plugin.data.composite;
100 // Overrides the value with using the aggregated default for the toolbar property
101 user[propertyId] =
102 (_d = plugin.data.user[propertyId]) !== null && _d !== void 0 ? _d : [];
103 composite[propertyId] = ((_e = SettingRegistry.reconcileToolbarItems(defaults, user[propertyId], false)) !== null && _e !== void 0 ? _e : []).sort((a, b) => {
104 var _a, _b;
105 return ((_a = a.rank) !== null && _a !== void 0 ? _a : DEFAULT_TOOLBAR_ITEM_RANK) -
106 ((_b = b.rank) !== null && _b !== void 0 ? _b : DEFAULT_TOOLBAR_ITEM_RANK);
107 });
108 plugin.data = { composite, user };
109 return plugin;
110 },
111 fetch: plugin => {
112 // Only override the canonical schema the first time.
113 if (!canonical) {
114 canonical = JSONExt.deepCopy(plugin.schema);
115 populate(canonical);
116 }
117 return {
118 data: plugin.data,
119 id: plugin.id,
120 raw: plugin.raw,
121 schema: canonical,
122 version: plugin.version
123 };
124 }
125 });
126 }
127 catch (error) {
128 if (error.name === 'TransformError') {
129 // Assume the existing transformer is the toolbar builder transformer
130 // from another factory set up.
131 listenPlugin = false;
132 }
133 else {
134 throw error;
135 }
136 }
137 // Repopulate the canonical variable after the setting registry has
138 // preloaded all initial plugins.
139 const settings = await registry.load(pluginId);
140 // React to customization by the user
141 settings.changed.connect(() => {
142 var _a;
143 const newItems = (_a = settings.composite[propertyId]) !== null && _a !== void 0 ? _a : [];
144 transferSettings(newItems);
145 });
146 const transferSettings = (newItems) => {
147 // This is not optimal but safer because a toolbar item with the same
148 // name cannot be inserted (it will be a no-op). But that could happen
149 // if the settings are changing the items order.
150 toolbarItems.clear();
151 toolbarItems.pushAll(newItems.filter(item => !item.disabled));
152 };
153 // Initialize the toolbar
154 transferSettings((_a = settings.composite[propertyId]) !== null && _a !== void 0 ? _a : []);
155 // React to plugin changes if no other transformer exists, otherwise bail.
156 if (!listenPlugin) {
157 return;
158 }
159 registry.pluginChanged.connect(async (sender, plugin) => {
160 var _a, _b, _c;
161 // Since the plugin storing the toolbar definition is transformed above,
162 // if it has changed, it means that a request to reload was triggered.
163 // Hence the toolbar definitions from the other plugins have been
164 // automatically reset during the transform step.
165 if (plugin === pluginId) {
166 return;
167 }
168 // If a plugin changed its toolbar items
169 const oldItems = (_a = loaded[plugin]) !== null && _a !== void 0 ? _a : [];
170 const newItems = (_c = ((_b = registry.plugins[plugin].schema[TOOLBAR_KEY]) !== null && _b !== void 0 ? _b : {})[factoryName]) !== null && _c !== void 0 ? _c : [];
171 if (!JSONExt.deepEqual(oldItems, newItems)) {
172 if (loaded[plugin]) {
173 // The plugin has changed, request the user to reload the UI
174 await displayInformation(trans);
175 }
176 else {
177 if (newItems.length > 0) {
178 // Empty the default values to avoid toolbar settings collisions.
179 canonical = null;
180 const schema = registry.plugins[pluginId].schema;
181 schema.properties.toolbar.default = [];
182 // Run again the transformations.
183 await registry.load(pluginId, true);
184 }
185 }
186 }
187 });
188}
189/**
190 * Create the toolbar factory for a given container widget based
191 * on a data description stored in settings
192 *
193 * @param toolbarRegistry Toolbar widgets registry
194 * @param settingsRegistry Settings registry
195 * @param factoryName Toolbar container factory name
196 * @param pluginId Settings plugin id
197 * @param translator Translator
198 * @param propertyId Toolbar definition key in the settings plugin
199 * @returns List of toolbar widgets factory
200 */
201export function createToolbarFactory(toolbarRegistry, settingsRegistry, factoryName, pluginId, translator, propertyId = 'toolbar') {
202 const items = new ObservableList({
203 itemCmp: (a, b) => JSONExt.deepEqual(a, b)
204 });
205 // Get toolbar definition from the settings
206 setToolbarItems(items, settingsRegistry, factoryName, pluginId, translator, propertyId).catch(reason => {
207 console.error(`Failed to load toolbar items for factory ${factoryName} from ${pluginId}`, reason);
208 });
209 return (widget) => {
210 const updateToolbar = (list, change) => {
211 switch (change.type) {
212 case 'move':
213 toolbar.move(change.oldIndex, change.newIndex);
214 break;
215 case 'add':
216 change.newValues.forEach(item => toolbar.push({
217 name: item.name,
218 widget: toolbarRegistry.createWidget(factoryName, widget, item)
219 }));
220 break;
221 case 'remove':
222 change.oldValues.forEach(() => toolbar.remove(change.oldIndex));
223 break;
224 case 'set':
225 change.newValues.forEach(item => toolbar.set(change.newIndex, {
226 name: item.name,
227 widget: toolbarRegistry.createWidget(factoryName, widget, item)
228 }));
229 break;
230 }
231 };
232 const updateWidget = (registry, itemName) => {
233 const itemIndex = Array.from(items).findIndex(item => item.name === itemName);
234 if (itemIndex >= 0) {
235 toolbar.set(itemIndex, {
236 name: itemName,
237 widget: toolbarRegistry.createWidget(factoryName, widget, items.get(itemIndex))
238 });
239 }
240 };
241 const toolbar = new ObservableList({
242 values: Array.from(items).map(item => {
243 return {
244 name: item.name,
245 widget: toolbarRegistry.createWidget(factoryName, widget, item)
246 };
247 })
248 });
249 // Re-render the widget if a new factory has been added.
250 toolbarRegistry.factoryAdded.connect(updateWidget);
251 items.changed.connect(updateToolbar);
252 widget.disposed.connect(() => {
253 items.changed.disconnect(updateToolbar);
254 toolbarRegistry.factoryAdded.disconnect(updateWidget);
255 });
256 return toolbar;
257 };
258}
259/**
260 * Set the toolbar items of a widget from a factory
261 *
262 * @param widget Widget with the toolbar to set
263 * @param factory Toolbar items factory
264 * @param toolbar Separated toolbar if widget is a raw widget
265 */
266export function setToolbar(widget, factory, toolbar) {
267 var _a;
268 // @ts-expect-error Widget has no toolbar
269 if (!widget.toolbar && !toolbar) {
270 console.log(`Widget ${widget.id} has no 'toolbar' and no explicit toolbar was provided.`);
271 return;
272 }
273 // @ts-expect-error Widget has no toolbar
274 const toolbar_ = (_a = widget.toolbar) !== null && _a !== void 0 ? _a : toolbar;
275 const items = factory(widget);
276 if (Array.isArray(items)) {
277 items.forEach(({ name, widget: item }) => {
278 toolbar_.addItem(name, item);
279 });
280 }
281 else {
282 const updateToolbar = (list, changes) => {
283 switch (changes.type) {
284 case 'add':
285 changes.newValues.forEach((item, index) => {
286 toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
287 });
288 break;
289 case 'move':
290 changes.oldValues.forEach(item => {
291 item.widget.parent = null;
292 });
293 changes.newValues.forEach((item, index) => {
294 toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
295 });
296 break;
297 case 'remove':
298 changes.oldValues.forEach(item => {
299 item.widget.parent = null;
300 });
301 break;
302 case 'set':
303 changes.oldValues.forEach(item => {
304 item.widget.parent = null;
305 });
306 changes.newValues.forEach((item, index) => {
307 const existingIndex = findIndex(toolbar_.names(), name => item.name === name);
308 if (existingIndex >= 0) {
309 Array.from(toolbar_.children())[existingIndex].parent = null;
310 }
311 toolbar_.insertItem(changes.newIndex + index, item.name, item.widget);
312 });
313 break;
314 }
315 };
316 updateToolbar(items, {
317 newIndex: 0,
318 newValues: Array.from(items),
319 oldIndex: 0,
320 oldValues: [],
321 type: 'add'
322 });
323 items.changed.connect(updateToolbar);
324 widget.disposed.connect(() => {
325 items.changed.disconnect(updateToolbar);
326 });
327 }
328}
329//# sourceMappingURL=factory.js.map
\No newline at end of file