UNPKG

73.2 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Cell, CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells';
4import { CodeEditor } from '@jupyterlab/codeeditor';
5import { ArrayExt, each, findIndex } from '@lumino/algorithm';
6import { MimeData } from '@lumino/coreutils';
7import { ElementExt } from '@lumino/domutils';
8import { Drag } from '@lumino/dragdrop';
9import { AttachedProperty } from '@lumino/properties';
10import { Signal } from '@lumino/signaling';
11import { h, VirtualDOM } from '@lumino/virtualdom';
12import { PanelLayout, Widget } from '@lumino/widgets';
13import { NotebookActions } from './actions';
14/**
15 * The data attribute added to a widget that has an active kernel.
16 */
17const KERNEL_USER = 'jpKernelUser';
18/**
19 * The data attribute added to a widget that can run code.
20 */
21const CODE_RUNNER = 'jpCodeRunner';
22/**
23 * The data attribute added to a widget that can undo.
24 */
25const UNDOER = 'jpUndoer';
26/**
27 * The class name added to notebook widgets.
28 */
29const NB_CLASS = 'jp-Notebook';
30/**
31 * The class name added to notebook widget cells.
32 */
33const NB_CELL_CLASS = 'jp-Notebook-cell';
34/**
35 * The class name added to a notebook in edit mode.
36 */
37const EDIT_CLASS = 'jp-mod-editMode';
38/**
39 * The class name added to a notebook in command mode.
40 */
41const COMMAND_CLASS = 'jp-mod-commandMode';
42/**
43 * The class name added to the active cell.
44 */
45const ACTIVE_CLASS = 'jp-mod-active';
46/**
47 * The class name added to selected cells.
48 */
49const SELECTED_CLASS = 'jp-mod-selected';
50/**
51 * The class name added to an active cell when there are other selected cells.
52 */
53const OTHER_SELECTED_CLASS = 'jp-mod-multiSelected';
54/**
55 * The class name added to unconfined images.
56 */
57const UNCONFINED_CLASS = 'jp-mod-unconfined';
58/**
59 * The class name added to a drop target.
60 */
61const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
62/**
63 * The class name added to a drop source.
64 */
65const DROP_SOURCE_CLASS = 'jp-mod-dropSource';
66/**
67 * The class name added to drag images.
68 */
69const DRAG_IMAGE_CLASS = 'jp-dragImage';
70/**
71 * The class name added to singular drag images
72 */
73const SINGLE_DRAG_IMAGE_CLASS = 'jp-dragImage-singlePrompt';
74/**
75 * The class name added to the drag image cell content.
76 */
77const CELL_DRAG_CONTENT_CLASS = 'jp-dragImage-content';
78/**
79 * The class name added to the drag image cell content.
80 */
81const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt';
82/**
83 * The class name added to the drag image cell content.
84 */
85const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack';
86/**
87 * The mimetype used for Jupyter cell data.
88 */
89const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
90/**
91 * The threshold in pixels to start a drag event.
92 */
93const DRAG_THRESHOLD = 5;
94/**
95 * The class attached to the heading collapser button
96 */
97const HEADING_COLLAPSER_CLASS = 'jp-collapseHeadingButton';
98const SIDE_BY_SIDE_CLASS = 'jp-mod-sideBySide';
99if (window.requestIdleCallback === undefined) {
100 // On Safari, requestIdleCallback is not available, so we use replacement functions for `idleCallbacks`
101 // See: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#falling_back_to_settimeout
102 window.requestIdleCallback = function (handler) {
103 let startTime = Date.now();
104 return setTimeout(function () {
105 handler({
106 didTimeout: false,
107 timeRemaining: function () {
108 return Math.max(0, 50.0 - (Date.now() - startTime));
109 }
110 });
111 }, 1);
112 };
113 window.cancelIdleCallback = function (id) {
114 clearTimeout(id);
115 };
116}
117/**
118 * A widget which renders static non-interactive notebooks.
119 *
120 * #### Notes
121 * The widget model must be set separately and can be changed
122 * at any time. Consumers of the widget must account for a
123 * `null` model, and may want to listen to the `modelChanged`
124 * signal.
125 */
126export class StaticNotebook extends Widget {
127 /**
128 * Construct a notebook widget.
129 */
130 constructor(options) {
131 var _a;
132 super();
133 this._editorConfig = StaticNotebook.defaultEditorConfig;
134 this._notebookConfig = StaticNotebook.defaultNotebookConfig;
135 this._mimetype = 'text/plain';
136 this._model = null;
137 this._modelChanged = new Signal(this);
138 this._modelContentChanged = new Signal(this);
139 this._fullyRendered = new Signal(this);
140 this._placeholderCellRendered = new Signal(this);
141 this._renderedCellsCount = 0;
142 this.addClass(NB_CLASS);
143 this.node.dataset[KERNEL_USER] = 'true';
144 this.node.dataset[UNDOER] = 'true';
145 this.node.dataset[CODE_RUNNER] = 'true';
146 this.rendermime = options.rendermime;
147 this.layout = new Private.NotebookPanelLayout();
148 this.contentFactory =
149 options.contentFactory || StaticNotebook.defaultContentFactory;
150 this.editorConfig =
151 options.editorConfig || StaticNotebook.defaultEditorConfig;
152 this.notebookConfig =
153 options.notebookConfig || StaticNotebook.defaultNotebookConfig;
154 this._mimetypeService = options.mimeTypeService;
155 this.renderingLayout = (_a = options.notebookConfig) === null || _a === void 0 ? void 0 : _a.renderingLayout;
156 // Section for the virtual-notebook behavior.
157 this._toRenderMap = new Map();
158 this._cellsArray = new Array();
159 if ('IntersectionObserver' in window) {
160 this._observer = new IntersectionObserver((entries, observer) => {
161 entries.forEach(o => {
162 if (o.isIntersecting) {
163 observer.unobserve(o.target);
164 const ci = this._toRenderMap.get(o.target.id);
165 if (ci) {
166 const { cell, index } = ci;
167 this._renderPlaceholderCell(cell, index);
168 }
169 }
170 });
171 }, {
172 root: this.node,
173 threshold: 1,
174 rootMargin: `${this.notebookConfig.observedTopMargin} 0px ${this.notebookConfig.observedBottomMargin} 0px`
175 });
176 }
177 }
178 /**
179 * A signal emitted when the notebook is fully rendered.
180 */
181 get fullyRendered() {
182 return this._fullyRendered;
183 }
184 /**
185 * A signal emitted when the a placeholder cell is rendered.
186 */
187 get placeholderCellRendered() {
188 return this._placeholderCellRendered;
189 }
190 /**
191 * A signal emitted when the model of the notebook changes.
192 */
193 get modelChanged() {
194 return this._modelChanged;
195 }
196 /**
197 * A signal emitted when the model content changes.
198 *
199 * #### Notes
200 * This is a convenience signal that follows the current model.
201 */
202 get modelContentChanged() {
203 return this._modelContentChanged;
204 }
205 /**
206 * The model for the widget.
207 */
208 get model() {
209 return this._model;
210 }
211 set model(newValue) {
212 newValue = newValue || null;
213 if (this._model === newValue) {
214 return;
215 }
216 const oldValue = this._model;
217 this._model = newValue;
218 if (oldValue && oldValue.modelDB.isCollaborative) {
219 void oldValue.modelDB.connected.then(() => {
220 oldValue.modelDB.collaborators.changed.disconnect(this._onCollaboratorsChanged, this);
221 });
222 }
223 if (newValue && newValue.modelDB.isCollaborative) {
224 void newValue.modelDB.connected.then(() => {
225 newValue.modelDB.collaborators.changed.connect(this._onCollaboratorsChanged, this);
226 });
227 }
228 // Trigger private, protected, and public changes.
229 this._onModelChanged(oldValue, newValue);
230 this.onModelChanged(oldValue, newValue);
231 this._modelChanged.emit(void 0);
232 }
233 /**
234 * Get the mimetype for code cells.
235 */
236 get codeMimetype() {
237 return this._mimetype;
238 }
239 /**
240 * A read-only sequence of the widgets in the notebook.
241 */
242 get widgets() {
243 return this.layout.widgets;
244 }
245 /**
246 * A configuration object for cell editor settings.
247 */
248 get editorConfig() {
249 return this._editorConfig;
250 }
251 set editorConfig(value) {
252 this._editorConfig = value;
253 this._updateEditorConfig();
254 }
255 /**
256 * A configuration object for notebook settings.
257 */
258 get notebookConfig() {
259 return this._notebookConfig;
260 }
261 set notebookConfig(value) {
262 this._notebookConfig = value;
263 this._updateNotebookConfig();
264 }
265 get renderingLayout() {
266 return this._renderingLayout;
267 }
268 set renderingLayout(value) {
269 this._renderingLayout = value;
270 if (this._renderingLayout === 'side-by-side') {
271 this.node.classList.add(SIDE_BY_SIDE_CLASS);
272 }
273 else {
274 this.node.classList.remove(SIDE_BY_SIDE_CLASS);
275 }
276 }
277 /**
278 * Dispose of the resources held by the widget.
279 */
280 dispose() {
281 // Do nothing if already disposed.
282 if (this.isDisposed) {
283 return;
284 }
285 this._model = null;
286 super.dispose();
287 }
288 /**
289 * Handle a new model.
290 *
291 * #### Notes
292 * This method is called after the model change has been handled
293 * internally and before the `modelChanged` signal is emitted.
294 * The default implementation is a no-op.
295 */
296 onModelChanged(oldValue, newValue) {
297 // No-op.
298 }
299 /**
300 * Handle changes to the notebook model content.
301 *
302 * #### Notes
303 * The default implementation emits the `modelContentChanged` signal.
304 */
305 onModelContentChanged(model, args) {
306 this._modelContentChanged.emit(void 0);
307 }
308 /**
309 * Handle changes to the notebook model metadata.
310 *
311 * #### Notes
312 * The default implementation updates the mimetypes of the code cells
313 * when the `language_info` metadata changes.
314 */
315 onMetadataChanged(sender, args) {
316 switch (args.key) {
317 case 'language_info':
318 this._updateMimetype();
319 break;
320 default:
321 break;
322 }
323 }
324 /**
325 * Handle a cell being inserted.
326 *
327 * The default implementation is a no-op
328 */
329 onCellInserted(index, cell) {
330 // This is a no-op.
331 }
332 /**
333 * Handle a cell being moved.
334 *
335 * The default implementation is a no-op
336 */
337 onCellMoved(fromIndex, toIndex) {
338 // This is a no-op.
339 }
340 /**
341 * Handle a cell being removed.
342 *
343 * The default implementation is a no-op
344 */
345 onCellRemoved(index, cell) {
346 // This is a no-op.
347 }
348 /**
349 * Handle a new model on the widget.
350 */
351 _onModelChanged(oldValue, newValue) {
352 const layout = this.layout;
353 if (oldValue) {
354 oldValue.cells.changed.disconnect(this._onCellsChanged, this);
355 oldValue.metadata.changed.disconnect(this.onMetadataChanged, this);
356 oldValue.contentChanged.disconnect(this.onModelContentChanged, this);
357 // TODO: reuse existing cell widgets if possible. Remember to initially
358 // clear the history of each cell if we do this.
359 while (layout.widgets.length) {
360 this._removeCell(0);
361 }
362 }
363 if (!newValue) {
364 this._mimetype = 'text/plain';
365 return;
366 }
367 this._updateMimetype();
368 const cells = newValue.cells;
369 if (!cells.length && newValue.isInitialized) {
370 cells.push(newValue.contentFactory.createCell(this.notebookConfig.defaultCell, {}));
371 }
372 each(cells, (cell, i) => {
373 this._insertCell(i, cell, 'set');
374 });
375 cells.changed.connect(this._onCellsChanged, this);
376 newValue.contentChanged.connect(this.onModelContentChanged, this);
377 newValue.metadata.changed.connect(this.onMetadataChanged, this);
378 }
379 /**
380 * Handle a change cells event.
381 */
382 _onCellsChanged(sender, args) {
383 let index = 0;
384 switch (args.type) {
385 case 'add':
386 index = args.newIndex;
387 // eslint-disable-next-line no-case-declarations
388 const insertType = args.oldIndex == -1 ? 'push' : 'insert';
389 each(args.newValues, value => {
390 this._insertCell(index++, value, insertType);
391 });
392 break;
393 case 'move':
394 this._moveCell(args.oldIndex, args.newIndex);
395 break;
396 case 'remove':
397 each(args.oldValues, value => {
398 this._removeCell(args.oldIndex);
399 });
400 // Add default cell if there are no cells remaining.
401 if (!sender.length) {
402 const model = this.model;
403 // Add the cell in a new context to avoid triggering another
404 // cell changed event during the handling of this signal.
405 requestAnimationFrame(() => {
406 if (model && !model.isDisposed && !model.cells.length) {
407 model.cells.push(model.contentFactory.createCell(this.notebookConfig.defaultCell, {}));
408 }
409 });
410 }
411 break;
412 case 'set':
413 // TODO: reuse existing widgets if possible.
414 index = args.newIndex;
415 each(args.newValues, value => {
416 // Note: this ordering (insert then remove)
417 // is important for getting the active cell
418 // index for the editable notebook correct.
419 this._insertCell(index, value, 'set');
420 this._removeCell(index + 1);
421 index++;
422 });
423 break;
424 default:
425 return;
426 }
427 }
428 /**
429 * Create a cell widget and insert into the notebook.
430 */
431 _insertCell(index, cell, insertType) {
432 let widget;
433 switch (cell.type) {
434 case 'code':
435 widget = this._createCodeCell(cell);
436 widget.model.mimeType = this._mimetype;
437 break;
438 case 'markdown':
439 widget = this._createMarkdownCell(cell);
440 if (cell.value.text === '') {
441 widget.rendered = false;
442 }
443 break;
444 default:
445 widget = this._createRawCell(cell);
446 }
447 widget.addClass(NB_CELL_CLASS);
448 const layout = this.layout;
449 this._cellsArray.push(widget);
450 if (this._observer &&
451 insertType === 'push' &&
452 this._renderedCellsCount >=
453 this.notebookConfig.numberCellsToRenderDirectly &&
454 cell.type !== 'markdown') {
455 // We have an observer and we are have been asked to push (not to insert).
456 // and we are above the number of cells to render directly, then
457 // we will add a placeholder and let the intersection observer or the
458 // idle browser render those placeholder cells.
459 this._toRenderMap.set(widget.model.id, { index: index, cell: widget });
460 const placeholder = this._createPlaceholderCell(cell, index);
461 placeholder.node.id = widget.model.id;
462 layout.insertWidget(index, placeholder);
463 this.onCellInserted(index, placeholder);
464 this._fullyRendered.emit(false);
465 this._observer.observe(placeholder.node);
466 }
467 else {
468 // We have no intersection observer, or we insert, or we are below
469 // the number of cells to render directly, so we render directly.
470 layout.insertWidget(index, widget);
471 this._incrementRenderedCount();
472 this.onCellInserted(index, widget);
473 }
474 if (this._observer && this.notebookConfig.renderCellOnIdle) {
475 const renderPlaceholderCells = this._renderPlaceholderCells.bind(this);
476 window.requestIdleCallback(renderPlaceholderCells, {
477 timeout: 1000
478 });
479 }
480 }
481 _renderPlaceholderCells(deadline) {
482 if (this._renderedCellsCount < this._cellsArray.length &&
483 this._renderedCellsCount >=
484 this.notebookConfig.numberCellsToRenderDirectly) {
485 const ci = this._toRenderMap.entries().next();
486 this._renderPlaceholderCell(ci.value[1].cell, ci.value[1].index);
487 }
488 }
489 _renderPlaceholderCell(cell, index) {
490 const pl = this.layout;
491 pl.removeWidgetAt(index);
492 pl.insertWidget(index, cell);
493 this._toRenderMap.delete(cell.model.id);
494 this._incrementRenderedCount();
495 this.onCellInserted(index, cell);
496 this._placeholderCellRendered.emit(cell);
497 }
498 /**
499 * Create a code cell widget from a code cell model.
500 */
501 _createCodeCell(model) {
502 const rendermime = this.rendermime;
503 const contentFactory = this.contentFactory;
504 const editorConfig = this.editorConfig.code;
505 const options = {
506 editorConfig,
507 model,
508 rendermime,
509 contentFactory,
510 updateEditorOnShow: false,
511 placeholder: false,
512 maxNumberOutputs: this.notebookConfig.maxNumberOutputs
513 };
514 const cell = this.contentFactory.createCodeCell(options, this);
515 cell.syncCollapse = true;
516 cell.syncEditable = true;
517 cell.syncScrolled = true;
518 return cell;
519 }
520 /**
521 * Create a markdown cell widget from a markdown cell model.
522 */
523 _createMarkdownCell(model) {
524 const rendermime = this.rendermime;
525 const contentFactory = this.contentFactory;
526 const editorConfig = this.editorConfig.markdown;
527 const options = {
528 editorConfig,
529 model,
530 rendermime,
531 contentFactory,
532 updateEditorOnShow: false,
533 placeholder: false,
534 showEditorForReadOnlyMarkdown: this._notebookConfig
535 .showEditorForReadOnlyMarkdown
536 };
537 const cell = this.contentFactory.createMarkdownCell(options, this);
538 cell.syncCollapse = true;
539 cell.syncEditable = true;
540 // Connect collapsed signal for each markdown cell widget
541 cell.toggleCollapsedSignal.connect((newCell, collapsed) => {
542 NotebookActions.setHeadingCollapse(newCell, collapsed, this);
543 });
544 return cell;
545 }
546 /**
547 * Create a placeholder cell widget from a raw cell model.
548 */
549 _createPlaceholderCell(model, index) {
550 const contentFactory = this.contentFactory;
551 const editorConfig = this.editorConfig.raw;
552 const options = {
553 editorConfig,
554 model,
555 contentFactory,
556 updateEditorOnShow: false,
557 placeholder: true
558 };
559 const cell = this.contentFactory.createRawCell(options, this);
560 cell.node.innerHTML = `
561 <div class="jp-Cell-Placeholder">
562 <div class="jp-Cell-Placeholder-wrapper">
563 </div>
564 </div>`;
565 cell.inputHidden = true;
566 cell.syncCollapse = true;
567 cell.syncEditable = true;
568 return cell;
569 }
570 /**
571 * Create a raw cell widget from a raw cell model.
572 */
573 _createRawCell(model) {
574 const contentFactory = this.contentFactory;
575 const editorConfig = this.editorConfig.raw;
576 const options = {
577 editorConfig,
578 model,
579 contentFactory,
580 updateEditorOnShow: false,
581 placeholder: false
582 };
583 const cell = this.contentFactory.createRawCell(options, this);
584 cell.syncCollapse = true;
585 cell.syncEditable = true;
586 return cell;
587 }
588 /**
589 * Move a cell widget.
590 */
591 _moveCell(fromIndex, toIndex) {
592 const layout = this.layout;
593 layout.insertWidget(toIndex, layout.widgets[fromIndex]);
594 this.onCellMoved(fromIndex, toIndex);
595 }
596 /**
597 * Remove a cell widget.
598 */
599 _removeCell(index) {
600 const layout = this.layout;
601 const widget = layout.widgets[index];
602 widget.parent = null;
603 this.onCellRemoved(index, widget);
604 widget.dispose();
605 }
606 /**
607 * Update the mimetype of the notebook.
608 */
609 _updateMimetype() {
610 var _a;
611 const info = (_a = this._model) === null || _a === void 0 ? void 0 : _a.metadata.get('language_info');
612 if (!info) {
613 return;
614 }
615 this._mimetype = this._mimetypeService.getMimeTypeByLanguage(info);
616 each(this.widgets, widget => {
617 if (widget.model.type === 'code') {
618 widget.model.mimeType = this._mimetype;
619 }
620 });
621 }
622 /**
623 * Handle an update to the collaborators.
624 */
625 _onCollaboratorsChanged() {
626 var _a, _b, _c;
627 // If there are selections corresponding to non-collaborators,
628 // they are stale and should be removed.
629 for (let i = 0; i < this.widgets.length; i++) {
630 const cell = this.widgets[i];
631 for (const key of cell.model.selections.keys()) {
632 if (false === ((_c = (_b = (_a = this._model) === null || _a === void 0 ? void 0 : _a.modelDB) === null || _b === void 0 ? void 0 : _b.collaborators) === null || _c === void 0 ? void 0 : _c.has(key))) {
633 cell.model.selections.delete(key);
634 }
635 }
636 }
637 }
638 /**
639 * Update editor settings for notebook cells.
640 */
641 _updateEditorConfig() {
642 for (let i = 0; i < this.widgets.length; i++) {
643 const cell = this.widgets[i];
644 let config = {};
645 switch (cell.model.type) {
646 case 'code':
647 config = this._editorConfig.code;
648 break;
649 case 'markdown':
650 config = this._editorConfig.markdown;
651 break;
652 default:
653 config = this._editorConfig.raw;
654 break;
655 }
656 cell.editor.setOptions(Object.assign({}, config));
657 cell.editor.refresh();
658 }
659 }
660 /**
661 * Apply updated notebook settings.
662 */
663 _updateNotebookConfig() {
664 // Apply scrollPastEnd setting.
665 this.toggleClass('jp-mod-scrollPastEnd', this._notebookConfig.scrollPastEnd);
666 // Control editor visibility for read-only Markdown cells
667 const showEditorForReadOnlyMarkdown = this._notebookConfig
668 .showEditorForReadOnlyMarkdown;
669 // 'this._cellsArray' check is here as '_updateNotebookConfig()'
670 // can be called before 'this._cellsArray' is defined
671 if (showEditorForReadOnlyMarkdown !== undefined && this._cellsArray) {
672 for (const cell of this._cellsArray) {
673 if (cell.model.type === 'markdown') {
674 cell.showEditorForReadOnly = showEditorForReadOnlyMarkdown;
675 }
676 }
677 }
678 }
679 _incrementRenderedCount() {
680 if (this._toRenderMap.size === 0) {
681 this._fullyRendered.emit(true);
682 }
683 this._renderedCellsCount++;
684 }
685}
686/**
687 * The namespace for the `StaticNotebook` class statics.
688 */
689(function (StaticNotebook) {
690 /**
691 * Default configuration options for cell editors.
692 */
693 StaticNotebook.defaultEditorConfig = {
694 code: Object.assign(Object.assign({}, CodeEditor.defaultConfig), { lineWrap: 'off', matchBrackets: true, autoClosingBrackets: false }),
695 markdown: Object.assign(Object.assign({}, CodeEditor.defaultConfig), { lineWrap: 'on', matchBrackets: false, autoClosingBrackets: false }),
696 raw: Object.assign(Object.assign({}, CodeEditor.defaultConfig), { lineWrap: 'on', matchBrackets: false, autoClosingBrackets: false })
697 };
698 /**
699 * Default configuration options for notebooks.
700 */
701 StaticNotebook.defaultNotebookConfig = {
702 scrollPastEnd: true,
703 defaultCell: 'code',
704 recordTiming: false,
705 numberCellsToRenderDirectly: 20,
706 renderCellOnIdle: true,
707 observedTopMargin: '1000px',
708 observedBottomMargin: '1000px',
709 maxNumberOutputs: 50,
710 showEditorForReadOnlyMarkdown: true,
711 disableDocumentWideUndoRedo: false,
712 renderingLayout: 'default',
713 sideBySideLeftMarginOverride: '10px',
714 sideBySideRightMarginOverride: '10px'
715 };
716 /**
717 * The default implementation of an `IContentFactory`.
718 */
719 class ContentFactory extends Cell.ContentFactory {
720 /**
721 * Create a new code cell widget.
722 *
723 * #### Notes
724 * If no cell content factory is passed in with the options, the one on the
725 * notebook content factory is used.
726 */
727 createCodeCell(options, parent) {
728 if (!options.contentFactory) {
729 options.contentFactory = this;
730 }
731 return new CodeCell(options).initializeState();
732 }
733 /**
734 * Create a new markdown cell widget.
735 *
736 * #### Notes
737 * If no cell content factory is passed in with the options, the one on the
738 * notebook content factory is used.
739 */
740 createMarkdownCell(options, parent) {
741 if (!options.contentFactory) {
742 options.contentFactory = this;
743 }
744 return new MarkdownCell(options).initializeState();
745 }
746 /**
747 * Create a new raw cell widget.
748 *
749 * #### Notes
750 * If no cell content factory is passed in with the options, the one on the
751 * notebook content factory is used.
752 */
753 createRawCell(options, parent) {
754 if (!options.contentFactory) {
755 options.contentFactory = this;
756 }
757 return new RawCell(options).initializeState();
758 }
759 }
760 StaticNotebook.ContentFactory = ContentFactory;
761 /**
762 * Default content factory for the static notebook widget.
763 */
764 StaticNotebook.defaultContentFactory = new ContentFactory();
765})(StaticNotebook || (StaticNotebook = {}));
766/**
767 * A notebook widget that supports interactivity.
768 */
769export class Notebook extends StaticNotebook {
770 /**
771 * Construct a notebook widget.
772 */
773 constructor(options) {
774 super(Private.processNotebookOptions(options));
775 this._activeCellIndex = -1;
776 this._activeCell = null;
777 this._mode = 'command';
778 this._drag = null;
779 this._fragment = '';
780 this._dragData = null;
781 this._mouseMode = null;
782 this._activeCellChanged = new Signal(this);
783 this._stateChanged = new Signal(this);
784 this._selectionChanged = new Signal(this);
785 this._checkCacheOnNextResize = false;
786 this._lastClipboardInteraction = null;
787 this.node.tabIndex = 0; // Allow the widget to take focus.
788 // Allow the node to scroll while dragging items.
789 this.node.setAttribute('data-lm-dragscroll', 'true');
790 }
791 /**
792 * A signal emitted when the active cell changes.
793 *
794 * #### Notes
795 * This can be due to the active index changing or the
796 * cell at the active index changing.
797 */
798 get activeCellChanged() {
799 return this._activeCellChanged;
800 }
801 /**
802 * A signal emitted when the state of the notebook changes.
803 */
804 get stateChanged() {
805 return this._stateChanged;
806 }
807 /**
808 * A signal emitted when the selection state of the notebook changes.
809 */
810 get selectionChanged() {
811 return this._selectionChanged;
812 }
813 /**
814 * The interactivity mode of the notebook.
815 */
816 get mode() {
817 return this._mode;
818 }
819 set mode(newValue) {
820 const activeCell = this.activeCell;
821 if (!activeCell) {
822 newValue = 'command';
823 }
824 if (newValue === this._mode) {
825 this._ensureFocus();
826 return;
827 }
828 // Post an update request.
829 this.update();
830 const oldValue = this._mode;
831 this._mode = newValue;
832 if (newValue === 'edit') {
833 // Edit mode deselects all cells.
834 each(this.widgets, widget => {
835 this.deselect(widget);
836 });
837 // Edit mode unrenders an active markdown widget.
838 if (activeCell instanceof MarkdownCell) {
839 activeCell.rendered = false;
840 }
841 activeCell.inputHidden = false;
842 }
843 else {
844 // Focus on the notebook document, which blurs the active cell.
845 this.node.focus();
846 }
847 this._stateChanged.emit({ name: 'mode', oldValue, newValue });
848 this._ensureFocus();
849 }
850 /**
851 * The active cell index of the notebook.
852 *
853 * #### Notes
854 * The index will be clamped to the bounds of the notebook cells.
855 */
856 get activeCellIndex() {
857 if (!this.model) {
858 return -1;
859 }
860 return this.model.cells.length ? this._activeCellIndex : -1;
861 }
862 set activeCellIndex(newValue) {
863 const oldValue = this._activeCellIndex;
864 if (!this.model || !this.model.cells.length) {
865 newValue = -1;
866 }
867 else {
868 newValue = Math.max(newValue, 0);
869 newValue = Math.min(newValue, this.model.cells.length - 1);
870 }
871 this._activeCellIndex = newValue;
872 const cell = this.widgets[newValue];
873 if (cell !== this._activeCell) {
874 // Post an update request.
875 this.update();
876 this._activeCell = cell;
877 this._activeCellChanged.emit(cell);
878 }
879 if (this.mode === 'edit' && cell instanceof MarkdownCell) {
880 cell.rendered = false;
881 }
882 this._ensureFocus();
883 if (newValue === oldValue) {
884 return;
885 }
886 this._trimSelections();
887 this._stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue });
888 }
889 /**
890 * Get the active cell widget.
891 *
892 * #### Notes
893 * This is a cell or `null` if there is no active cell.
894 */
895 get activeCell() {
896 return this._activeCell;
897 }
898 get lastClipboardInteraction() {
899 return this._lastClipboardInteraction;
900 }
901 set lastClipboardInteraction(newValue) {
902 this._lastClipboardInteraction = newValue;
903 }
904 /**
905 * Dispose of the resources held by the widget.
906 */
907 dispose() {
908 if (this.isDisposed) {
909 return;
910 }
911 this._activeCell = null;
912 super.dispose();
913 }
914 /**
915 * Select a cell widget.
916 *
917 * #### Notes
918 * It is a no-op if the value does not change.
919 * It will emit the `selectionChanged` signal.
920 */
921 select(widget) {
922 if (Private.selectedProperty.get(widget)) {
923 return;
924 }
925 Private.selectedProperty.set(widget, true);
926 this._selectionChanged.emit(void 0);
927 this.update();
928 }
929 /**
930 * Deselect a cell widget.
931 *
932 * #### Notes
933 * It is a no-op if the value does not change.
934 * It will emit the `selectionChanged` signal.
935 */
936 deselect(widget) {
937 if (!Private.selectedProperty.get(widget)) {
938 return;
939 }
940 Private.selectedProperty.set(widget, false);
941 this._selectionChanged.emit(void 0);
942 this.update();
943 }
944 /**
945 * Whether a cell is selected.
946 */
947 isSelected(widget) {
948 return Private.selectedProperty.get(widget);
949 }
950 /**
951 * Whether a cell is selected or is the active cell.
952 */
953 isSelectedOrActive(widget) {
954 if (widget === this._activeCell) {
955 return true;
956 }
957 return Private.selectedProperty.get(widget);
958 }
959 /**
960 * Deselect all of the cells.
961 */
962 deselectAll() {
963 let changed = false;
964 each(this.widgets, widget => {
965 if (Private.selectedProperty.get(widget)) {
966 changed = true;
967 }
968 Private.selectedProperty.set(widget, false);
969 });
970 if (changed) {
971 this._selectionChanged.emit(void 0);
972 }
973 // Make sure we have a valid active cell.
974 this.activeCellIndex = this.activeCellIndex; // eslint-disable-line
975 this.update();
976 }
977 /**
978 * Move the head of an existing contiguous selection to extend the selection.
979 *
980 * @param index - The new head of the existing selection.
981 *
982 * #### Notes
983 * If there is no existing selection, the active cell is considered an
984 * existing one-cell selection.
985 *
986 * If the new selection is a single cell, that cell becomes the active cell
987 * and all cells are deselected.
988 *
989 * There is no change if there are no cells (i.e., activeCellIndex is -1).
990 */
991 extendContiguousSelectionTo(index) {
992 let { head, anchor } = this.getContiguousSelection();
993 let i;
994 // Handle the case of no current selection.
995 if (anchor === null || head === null) {
996 if (index === this.activeCellIndex) {
997 // Already collapsed selection, nothing more to do.
998 return;
999 }
1000 // We will start a new selection below.
1001 head = this.activeCellIndex;
1002 anchor = this.activeCellIndex;
1003 }
1004 // Move the active cell. We do this before the collapsing shortcut below.
1005 this.activeCellIndex = index;
1006 // Make sure the index is valid, according to the rules for setting and clipping the
1007 // active cell index. This may change the index.
1008 index = this.activeCellIndex;
1009 // Collapse the selection if it is only the active cell.
1010 if (index === anchor) {
1011 this.deselectAll();
1012 return;
1013 }
1014 let selectionChanged = false;
1015 if (head < index) {
1016 if (head < anchor) {
1017 Private.selectedProperty.set(this.widgets[head], false);
1018 selectionChanged = true;
1019 }
1020 // Toggle everything strictly between head and index except anchor.
1021 for (i = head + 1; i < index; i++) {
1022 if (i !== anchor) {
1023 Private.selectedProperty.set(this.widgets[i], !Private.selectedProperty.get(this.widgets[i]));
1024 selectionChanged = true;
1025 }
1026 }
1027 }
1028 else if (index < head) {
1029 if (anchor < head) {
1030 Private.selectedProperty.set(this.widgets[head], false);
1031 selectionChanged = true;
1032 }
1033 // Toggle everything strictly between index and head except anchor.
1034 for (i = index + 1; i < head; i++) {
1035 if (i !== anchor) {
1036 Private.selectedProperty.set(this.widgets[i], !Private.selectedProperty.get(this.widgets[i]));
1037 selectionChanged = true;
1038 }
1039 }
1040 }
1041 // Anchor and index should *always* be selected.
1042 if (!Private.selectedProperty.get(this.widgets[anchor])) {
1043 selectionChanged = true;
1044 }
1045 Private.selectedProperty.set(this.widgets[anchor], true);
1046 if (!Private.selectedProperty.get(this.widgets[index])) {
1047 selectionChanged = true;
1048 }
1049 Private.selectedProperty.set(this.widgets[index], true);
1050 if (selectionChanged) {
1051 this._selectionChanged.emit(void 0);
1052 }
1053 }
1054 /**
1055 * Get the head and anchor of a contiguous cell selection.
1056 *
1057 * The head of a contiguous selection is always the active cell.
1058 *
1059 * If there are no cells selected, `{head: null, anchor: null}` is returned.
1060 *
1061 * Throws an error if the currently selected cells do not form a contiguous
1062 * selection.
1063 */
1064 getContiguousSelection() {
1065 const cells = this.widgets;
1066 const first = ArrayExt.findFirstIndex(cells, c => this.isSelected(c));
1067 // Return early if no cells are selected.
1068 if (first === -1) {
1069 return { head: null, anchor: null };
1070 }
1071 const last = ArrayExt.findLastIndex(cells, c => this.isSelected(c), -1, first);
1072 // Check that the selection is contiguous.
1073 for (let i = first; i <= last; i++) {
1074 if (!this.isSelected(cells[i])) {
1075 throw new Error('Selection not contiguous');
1076 }
1077 }
1078 // Check that the active cell is one of the endpoints of the selection.
1079 const activeIndex = this.activeCellIndex;
1080 if (first !== activeIndex && last !== activeIndex) {
1081 throw new Error('Active cell not at endpoint of selection');
1082 }
1083 // Determine the head and anchor of the selection.
1084 if (first === activeIndex) {
1085 return { head: first, anchor: last };
1086 }
1087 else {
1088 return { head: last, anchor: first };
1089 }
1090 }
1091 /**
1092 * Scroll so that the given position is centered.
1093 *
1094 * @param position - The vertical position in the notebook widget.
1095 *
1096 * @param threshold - An optional threshold for the scroll (0-50, defaults to
1097 * 25).
1098 *
1099 * #### Notes
1100 * If the position is within the threshold percentage of the widget height,
1101 * measured from the center of the widget, the scroll position will not be
1102 * changed. A threshold of 0 means we will always scroll so the position is
1103 * centered, and a threshold of 50 means scrolling only happens if position is
1104 * outside the current window.
1105 */
1106 scrollToPosition(position, threshold = 25) {
1107 const node = this.node;
1108 const ar = node.getBoundingClientRect();
1109 const delta = position - ar.top - ar.height / 2;
1110 if (Math.abs(delta) > (ar.height * threshold) / 100) {
1111 node.scrollTop += delta;
1112 }
1113 }
1114 /**
1115 * Scroll so that the given cell is in view. Selects and activates cell.
1116 *
1117 * @param cell - A cell in the notebook widget.
1118 *
1119 */
1120 scrollToCell(cell) {
1121 // use Phosphor to scroll
1122 ElementExt.scrollIntoViewIfNeeded(this.node, cell.node);
1123 // change selection and active cell:
1124 this.deselectAll();
1125 this.select(cell);
1126 cell.activate();
1127 }
1128 /**
1129 * Set URI fragment identifier.
1130 */
1131 setFragment(fragment) {
1132 // Wait all cells are rendered then set fragment and update.
1133 void Promise.all(this.widgets.map(widget => widget.ready)).then(() => {
1134 this._fragment = fragment;
1135 this.update();
1136 });
1137 }
1138 /**
1139 * Handle the DOM events for the widget.
1140 *
1141 * @param event - The DOM event sent to the widget.
1142 *
1143 * #### Notes
1144 * This method implements the DOM `EventListener` interface and is
1145 * called in response to events on the notebook panel's node. It should
1146 * not be called directly by user code.
1147 */
1148 handleEvent(event) {
1149 if (!this.model) {
1150 return;
1151 }
1152 switch (event.type) {
1153 case 'contextmenu':
1154 if (event.eventPhase === Event.CAPTURING_PHASE) {
1155 this._evtContextMenuCapture(event);
1156 }
1157 break;
1158 case 'mousedown':
1159 if (event.eventPhase === Event.CAPTURING_PHASE) {
1160 this._evtMouseDownCapture(event);
1161 }
1162 else {
1163 this._evtMouseDown(event);
1164 }
1165 break;
1166 case 'mouseup':
1167 if (event.currentTarget === document) {
1168 this._evtDocumentMouseup(event);
1169 }
1170 break;
1171 case 'mousemove':
1172 if (event.currentTarget === document) {
1173 this._evtDocumentMousemove(event);
1174 }
1175 break;
1176 case 'keydown':
1177 this._ensureFocus(true);
1178 break;
1179 case 'dblclick':
1180 this._evtDblClick(event);
1181 break;
1182 case 'focusin':
1183 this._evtFocusIn(event);
1184 break;
1185 case 'focusout':
1186 this._evtFocusOut(event);
1187 break;
1188 case 'lm-dragenter':
1189 this._evtDragEnter(event);
1190 break;
1191 case 'lm-dragleave':
1192 this._evtDragLeave(event);
1193 break;
1194 case 'lm-dragover':
1195 this._evtDragOver(event);
1196 break;
1197 case 'lm-drop':
1198 this._evtDrop(event);
1199 break;
1200 default:
1201 break;
1202 }
1203 }
1204 /**
1205 * Handle `after-attach` messages for the widget.
1206 */
1207 onAfterAttach(msg) {
1208 super.onAfterAttach(msg);
1209 const node = this.node;
1210 node.addEventListener('contextmenu', this, true);
1211 node.addEventListener('mousedown', this, true);
1212 node.addEventListener('mousedown', this);
1213 node.addEventListener('keydown', this);
1214 node.addEventListener('dblclick', this);
1215 node.addEventListener('focusin', this);
1216 node.addEventListener('focusout', this);
1217 // Capture drag events for the notebook widget
1218 // in order to preempt the drag/drop handlers in the
1219 // code editor widgets, which can take text data.
1220 node.addEventListener('lm-dragenter', this, true);
1221 node.addEventListener('lm-dragleave', this, true);
1222 node.addEventListener('lm-dragover', this, true);
1223 node.addEventListener('lm-drop', this, true);
1224 }
1225 /**
1226 * Handle `before-detach` messages for the widget.
1227 */
1228 onBeforeDetach(msg) {
1229 const node = this.node;
1230 node.removeEventListener('contextmenu', this, true);
1231 node.removeEventListener('mousedown', this, true);
1232 node.removeEventListener('mousedown', this);
1233 node.removeEventListener('keydown', this);
1234 node.removeEventListener('dblclick', this);
1235 node.removeEventListener('focusin', this);
1236 node.removeEventListener('focusout', this);
1237 node.removeEventListener('lm-dragenter', this, true);
1238 node.removeEventListener('lm-dragleave', this, true);
1239 node.removeEventListener('lm-dragover', this, true);
1240 node.removeEventListener('lm-drop', this, true);
1241 document.removeEventListener('mousemove', this, true);
1242 document.removeEventListener('mouseup', this, true);
1243 }
1244 /**
1245 * A message handler invoked on an `'after-show'` message.
1246 */
1247 onAfterShow(msg) {
1248 this._checkCacheOnNextResize = true;
1249 }
1250 /**
1251 * A message handler invoked on a `'resize'` message.
1252 */
1253 onResize(msg) {
1254 if (!this._checkCacheOnNextResize) {
1255 return super.onResize(msg);
1256 }
1257 this._checkCacheOnNextResize = false;
1258 const cache = this._cellLayoutStateCache;
1259 const width = parseInt(this.node.style.width, 10);
1260 if (cache) {
1261 if (width === cache.width) {
1262 // Cache identical, do nothing
1263 return;
1264 }
1265 }
1266 // Update cache
1267 this._cellLayoutStateCache = { width };
1268 // Fallback:
1269 for (const w of this.widgets) {
1270 if (w instanceof Cell) {
1271 w.editorWidget.update();
1272 }
1273 }
1274 }
1275 /**
1276 * A message handler invoked on an `'before-hide'` message.
1277 */
1278 onBeforeHide(msg) {
1279 // Update cache
1280 const width = parseInt(this.node.style.width, 10);
1281 this._cellLayoutStateCache = { width };
1282 }
1283 /**
1284 * Handle `'activate-request'` messages.
1285 */
1286 onActivateRequest(msg) {
1287 this._ensureFocus(true);
1288 }
1289 /**
1290 * Handle `update-request` messages sent to the widget.
1291 */
1292 onUpdateRequest(msg) {
1293 const activeCell = this.activeCell;
1294 // Set the appropriate classes on the cells.
1295 if (this.mode === 'edit') {
1296 this.addClass(EDIT_CLASS);
1297 this.removeClass(COMMAND_CLASS);
1298 }
1299 else {
1300 this.addClass(COMMAND_CLASS);
1301 this.removeClass(EDIT_CLASS);
1302 }
1303 if (activeCell) {
1304 activeCell.addClass(ACTIVE_CLASS);
1305 }
1306 let count = 0;
1307 each(this.widgets, widget => {
1308 if (widget !== activeCell) {
1309 widget.removeClass(ACTIVE_CLASS);
1310 }
1311 widget.removeClass(OTHER_SELECTED_CLASS);
1312 if (this.isSelectedOrActive(widget)) {
1313 widget.addClass(SELECTED_CLASS);
1314 count++;
1315 }
1316 else {
1317 widget.removeClass(SELECTED_CLASS);
1318 }
1319 });
1320 if (count > 1) {
1321 activeCell === null || activeCell === void 0 ? void 0 : activeCell.addClass(OTHER_SELECTED_CLASS);
1322 }
1323 if (this._fragment) {
1324 let el;
1325 try {
1326 el = this.node.querySelector(this._fragment.startsWith('#')
1327 ? `#${CSS.escape(this._fragment.slice(1))}`
1328 : this._fragment);
1329 }
1330 catch (error) {
1331 console.warn('Unable to set URI fragment identifier', error);
1332 }
1333 if (el) {
1334 el.scrollIntoView();
1335 }
1336 this._fragment = '';
1337 }
1338 }
1339 /**
1340 * Handle a cell being inserted.
1341 */
1342 onCellInserted(index, cell) {
1343 if (this.model && this.model.modelDB.isCollaborative) {
1344 const modelDB = this.model.modelDB;
1345 void modelDB.connected.then(() => {
1346 if (!cell.isDisposed) {
1347 // Setup the selection style for collaborators.
1348 const localCollaborator = modelDB.collaborators.localCollaborator;
1349 cell.editor.uuid = localCollaborator.sessionId;
1350 cell.editor.selectionStyle = Object.assign(Object.assign({}, CodeEditor.defaultSelectionStyle), { color: localCollaborator.color });
1351 }
1352 });
1353 }
1354 cell.editor.edgeRequested.connect(this._onEdgeRequest, this);
1355 // If the insertion happened above, increment the active cell
1356 // index, otherwise it stays the same.
1357 this.activeCellIndex =
1358 index <= this.activeCellIndex
1359 ? this.activeCellIndex + 1
1360 : this.activeCellIndex;
1361 }
1362 /**
1363 * Handle a cell being moved.
1364 */
1365 onCellMoved(fromIndex, toIndex) {
1366 const i = this.activeCellIndex;
1367 if (fromIndex === i) {
1368 this.activeCellIndex = toIndex;
1369 }
1370 else if (fromIndex < i && i <= toIndex) {
1371 this.activeCellIndex--;
1372 }
1373 else if (toIndex <= i && i < fromIndex) {
1374 this.activeCellIndex++;
1375 }
1376 }
1377 /**
1378 * Handle a cell being removed.
1379 */
1380 onCellRemoved(index, cell) {
1381 // If the removal happened above, decrement the active
1382 // cell index, otherwise it stays the same.
1383 this.activeCellIndex =
1384 index <= this.activeCellIndex
1385 ? this.activeCellIndex - 1
1386 : this.activeCellIndex;
1387 if (this.isSelected(cell)) {
1388 this._selectionChanged.emit(void 0);
1389 }
1390 }
1391 /**
1392 * Handle a new model.
1393 */
1394 onModelChanged(oldValue, newValue) {
1395 super.onModelChanged(oldValue, newValue);
1396 // Try to set the active cell index to 0.
1397 // It will be set to `-1` if there is no new model or the model is empty.
1398 this.activeCellIndex = 0;
1399 }
1400 /**
1401 * Handle edge request signals from cells.
1402 */
1403 _onEdgeRequest(editor, location) {
1404 const prev = this.activeCellIndex;
1405 if (location === 'top') {
1406 this.activeCellIndex--;
1407 // Move the cursor to the first position on the last line.
1408 if (this.activeCellIndex < prev) {
1409 const editor = this.activeCell.editor;
1410 const lastLine = editor.lineCount - 1;
1411 editor.setCursorPosition({ line: lastLine, column: 0 });
1412 }
1413 }
1414 else if (location === 'bottom') {
1415 this.activeCellIndex++;
1416 // Move the cursor to the first character.
1417 if (this.activeCellIndex > prev) {
1418 const editor = this.activeCell.editor;
1419 editor.setCursorPosition({ line: 0, column: 0 });
1420 }
1421 }
1422 this.mode = 'edit';
1423 }
1424 /**
1425 * Ensure that the notebook has proper focus.
1426 */
1427 _ensureFocus(force = false) {
1428 const activeCell = this.activeCell;
1429 if (this.mode === 'edit' && activeCell) {
1430 if (!activeCell.editor.hasFocus()) {
1431 activeCell.editor.focus();
1432 }
1433 }
1434 if (force && !this.node.contains(document.activeElement)) {
1435 this.node.focus();
1436 }
1437 }
1438 /**
1439 * Find the cell index containing the target html element.
1440 *
1441 * #### Notes
1442 * Returns -1 if the cell is not found.
1443 */
1444 _findCell(node) {
1445 // Trace up the DOM hierarchy to find the root cell node.
1446 // Then find the corresponding child and select it.
1447 let n = node;
1448 while (n && n !== this.node) {
1449 if (n.classList.contains(NB_CELL_CLASS)) {
1450 const i = ArrayExt.findFirstIndex(this.widgets, widget => widget.node === n);
1451 if (i !== -1) {
1452 return i;
1453 }
1454 break;
1455 }
1456 n = n.parentElement;
1457 }
1458 return -1;
1459 }
1460 /**
1461 * Find the target of html mouse event and cell index containing this target.
1462 *
1463 * #### Notes
1464 * Returned index is -1 if the cell is not found.
1465 */
1466 _findEventTargetAndCell(event) {
1467 let target = event.target;
1468 let index = this._findCell(target);
1469 if (index === -1) {
1470 // `event.target` sometimes gives an orphaned node in Firefox 57, which
1471 // can have `null` anywhere in its parent line. If we fail to find a cell
1472 // using `event.target`, try again using a target reconstructed from the
1473 // position of the click event.
1474 target = document.elementFromPoint(event.clientX, event.clientY);
1475 index = this._findCell(target);
1476 }
1477 return [target, index];
1478 }
1479 /**
1480 * Handle `contextmenu` event.
1481 */
1482 _evtContextMenuCapture(event) {
1483 // Allow the event to propagate un-modified if the user
1484 // is holding the shift-key (and probably requesting
1485 // the native context menu).
1486 if (event.shiftKey) {
1487 return;
1488 }
1489 const [target, index] = this._findEventTargetAndCell(event);
1490 const widget = this.widgets[index];
1491 if (widget && widget.editorWidget.node.contains(target)) {
1492 // Prevent CodeMirror from focusing the editor.
1493 // TODO: find an editor-agnostic solution.
1494 event.preventDefault();
1495 }
1496 }
1497 /**
1498 * Handle `mousedown` event in the capture phase for the widget.
1499 */
1500 _evtMouseDownCapture(event) {
1501 const { button, shiftKey } = event;
1502 const [target, index] = this._findEventTargetAndCell(event);
1503 const widget = this.widgets[index];
1504 // On OS X, the context menu may be triggered with ctrl-left-click. In
1505 // Firefox, ctrl-left-click gives an event with button 2, but in Chrome,
1506 // ctrl-left-click gives an event with button 0 with the ctrl modifier.
1507 if (button === 2 &&
1508 !shiftKey &&
1509 widget &&
1510 widget.editorWidget.node.contains(target)) {
1511 this.mode = 'command';
1512 // Prevent CodeMirror from focusing the editor.
1513 // TODO: find an editor-agnostic solution.
1514 event.preventDefault();
1515 }
1516 }
1517 /**
1518 * Handle `mousedown` events for the widget.
1519 */
1520 _evtMouseDown(event) {
1521 var _a;
1522 const { button, shiftKey } = event;
1523 // We only handle main or secondary button actions.
1524 if (!(button === 0 || button === 2)) {
1525 return;
1526 }
1527 // Shift right-click gives the browser default behavior.
1528 if (shiftKey && button === 2) {
1529 return;
1530 }
1531 const [target, index] = this._findEventTargetAndCell(event);
1532 const widget = this.widgets[index];
1533 let targetArea;
1534 if (widget) {
1535 if (widget.editorWidget.node.contains(target)) {
1536 targetArea = 'input';
1537 }
1538 else if (widget.promptNode.contains(target)) {
1539 targetArea = 'prompt';
1540 }
1541 else {
1542 targetArea = 'cell';
1543 }
1544 }
1545 else {
1546 targetArea = 'notebook';
1547 }
1548 // Make sure we go to command mode if the click isn't in the cell editor If
1549 // we do click in the cell editor, the editor handles the focus event to
1550 // switch to edit mode.
1551 if (targetArea !== 'input') {
1552 this.mode = 'command';
1553 }
1554 if (targetArea === 'notebook') {
1555 this.deselectAll();
1556 }
1557 else if (targetArea === 'prompt' || targetArea === 'cell') {
1558 // We don't want to prevent the default selection behavior
1559 // if there is currently text selected in an output.
1560 const hasSelection = ((_a = window.getSelection()) !== null && _a !== void 0 ? _a : '').toString() !== '';
1561 if (button === 0 && shiftKey && !hasSelection) {
1562 // Prevent browser selecting text in prompt or output
1563 event.preventDefault();
1564 // Shift-click - extend selection
1565 try {
1566 this.extendContiguousSelectionTo(index);
1567 }
1568 catch (e) {
1569 console.error(e);
1570 this.deselectAll();
1571 return;
1572 }
1573 // Enter selecting mode
1574 this._mouseMode = 'select';
1575 document.addEventListener('mouseup', this, true);
1576 document.addEventListener('mousemove', this, true);
1577 }
1578 else if (button === 0 && !shiftKey) {
1579 // Prepare to start a drag if we are on the drag region.
1580 if (targetArea === 'prompt') {
1581 // Prepare for a drag start
1582 this._dragData = {
1583 pressX: event.clientX,
1584 pressY: event.clientY,
1585 index: index
1586 };
1587 // Enter possible drag mode
1588 this._mouseMode = 'couldDrag';
1589 document.addEventListener('mouseup', this, true);
1590 document.addEventListener('mousemove', this, true);
1591 event.preventDefault();
1592 }
1593 if (!this.isSelectedOrActive(widget)) {
1594 this.deselectAll();
1595 this.activeCellIndex = index;
1596 }
1597 }
1598 else if (button === 2) {
1599 if (!this.isSelectedOrActive(widget)) {
1600 this.deselectAll();
1601 this.activeCellIndex = index;
1602 }
1603 event.preventDefault();
1604 }
1605 }
1606 else if (targetArea === 'input') {
1607 if (button === 2 && !this.isSelectedOrActive(widget)) {
1608 this.deselectAll();
1609 this.activeCellIndex = index;
1610 }
1611 }
1612 // If we didn't set focus above, make sure we get focus now.
1613 this._ensureFocus(true);
1614 }
1615 /**
1616 * Handle the `'mouseup'` event on the document.
1617 */
1618 _evtDocumentMouseup(event) {
1619 event.preventDefault();
1620 event.stopPropagation();
1621 // Remove the event listeners we put on the document
1622 document.removeEventListener('mousemove', this, true);
1623 document.removeEventListener('mouseup', this, true);
1624 if (this._mouseMode === 'couldDrag') {
1625 // We didn't end up dragging if we are here, so treat it as a click event.
1626 const [, index] = this._findEventTargetAndCell(event);
1627 this.deselectAll();
1628 this.activeCellIndex = index;
1629 }
1630 this._mouseMode = null;
1631 }
1632 /**
1633 * Handle the `'mousemove'` event for the widget.
1634 */
1635 _evtDocumentMousemove(event) {
1636 event.preventDefault();
1637 event.stopPropagation();
1638 // If in select mode, update the selection
1639 switch (this._mouseMode) {
1640 case 'select': {
1641 const target = event.target;
1642 const index = this._findCell(target);
1643 if (index !== -1) {
1644 this.extendContiguousSelectionTo(index);
1645 }
1646 break;
1647 }
1648 case 'couldDrag': {
1649 // Check for a drag initialization.
1650 const data = this._dragData;
1651 const dx = Math.abs(event.clientX - data.pressX);
1652 const dy = Math.abs(event.clientY - data.pressY);
1653 if (dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD) {
1654 this._mouseMode = null;
1655 this._startDrag(data.index, event.clientX, event.clientY);
1656 }
1657 break;
1658 }
1659 default:
1660 break;
1661 }
1662 }
1663 /**
1664 * Handle the `'lm-dragenter'` event for the widget.
1665 */
1666 _evtDragEnter(event) {
1667 if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
1668 return;
1669 }
1670 event.preventDefault();
1671 event.stopPropagation();
1672 const target = event.target;
1673 const index = this._findCell(target);
1674 if (index === -1) {
1675 return;
1676 }
1677 const widget = this.layout.widgets[index];
1678 widget.node.classList.add(DROP_TARGET_CLASS);
1679 }
1680 /**
1681 * Handle the `'lm-dragleave'` event for the widget.
1682 */
1683 _evtDragLeave(event) {
1684 if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
1685 return;
1686 }
1687 event.preventDefault();
1688 event.stopPropagation();
1689 const elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
1690 if (elements.length) {
1691 elements[0].classList.remove(DROP_TARGET_CLASS);
1692 }
1693 }
1694 /**
1695 * Handle the `'lm-dragover'` event for the widget.
1696 */
1697 _evtDragOver(event) {
1698 if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
1699 return;
1700 }
1701 event.preventDefault();
1702 event.stopPropagation();
1703 event.dropAction = event.proposedAction;
1704 const elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
1705 if (elements.length) {
1706 elements[0].classList.remove(DROP_TARGET_CLASS);
1707 }
1708 const target = event.target;
1709 const index = this._findCell(target);
1710 if (index === -1) {
1711 return;
1712 }
1713 const widget = this.layout.widgets[index];
1714 widget.node.classList.add(DROP_TARGET_CLASS);
1715 }
1716 /**
1717 * Handle the `'lm-drop'` event for the widget.
1718 */
1719 _evtDrop(event) {
1720 if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
1721 return;
1722 }
1723 event.preventDefault();
1724 event.stopPropagation();
1725 if (event.proposedAction === 'none') {
1726 event.dropAction = 'none';
1727 return;
1728 }
1729 let target = event.target;
1730 while (target && target.parentElement) {
1731 if (target.classList.contains(DROP_TARGET_CLASS)) {
1732 target.classList.remove(DROP_TARGET_CLASS);
1733 break;
1734 }
1735 target = target.parentElement;
1736 }
1737 // Model presence should be checked before calling event handlers
1738 const model = this.model;
1739 const source = event.source;
1740 if (source === this) {
1741 // Handle the case where we are moving cells within
1742 // the same notebook.
1743 event.dropAction = 'move';
1744 const toMove = event.mimeData.getData('internal:cells');
1745 // For collapsed markdown headings with hidden "child" cells, move all
1746 // child cells as well as the markdown heading.
1747 const cell = toMove[toMove.length - 1];
1748 if (cell instanceof MarkdownCell && cell.headingCollapsed) {
1749 const nextParent = NotebookActions.findNextParentHeading(cell, source);
1750 if (nextParent > 0) {
1751 const index = findIndex(source.widgets, (possibleCell) => {
1752 return cell.model.id === possibleCell.model.id;
1753 });
1754 toMove.push(...source.widgets.slice(index + 1, nextParent));
1755 }
1756 }
1757 // Compute the to/from indices for the move.
1758 let fromIndex = ArrayExt.firstIndexOf(this.widgets, toMove[0]);
1759 let toIndex = this._findCell(target);
1760 // This check is needed for consistency with the view.
1761 if (toIndex !== -1 && toIndex > fromIndex) {
1762 toIndex -= 1;
1763 }
1764 else if (toIndex === -1) {
1765 // If the drop is within the notebook but not on any cell,
1766 // most often this means it is past the cell areas, so
1767 // set it to move the cells to the end of the notebook.
1768 toIndex = this.widgets.length - 1;
1769 }
1770 // Don't move if we are within the block of selected cells.
1771 if (toIndex >= fromIndex && toIndex < fromIndex + toMove.length) {
1772 return;
1773 }
1774 // Move the cells one by one
1775 model.cells.beginCompoundOperation();
1776 if (fromIndex < toIndex) {
1777 each(toMove, cellWidget => {
1778 model.cells.move(fromIndex, toIndex);
1779 });
1780 }
1781 else if (fromIndex > toIndex) {
1782 each(toMove, cellWidget => {
1783 model.cells.move(fromIndex++, toIndex++);
1784 });
1785 }
1786 model.cells.endCompoundOperation();
1787 }
1788 else {
1789 // Handle the case where we are copying cells between
1790 // notebooks.
1791 event.dropAction = 'copy';
1792 // Find the target cell and insert the copied cells.
1793 let index = this._findCell(target);
1794 if (index === -1) {
1795 index = this.widgets.length;
1796 }
1797 const start = index;
1798 const values = event.mimeData.getData(JUPYTER_CELL_MIME);
1799 const factory = model.contentFactory;
1800 // Insert the copies of the original cells.
1801 model.cells.beginCompoundOperation();
1802 each(values, (cell) => {
1803 let value;
1804 switch (cell.cell_type) {
1805 case 'code':
1806 value = factory.createCodeCell({ cell });
1807 break;
1808 case 'markdown':
1809 value = factory.createMarkdownCell({ cell });
1810 break;
1811 default:
1812 value = factory.createRawCell({ cell });
1813 break;
1814 }
1815 model.cells.insert(index++, value);
1816 });
1817 model.cells.endCompoundOperation();
1818 // Select the inserted cells.
1819 this.deselectAll();
1820 this.activeCellIndex = start;
1821 this.extendContiguousSelectionTo(index - 1);
1822 }
1823 }
1824 /**
1825 * Start a drag event.
1826 */
1827 _startDrag(index, clientX, clientY) {
1828 var _a;
1829 const cells = this.model.cells;
1830 const selected = [];
1831 const toMove = [];
1832 each(this.widgets, (widget, i) => {
1833 const cell = cells.get(i);
1834 if (this.isSelectedOrActive(widget)) {
1835 widget.addClass(DROP_SOURCE_CLASS);
1836 selected.push(cell.toJSON());
1837 toMove.push(widget);
1838 }
1839 });
1840 const activeCell = this.activeCell;
1841 let dragImage = null;
1842 let countString;
1843 if ((activeCell === null || activeCell === void 0 ? void 0 : activeCell.model.type) === 'code') {
1844 const executionCount = activeCell.model
1845 .executionCount;
1846 countString = ' ';
1847 if (executionCount) {
1848 countString = executionCount.toString();
1849 }
1850 }
1851 else {
1852 countString = '';
1853 }
1854 // Create the drag image.
1855 dragImage = Private.createDragImage(selected.length, countString, (_a = activeCell === null || activeCell === void 0 ? void 0 : activeCell.model.value.text.split('\n')[0].slice(0, 26)) !== null && _a !== void 0 ? _a : '');
1856 // Set up the drag event.
1857 this._drag = new Drag({
1858 mimeData: new MimeData(),
1859 dragImage,
1860 supportedActions: 'copy-move',
1861 proposedAction: 'copy',
1862 source: this
1863 });
1864 this._drag.mimeData.setData(JUPYTER_CELL_MIME, selected);
1865 // Add mimeData for the fully reified cell widgets, for the
1866 // case where the target is in the same notebook and we
1867 // can just move the cells.
1868 this._drag.mimeData.setData('internal:cells', toMove);
1869 // Add mimeData for the text content of the selected cells,
1870 // allowing for drag/drop into plain text fields.
1871 const textContent = toMove.map(cell => cell.model.value.text).join('\n');
1872 this._drag.mimeData.setData('text/plain', textContent);
1873 // Remove mousemove and mouseup listeners and start the drag.
1874 document.removeEventListener('mousemove', this, true);
1875 document.removeEventListener('mouseup', this, true);
1876 this._mouseMode = null;
1877 void this._drag.start(clientX, clientY).then(action => {
1878 if (this.isDisposed) {
1879 return;
1880 }
1881 this._drag = null;
1882 each(toMove, widget => {
1883 widget.removeClass(DROP_SOURCE_CLASS);
1884 });
1885 });
1886 }
1887 /**
1888 * Handle `focus` events for the widget.
1889 */
1890 _evtFocusIn(event) {
1891 const target = event.target;
1892 const index = this._findCell(target);
1893 if (index !== -1) {
1894 const widget = this.widgets[index];
1895 // If the editor itself does not have focus, ensure command mode.
1896 if (!widget.editorWidget.node.contains(target)) {
1897 this.mode = 'command';
1898 }
1899 this.activeCellIndex = index;
1900 // If the editor has focus, ensure edit mode.
1901 const node = widget.editorWidget.node;
1902 if (node.contains(target)) {
1903 this.mode = 'edit';
1904 }
1905 this.activeCellIndex = index;
1906 }
1907 else {
1908 // No cell has focus, ensure command mode.
1909 this.mode = 'command';
1910 }
1911 }
1912 /**
1913 * Handle `focusout` events for the notebook.
1914 */
1915 _evtFocusOut(event) {
1916 const relatedTarget = event.relatedTarget;
1917 // Bail if the window is losing focus, to preserve edit mode. This test
1918 // assumes that we explicitly focus things rather than calling blur()
1919 if (!relatedTarget) {
1920 return;
1921 }
1922 // Bail if the item gaining focus is another cell,
1923 // and we should not be entering command mode.
1924 const index = this._findCell(relatedTarget);
1925 if (index !== -1) {
1926 const widget = this.widgets[index];
1927 if (widget.editorWidget.node.contains(relatedTarget)) {
1928 return;
1929 }
1930 }
1931 // Otherwise enter command mode if not already.
1932 if (this.mode !== 'command') {
1933 this.mode = 'command';
1934 // Switching to command mode currently focuses the notebook element, so
1935 // refocus the relatedTarget so the focus actually switches as intended.
1936 if (relatedTarget) {
1937 relatedTarget.focus();
1938 }
1939 }
1940 }
1941 /**
1942 * Handle `dblclick` events for the widget.
1943 */
1944 _evtDblClick(event) {
1945 const model = this.model;
1946 if (!model) {
1947 return;
1948 }
1949 this.deselectAll();
1950 const [target, index] = this._findEventTargetAndCell(event);
1951 if (event.target.classList.contains(HEADING_COLLAPSER_CLASS)) {
1952 return;
1953 }
1954 if (index === -1) {
1955 return;
1956 }
1957 this.activeCellIndex = index;
1958 if (model.cells.get(index).type === 'markdown') {
1959 const widget = this.widgets[index];
1960 widget.rendered = false;
1961 }
1962 else if (target.localName === 'img') {
1963 target.classList.toggle(UNCONFINED_CLASS);
1964 }
1965 }
1966 /**
1967 * Remove selections from inactive cells to avoid
1968 * spurious cursors.
1969 */
1970 _trimSelections() {
1971 for (let i = 0; i < this.widgets.length; i++) {
1972 if (i !== this._activeCellIndex) {
1973 const cell = this.widgets[i];
1974 cell.model.selections.delete(cell.editor.uuid);
1975 }
1976 }
1977 }
1978}
1979/**
1980 * The namespace for the `Notebook` class statics.
1981 */
1982(function (Notebook) {
1983 /**
1984 * The default implementation of a notebook content factory..
1985 *
1986 * #### Notes
1987 * Override methods on this class to customize the default notebook factory
1988 * methods that create notebook content.
1989 */
1990 class ContentFactory extends StaticNotebook.ContentFactory {
1991 }
1992 Notebook.ContentFactory = ContentFactory;
1993 Notebook.defaultContentFactory = new ContentFactory();
1994})(Notebook || (Notebook = {}));
1995/**
1996 * A namespace for private data.
1997 */
1998var Private;
1999(function (Private) {
2000 /**
2001 * An attached property for the selected state of a cell.
2002 */
2003 Private.selectedProperty = new AttachedProperty({
2004 name: 'selected',
2005 create: () => false
2006 });
2007 /**
2008 * A custom panel layout for the notebook.
2009 */
2010 class NotebookPanelLayout extends PanelLayout {
2011 /**
2012 * A message handler invoked on an `'update-request'` message.
2013 *
2014 * #### Notes
2015 * This is a reimplementation of the base class method,
2016 * and is a no-op.
2017 */
2018 onUpdateRequest(msg) {
2019 // This is a no-op.
2020 }
2021 }
2022 Private.NotebookPanelLayout = NotebookPanelLayout;
2023 /**
2024 * Create a cell drag image.
2025 */
2026 function createDragImage(count, promptNumber, cellContent) {
2027 if (count > 1) {
2028 if (promptNumber !== '') {
2029 return VirtualDOM.realize(h.div(h.div({ className: DRAG_IMAGE_CLASS }, h.span({ className: CELL_DRAG_PROMPT_CLASS }, '[' + promptNumber + ']:'), h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)), h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')));
2030 }
2031 else {
2032 return VirtualDOM.realize(h.div(h.div({ className: DRAG_IMAGE_CLASS }, h.span({ className: CELL_DRAG_PROMPT_CLASS }), h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)), h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')));
2033 }
2034 }
2035 else {
2036 if (promptNumber !== '') {
2037 return VirtualDOM.realize(h.div(h.div({ className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` }, h.span({ className: CELL_DRAG_PROMPT_CLASS }, '[' + promptNumber + ']:'), h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent))));
2038 }
2039 else {
2040 return VirtualDOM.realize(h.div(h.div({ className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` }, h.span({ className: CELL_DRAG_PROMPT_CLASS }), h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent))));
2041 }
2042 }
2043 }
2044 Private.createDragImage = createDragImage;
2045 /**
2046 * Process the `IOptions` passed to the notebook widget.
2047 *
2048 * #### Notes
2049 * This defaults the content factory to that in the `Notebook` namespace.
2050 */
2051 function processNotebookOptions(options) {
2052 if (options.contentFactory) {
2053 return options;
2054 }
2055 else {
2056 return {
2057 rendermime: options.rendermime,
2058 languagePreference: options.languagePreference,
2059 contentFactory: Notebook.defaultContentFactory,
2060 mimeTypeService: options.mimeTypeService
2061 };
2062 }
2063 }
2064 Private.processNotebookOptions = processNotebookOptions;
2065})(Private || (Private = {}));
2066//# sourceMappingURL=widget.js.map
\No newline at end of file