UNPKG

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