UNPKG

24.6 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Collapse, Styling } from '@jupyterlab/apputils';
4import { CodeEditor, CodeEditorWrapper, JSONEditor } from '@jupyterlab/codeeditor';
5import { ObservableJSON } from '@jupyterlab/observables';
6import { nullTranslator } from '@jupyterlab/translation';
7import { ArrayExt, chain, each } from '@lumino/algorithm';
8import { ConflatableMessage, MessageLoop } from '@lumino/messaging';
9import { h, VirtualDOM } from '@lumino/virtualdom';
10import { PanelLayout, Widget } from '@lumino/widgets';
11class RankedPanel extends Widget {
12 constructor() {
13 super();
14 this._items = [];
15 this.layout = new PanelLayout();
16 this.addClass('jp-RankedPanel');
17 }
18 addWidget(widget, rank) {
19 const rankItem = { widget, rank };
20 const index = ArrayExt.upperBound(this._items, rankItem, Private.itemCmp);
21 ArrayExt.insert(this._items, index, rankItem);
22 const layout = this.layout;
23 layout.insertWidget(index, widget);
24 }
25 /**
26 * Handle the removal of a child
27 *
28 */
29 onChildRemoved(msg) {
30 const index = ArrayExt.findFirstIndex(this._items, item => item.widget === msg.child);
31 if (index !== -1) {
32 ArrayExt.removeAt(this._items, index);
33 }
34 }
35}
36/**
37 * A widget that provides metadata tools.
38 */
39export class NotebookTools extends Widget {
40 /**
41 * Construct a new NotebookTools object.
42 */
43 constructor(options) {
44 super();
45 this.addClass('jp-NotebookTools');
46 this.translator = options.translator || nullTranslator;
47 this._trans = this.translator.load('jupyterlab');
48 this._commonTools = new RankedPanel();
49 this._advancedTools = new RankedPanel();
50 this._advancedTools.title.label = this._trans.__('Advanced Tools');
51 const layout = (this.layout = new PanelLayout());
52 layout.addWidget(this._commonTools);
53 layout.addWidget(new Collapse({ widget: this._advancedTools }));
54 this._tracker = options.tracker;
55 this._tracker.currentChanged.connect(this._onActiveNotebookPanelChanged, this);
56 this._tracker.activeCellChanged.connect(this._onActiveCellChanged, this);
57 this._tracker.selectionChanged.connect(this._onSelectionChanged, this);
58 this._onActiveNotebookPanelChanged();
59 this._onActiveCellChanged();
60 this._onSelectionChanged();
61 }
62 /**
63 * The active cell widget.
64 */
65 get activeCell() {
66 return this._tracker.activeCell;
67 }
68 /**
69 * The currently selected cells.
70 */
71 get selectedCells() {
72 const panel = this._tracker.currentWidget;
73 if (!panel) {
74 return [];
75 }
76 const notebook = panel.content;
77 return notebook.widgets.filter(cell => notebook.isSelectedOrActive(cell));
78 }
79 /**
80 * The current notebook.
81 */
82 get activeNotebookPanel() {
83 return this._tracker.currentWidget;
84 }
85 /**
86 * Add a cell tool item.
87 */
88 addItem(options) {
89 var _a;
90 const tool = options.tool;
91 const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : 100;
92 let section;
93 if (options.section === 'advanced') {
94 section = this._advancedTools;
95 }
96 else {
97 section = this._commonTools;
98 }
99 tool.addClass('jp-NotebookTools-tool');
100 section.addWidget(tool, rank);
101 // TODO: perhaps the necessary notebookTools functionality should be
102 // consolidated into a single object, rather than a broad reference to this.
103 tool.notebookTools = this;
104 // Trigger the tool to update its active notebook and cell.
105 MessageLoop.sendMessage(tool, NotebookTools.ActiveNotebookPanelMessage);
106 MessageLoop.sendMessage(tool, NotebookTools.ActiveCellMessage);
107 }
108 /**
109 * Handle a change to the notebook panel.
110 */
111 _onActiveNotebookPanelChanged() {
112 if (this._prevActiveNotebookModel &&
113 !this._prevActiveNotebookModel.isDisposed) {
114 this._prevActiveNotebookModel.metadata.changed.disconnect(this._onActiveNotebookPanelMetadataChanged, this);
115 }
116 const activeNBModel = this.activeNotebookPanel && this.activeNotebookPanel.content
117 ? this.activeNotebookPanel.content.model
118 : null;
119 this._prevActiveNotebookModel = activeNBModel;
120 if (activeNBModel) {
121 activeNBModel.metadata.changed.connect(this._onActiveNotebookPanelMetadataChanged, this);
122 }
123 each(this._toolChildren(), widget => {
124 MessageLoop.sendMessage(widget, NotebookTools.ActiveNotebookPanelMessage);
125 });
126 }
127 /**
128 * Handle a change to the active cell.
129 */
130 _onActiveCellChanged() {
131 if (this._prevActiveCell && !this._prevActiveCell.isDisposed) {
132 this._prevActiveCell.metadata.changed.disconnect(this._onActiveCellMetadataChanged, this);
133 }
134 const activeCell = this.activeCell ? this.activeCell.model : null;
135 this._prevActiveCell = activeCell;
136 if (activeCell) {
137 activeCell.metadata.changed.connect(this._onActiveCellMetadataChanged, this);
138 }
139 each(this._toolChildren(), widget => {
140 MessageLoop.sendMessage(widget, NotebookTools.ActiveCellMessage);
141 });
142 }
143 /**
144 * Handle a change in the selection.
145 */
146 _onSelectionChanged() {
147 each(this._toolChildren(), widget => {
148 MessageLoop.sendMessage(widget, NotebookTools.SelectionMessage);
149 });
150 }
151 /**
152 * Handle a change in the active cell metadata.
153 */
154 _onActiveNotebookPanelMetadataChanged(sender, args) {
155 const message = new ObservableJSON.ChangeMessage('activenotebookpanel-metadata-changed', args);
156 each(this._toolChildren(), widget => {
157 MessageLoop.sendMessage(widget, message);
158 });
159 }
160 /**
161 * Handle a change in the notebook model metadata.
162 */
163 _onActiveCellMetadataChanged(sender, args) {
164 const message = new ObservableJSON.ChangeMessage('activecell-metadata-changed', args);
165 each(this._toolChildren(), widget => {
166 MessageLoop.sendMessage(widget, message);
167 });
168 }
169 _toolChildren() {
170 return chain(this._commonTools.children(), this._advancedTools.children());
171 }
172}
173/**
174 * The namespace for NotebookTools class statics.
175 */
176(function (NotebookTools) {
177 /**
178 * A singleton conflatable `'activenotebookpanel-changed'` message.
179 */
180 NotebookTools.ActiveNotebookPanelMessage = new ConflatableMessage('activenotebookpanel-changed');
181 /**
182 * A singleton conflatable `'activecell-changed'` message.
183 */
184 NotebookTools.ActiveCellMessage = new ConflatableMessage('activecell-changed');
185 /**
186 * A singleton conflatable `'selection-changed'` message.
187 */
188 NotebookTools.SelectionMessage = new ConflatableMessage('selection-changed');
189 /**
190 * The base notebook tool, meant to be subclassed.
191 */
192 class Tool extends Widget {
193 dispose() {
194 super.dispose();
195 if (this.notebookTools) {
196 this.notebookTools = null;
197 }
198 }
199 /**
200 * Process a message sent to the widget.
201 *
202 * @param msg - The message sent to the widget.
203 */
204 processMessage(msg) {
205 super.processMessage(msg);
206 switch (msg.type) {
207 case 'activenotebookpanel-changed':
208 this.onActiveNotebookPanelChanged(msg);
209 break;
210 case 'activecell-changed':
211 this.onActiveCellChanged(msg);
212 break;
213 case 'selection-changed':
214 this.onSelectionChanged(msg);
215 break;
216 case 'activecell-metadata-changed':
217 this.onActiveCellMetadataChanged(msg);
218 break;
219 case 'activenotebookpanel-metadata-changed':
220 this.onActiveNotebookPanelMetadataChanged(msg);
221 break;
222 default:
223 break;
224 }
225 }
226 /**
227 * Handle a change to the notebook panel.
228 *
229 * #### Notes
230 * The default implementation is a no-op.
231 */
232 onActiveNotebookPanelChanged(msg) {
233 /* no-op */
234 }
235 /**
236 * Handle a change to the active cell.
237 *
238 * #### Notes
239 * The default implementation is a no-op.
240 */
241 onActiveCellChanged(msg) {
242 /* no-op */
243 }
244 /**
245 * Handle a change to the selection.
246 *
247 * #### Notes
248 * The default implementation is a no-op.
249 */
250 onSelectionChanged(msg) {
251 /* no-op */
252 }
253 /**
254 * Handle a change to the metadata of the active cell.
255 *
256 * #### Notes
257 * The default implementation is a no-op.
258 */
259 onActiveCellMetadataChanged(msg) {
260 /* no-op */
261 }
262 /**
263 * Handle a change to the metadata of the active cell.
264 *
265 * #### Notes
266 * The default implementation is a no-op.
267 */
268 onActiveNotebookPanelMetadataChanged(msg) {
269 /* no-op */
270 }
271 }
272 NotebookTools.Tool = Tool;
273 /**
274 * A cell tool displaying the active cell contents.
275 */
276 class ActiveCellTool extends Tool {
277 /**
278 * Construct a new active cell tool.
279 */
280 constructor() {
281 super();
282 this._model = new CodeEditor.Model();
283 this.addClass('jp-ActiveCellTool');
284 this.addClass('jp-InputArea');
285 this.layout = new PanelLayout();
286 }
287 /**
288 * Dispose of the resources used by the tool.
289 */
290 dispose() {
291 if (this._model === null) {
292 return;
293 }
294 this._model.dispose();
295 this._model = null;
296 super.dispose();
297 }
298 /**
299 * Handle a change to the active cell.
300 */
301 onActiveCellChanged() {
302 const activeCell = this.notebookTools.activeCell;
303 const layout = this.layout;
304 const count = layout.widgets.length;
305 for (let i = 0; i < count; i++) {
306 layout.widgets[0].dispose();
307 }
308 if (this._cellModel && !this._cellModel.isDisposed) {
309 this._cellModel.value.changed.disconnect(this._onValueChanged, this);
310 this._cellModel.mimeTypeChanged.disconnect(this._onMimeTypeChanged, this);
311 }
312 if (!activeCell) {
313 const cell = new Widget();
314 cell.addClass('jp-InputArea-editor');
315 cell.addClass('jp-InputArea-editor');
316 layout.addWidget(cell);
317 this._cellModel = null;
318 return;
319 }
320 const promptNode = activeCell.promptNode
321 ? activeCell.promptNode.cloneNode(true)
322 : undefined;
323 const prompt = new Widget({ node: promptNode });
324 const factory = activeCell.contentFactory.editorFactory;
325 const cellModel = (this._cellModel = activeCell.model);
326 cellModel.value.changed.connect(this._onValueChanged, this);
327 cellModel.mimeTypeChanged.connect(this._onMimeTypeChanged, this);
328 this._model.value.text = cellModel.value.text.split('\n')[0];
329 this._model.mimeType = cellModel.mimeType;
330 const model = this._model;
331 const editorWidget = new CodeEditorWrapper({ model, factory });
332 editorWidget.addClass('jp-InputArea-editor');
333 editorWidget.addClass('jp-InputArea-editor');
334 editorWidget.editor.setOption('readOnly', true);
335 layout.addWidget(prompt);
336 layout.addWidget(editorWidget);
337 }
338 /**
339 * Handle a change to the notebook panel.
340 *
341 * #### Notes
342 * The default implementation is a no-op.
343 */
344 onActiveNotebookPanelChanged(msg) {
345 if (!this.notebookTools.activeNotebookPanel) {
346 // Force cleaning up the signal
347 void this.onActiveCellChanged();
348 }
349 }
350 /**
351 * Handle a change to the current editor value.
352 */
353 _onValueChanged() {
354 this._model.value.text = this._cellModel.value.text.split('\n')[0];
355 }
356 /**
357 * Handle a change to the current editor mimetype.
358 */
359 _onMimeTypeChanged() {
360 this._model.mimeType = this._cellModel.mimeType;
361 }
362 }
363 NotebookTools.ActiveCellTool = ActiveCellTool;
364 /**
365 * A raw metadata editor.
366 */
367 class MetadataEditorTool extends Tool {
368 /**
369 * Construct a new raw metadata tool.
370 */
371 constructor(options) {
372 super();
373 const { editorFactory } = options;
374 this.addClass('jp-MetadataEditorTool');
375 const layout = (this.layout = new PanelLayout());
376 this._editorFactory = editorFactory;
377 this._editorLabel = options.label || 'Edit Metadata';
378 this.createEditor();
379 const titleNode = new Widget({ node: document.createElement('label') });
380 titleNode.node.textContent = options.label || 'Edit Metadata';
381 layout.addWidget(titleNode);
382 layout.addWidget(this.editor);
383 }
384 /**
385 * The editor used by the tool.
386 */
387 get editor() {
388 return this._editor;
389 }
390 /**
391 * Handle a change to the notebook.
392 */
393 onActiveNotebookPanelChanged(msg) {
394 this.editor.dispose();
395 if (this.notebookTools.activeNotebookPanel) {
396 this.createEditor();
397 }
398 }
399 createEditor() {
400 this._editor = new JSONEditor({
401 editorFactory: this._editorFactory
402 });
403 this.editor.title.label = this._editorLabel;
404 this.layout.addWidget(this.editor);
405 }
406 }
407 NotebookTools.MetadataEditorTool = MetadataEditorTool;
408 /**
409 * A notebook metadata editor
410 */
411 class NotebookMetadataEditorTool extends MetadataEditorTool {
412 constructor(options) {
413 const translator = options.translator || nullTranslator;
414 const trans = translator.load('jupyterlab');
415 options.label = options.label || trans.__('Notebook Metadata');
416 super(options);
417 }
418 /**
419 * Handle a change to the notebook.
420 */
421 onActiveNotebookPanelChanged(msg) {
422 super.onActiveNotebookPanelChanged(msg);
423 if (this.notebookTools.activeNotebookPanel) {
424 this._update();
425 }
426 }
427 /**
428 * Handle a change to the notebook metadata.
429 */
430 onActiveNotebookPanelMetadataChanged(msg) {
431 this._update();
432 }
433 _update() {
434 var _a, _b;
435 const nb = this.notebookTools.activeNotebookPanel &&
436 this.notebookTools.activeNotebookPanel.content;
437 this.editor.source = (_b = (_a = nb === null || nb === void 0 ? void 0 : nb.model) === null || _a === void 0 ? void 0 : _a.metadata) !== null && _b !== void 0 ? _b : null;
438 }
439 }
440 NotebookTools.NotebookMetadataEditorTool = NotebookMetadataEditorTool;
441 /**
442 * A cell metadata editor
443 */
444 class CellMetadataEditorTool extends MetadataEditorTool {
445 constructor(options) {
446 const translator = options.translator || nullTranslator;
447 const trans = translator.load('jupyterlab');
448 options.label = options.label || trans.__('Cell Metadata');
449 super(options);
450 }
451 /**
452 * Handle a change to the active cell.
453 */
454 onActiveCellChanged(msg) {
455 this.editor.dispose();
456 if (this.notebookTools.activeCell) {
457 this.createEditor();
458 this._update();
459 }
460 }
461 /**
462 * Handle a change to the active cell metadata.
463 */
464 onActiveCellMetadataChanged(msg) {
465 this._update();
466 }
467 _update() {
468 const cell = this.notebookTools.activeCell;
469 this.editor.source = cell ? cell.model.metadata : null;
470 }
471 }
472 NotebookTools.CellMetadataEditorTool = CellMetadataEditorTool;
473 /**
474 * A cell tool that provides a selection for a given metadata key.
475 */
476 class KeySelector extends Tool {
477 /**
478 * Construct a new KeySelector.
479 */
480 constructor(options) {
481 // TODO: use react
482 super({ node: Private.createSelectorNode(options) });
483 /**
484 * Get the value for the data.
485 */
486 this._getValue = (cell) => {
487 let value = cell.model.metadata.get(this.key);
488 if (value === undefined) {
489 value = this._default;
490 }
491 return value;
492 };
493 /**
494 * Set the value for the data.
495 */
496 this._setValue = (cell, value) => {
497 if (value === this._default) {
498 cell.model.metadata.delete(this.key);
499 }
500 else {
501 cell.model.metadata.set(this.key, value);
502 }
503 };
504 this._changeGuard = false;
505 this.addClass('jp-KeySelector');
506 this.key = options.key;
507 this._default = options.default;
508 this._validCellTypes = options.validCellTypes || [];
509 this._getter = options.getter || this._getValue;
510 this._setter = options.setter || this._setValue;
511 }
512 /**
513 * The select node for the widget.
514 */
515 get selectNode() {
516 return this.node.getElementsByTagName('select')[0];
517 }
518 /**
519 * Handle the DOM events for the widget.
520 *
521 * @param event - The DOM event sent to the widget.
522 *
523 * #### Notes
524 * This method implements the DOM `EventListener` interface and is
525 * called in response to events on the notebook panel's node. It should
526 * not be called directly by user code.
527 */
528 handleEvent(event) {
529 switch (event.type) {
530 case 'change':
531 this.onValueChanged();
532 break;
533 default:
534 break;
535 }
536 }
537 /**
538 * Handle `after-attach` messages for the widget.
539 */
540 onAfterAttach(msg) {
541 const node = this.selectNode;
542 node.addEventListener('change', this);
543 }
544 /**
545 * Handle `before-detach` messages for the widget.
546 */
547 onBeforeDetach(msg) {
548 const node = this.selectNode;
549 node.removeEventListener('change', this);
550 }
551 /**
552 * Handle a change to the active cell.
553 */
554 onActiveCellChanged(msg) {
555 const select = this.selectNode;
556 const activeCell = this.notebookTools.activeCell;
557 if (!activeCell) {
558 select.disabled = true;
559 select.value = '';
560 return;
561 }
562 const cellType = activeCell.model.type;
563 if (this._validCellTypes.length &&
564 this._validCellTypes.indexOf(cellType) === -1) {
565 select.value = '';
566 select.disabled = true;
567 return;
568 }
569 select.disabled = false;
570 this._changeGuard = true;
571 const getter = this._getter;
572 select.value = JSON.stringify(getter(activeCell));
573 this._changeGuard = false;
574 }
575 /**
576 * Handle a change to the metadata of the active cell.
577 */
578 onActiveCellMetadataChanged(msg) {
579 if (this._changeGuard) {
580 return;
581 }
582 const select = this.selectNode;
583 const cell = this.notebookTools.activeCell;
584 if (msg.args.key === this.key && cell) {
585 this._changeGuard = true;
586 const getter = this._getter;
587 select.value = JSON.stringify(getter(cell));
588 this._changeGuard = false;
589 }
590 }
591 /**
592 * Handle a change to the value.
593 */
594 onValueChanged() {
595 const activeCell = this.notebookTools.activeCell;
596 if (!activeCell || this._changeGuard) {
597 return;
598 }
599 this._changeGuard = true;
600 const select = this.selectNode;
601 const setter = this._setter;
602 setter(activeCell, JSON.parse(select.value));
603 this._changeGuard = false;
604 }
605 }
606 NotebookTools.KeySelector = KeySelector;
607 /**
608 * Create a slideshow selector.
609 */
610 function createSlideShowSelector(translator) {
611 translator = translator || nullTranslator;
612 const trans = translator.load('jupyterlab');
613 trans.__('');
614 const options = {
615 key: 'slideshow',
616 title: trans.__('Slide Type'),
617 optionValueArray: [
618 ['-', null],
619 [trans.__('Slide'), 'slide'],
620 [trans.__('Sub-Slide'), 'subslide'],
621 [trans.__('Fragment'), 'fragment'],
622 [trans.__('Skip'), 'skip'],
623 [trans.__('Notes'), 'notes']
624 ],
625 getter: cell => {
626 const value = cell.model.metadata.get('slideshow');
627 return value && value['slide_type'];
628 },
629 setter: (cell, value) => {
630 let data = cell.model.metadata.get('slideshow') || Object.create(null);
631 if (value === null) {
632 // Make a shallow copy so we aren't modifying the original metadata.
633 data = Object.assign({}, data);
634 delete data.slide_type;
635 }
636 else {
637 data = Object.assign(Object.assign({}, data), { slide_type: value });
638 }
639 if (Object.keys(data).length > 0) {
640 cell.model.metadata.set('slideshow', data);
641 }
642 else {
643 cell.model.metadata.delete('slideshow');
644 }
645 }
646 };
647 return new KeySelector(options);
648 }
649 NotebookTools.createSlideShowSelector = createSlideShowSelector;
650 /**
651 * Create an nbconvert selector.
652 */
653 function createNBConvertSelector(optionValueArray, translator) {
654 translator = translator || nullTranslator;
655 const trans = translator.load('jupyterlab');
656 return new KeySelector({
657 key: 'raw_mimetype',
658 title: trans.__('Raw NBConvert Format'),
659 optionValueArray: optionValueArray,
660 validCellTypes: ['raw']
661 });
662 }
663 NotebookTools.createNBConvertSelector = createNBConvertSelector;
664})(NotebookTools || (NotebookTools = {}));
665/**
666 * A namespace for private data.
667 */
668var Private;
669(function (Private) {
670 /**
671 * A comparator function for widget rank items.
672 */
673 function itemCmp(first, second) {
674 return first.rank - second.rank;
675 }
676 Private.itemCmp = itemCmp;
677 /**
678 * Create the node for a KeySelector.
679 */
680 function createSelectorNode(options) {
681 const name = options.key;
682 const title = options.title || name[0].toLocaleUpperCase() + name.slice(1);
683 const optionNodes = [];
684 let value;
685 let option;
686 each(options.optionValueArray, item => {
687 option = item[0];
688 value = JSON.stringify(item[1]);
689 optionNodes.push(h.option({ value }, option));
690 });
691 const node = VirtualDOM.realize(h.div({}, h.label(title, h.select({}, optionNodes))));
692 Styling.styleNode(node);
693 return node;
694 }
695 Private.createSelectorNode = createSelectorNode;
696})(Private || (Private = {}));
697//# sourceMappingURL=notebooktools.js.map
\No newline at end of file