UNPKG

@jupyterlab/notebook

Version:
1,431 lines 98.7 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { DOMUtils } from '@jupyterlab/apputils'; import { Cell, CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells'; import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; import { TableOfContentsUtils } from '@jupyterlab/toc'; import { nullTranslator } from '@jupyterlab/translation'; import { WindowedList } from '@jupyterlab/ui-components'; import { ArrayExt, findIndex } from '@lumino/algorithm'; import { MimeData } from '@lumino/coreutils'; import { ElementExt } from '@lumino/domutils'; import { Drag } from '@lumino/dragdrop'; import { AttachedProperty } from '@lumino/properties'; import { Signal } from '@lumino/signaling'; import { h, VirtualDOM } from '@lumino/virtualdom'; import { PanelLayout, Widget } from '@lumino/widgets'; import { NotebookActions } from './actions'; import { DROP_SOURCE_CLASS, DROP_TARGET_CLASS } from './constants'; import { NotebookViewModel, NotebookWindowedLayout } from './windowing'; import { NotebookFooter } from './notebookfooter'; /** * The data attribute added to a widget that has an active kernel. */ const KERNEL_USER = 'jpKernelUser'; /** * The data attribute added to a widget that can run code. */ const CODE_RUNNER = 'jpCodeRunner'; /** * The data attribute added to a widget that can undo. */ const UNDOER = 'jpUndoer'; /** * The class name added to notebook widgets. */ const NB_CLASS = 'jp-Notebook'; /** * The class name added to notebook widget cells. */ const NB_CELL_CLASS = 'jp-Notebook-cell'; /** * The class name added to a notebook in edit mode. */ const EDIT_CLASS = 'jp-mod-editMode'; /** * The class name added to a notebook in command mode. */ const COMMAND_CLASS = 'jp-mod-commandMode'; /** * The class name added to the active cell. */ const ACTIVE_CLASS = 'jp-mod-active'; /** * The class name added to selected cells. */ const SELECTED_CLASS = 'jp-mod-selected'; /** * The class name added to the cell when dirty. */ const DIRTY_CLASS = 'jp-mod-dirty'; /** * The class name added to an active cell when there are other selected cells. */ const OTHER_SELECTED_CLASS = 'jp-mod-multiSelected'; /** * The class name added to unconfined images. */ const UNCONFINED_CLASS = 'jp-mod-unconfined'; /** * The class name added to the notebook when an element within it is focused * and takes keyboard input, such as focused <input> or <div contenteditable>. * * This class is also effective when the focused element is in shadow DOM. */ const READ_WRITE_CLASS = 'jp-mod-readWrite'; /** * The class name added to drag images. */ const DRAG_IMAGE_CLASS = 'jp-dragImage'; /** * The class name added to singular drag images */ const SINGLE_DRAG_IMAGE_CLASS = 'jp-dragImage-singlePrompt'; /** * The class name added to the drag image cell content. */ const CELL_DRAG_CONTENT_CLASS = 'jp-dragImage-content'; /** * The class name added to the drag image cell content. */ const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt'; /** * The class name added to the drag image cell content. */ const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack'; /** * The mimetype used for Jupyter cell data. */ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells'; /** * The threshold in pixels to start a drag event. */ const DRAG_THRESHOLD = 5; /** * Maximal remaining time for idle callback * * Ref: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#getting_the_most_out_of_idle_callbacks */ const MAXIMUM_TIME_REMAINING = 50; /** * The class attached to the heading collapser button */ const HEADING_COLLAPSER_CLASS = 'jp-collapseHeadingButton'; /** * The class that controls the visibility of "heading collapser" and "show hidden cells" buttons. */ const HEADING_COLLAPSER_VISBILITY_CONTROL_CLASS = 'jp-mod-showHiddenCellsButton'; const SIDE_BY_SIDE_CLASS = 'jp-mod-sideBySide'; if (window.requestIdleCallback === undefined) { // On Safari, requestIdleCallback is not available, so we use replacement functions for `idleCallbacks` // See: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#falling_back_to_settimeout // eslint-disable-next-line @typescript-eslint/ban-types window.requestIdleCallback = function (handler) { let startTime = Date.now(); return setTimeout(function () { handler({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50.0 - (Date.now() - startTime)); } }); }, 1); }; window.cancelIdleCallback = function (id) { clearTimeout(id); }; } /** * A widget which renders static non-interactive notebooks. * * #### Notes * The widget model must be set separately and can be changed * at any time. Consumers of the widget must account for a * `null` model, and may want to listen to the `modelChanged` * signal. */ export class StaticNotebook extends WindowedList { /** * Construct a notebook widget. */ constructor(options) { var _a, _b, _c, _d, _e, _f; const cells = new Array(); const windowingActive = ((_b = (_a = options.notebookConfig) === null || _a === void 0 ? void 0 : _a.windowingMode) !== null && _b !== void 0 ? _b : StaticNotebook.defaultNotebookConfig.windowingMode) === 'full'; super({ model: new NotebookViewModel(cells, { overscanCount: (_d = (_c = options.notebookConfig) === null || _c === void 0 ? void 0 : _c.overscanCount) !== null && _d !== void 0 ? _d : StaticNotebook.defaultNotebookConfig.overscanCount, windowingActive }), layout: new NotebookWindowedLayout(), renderer: (_e = options.renderer) !== null && _e !== void 0 ? _e : WindowedList.defaultRenderer, scrollbar: false }); this._cellCollapsed = new Signal(this); this._cellInViewportChanged = new Signal(this); this._renderingLayoutChanged = new Signal(this); this.addClass(NB_CLASS); this.cellsArray = cells; this._idleCallBack = null; this._editorConfig = StaticNotebook.defaultEditorConfig; this._notebookConfig = StaticNotebook.defaultNotebookConfig; this._mimetype = IEditorMimeTypeService.defaultMimeType; this._notebookModel = null; this._modelChanged = new Signal(this); this._modelContentChanged = new Signal(this); this.node.dataset[KERNEL_USER] = 'true'; this.node.dataset[UNDOER] = 'true'; this.node.dataset[CODE_RUNNER] = 'true'; this.rendermime = options.rendermime; this.translator = options.translator || nullTranslator; this.contentFactory = options.contentFactory; this.editorConfig = options.editorConfig || StaticNotebook.defaultEditorConfig; this.notebookConfig = options.notebookConfig || StaticNotebook.defaultNotebookConfig; this._updateNotebookConfig(); this._mimetypeService = options.mimeTypeService; this.renderingLayout = (_f = options.notebookConfig) === null || _f === void 0 ? void 0 : _f.renderingLayout; this.kernelHistory = options.kernelHistory; } get cellCollapsed() { return this._cellCollapsed; } get cellInViewportChanged() { return this._cellInViewportChanged; } /** * A signal emitted when the model of the notebook changes. */ get modelChanged() { return this._modelChanged; } /** * A signal emitted when the model content changes. * * #### Notes * This is a convenience signal that follows the current model. */ get modelContentChanged() { return this._modelContentChanged; } /** * A signal emitted when the rendering layout of the notebook changes. */ get renderingLayoutChanged() { return this._renderingLayoutChanged; } /** * The model for the widget. */ get model() { return this._notebookModel; } set model(newValue) { var _a; newValue = newValue || null; if (this._notebookModel === newValue) { return; } const oldValue = this._notebookModel; this._notebookModel = newValue; // Trigger private, protected, and public changes. this._onModelChanged(oldValue, newValue); this.onModelChanged(oldValue, newValue); this._modelChanged.emit(void 0); // Trigger state change this.viewModel.itemsList = (_a = newValue === null || newValue === void 0 ? void 0 : newValue.cells) !== null && _a !== void 0 ? _a : null; } /** * Get the mimetype for code cells. */ get codeMimetype() { return this._mimetype; } /** * A read-only sequence of the widgets in the notebook. */ get widgets() { return this.cellsArray; } /** * A configuration object for cell editor settings. */ get editorConfig() { return this._editorConfig; } set editorConfig(value) { this._editorConfig = value; this._updateEditorConfig(); } /** * A configuration object for notebook settings. */ get notebookConfig() { return this._notebookConfig; } set notebookConfig(value) { this._notebookConfig = value; this._updateNotebookConfig(); } get renderingLayout() { return this._renderingLayout; } set renderingLayout(value) { var _a; this._renderingLayout = value; if (this._renderingLayout === 'side-by-side') { this.node.classList.add(SIDE_BY_SIDE_CLASS); } else { this.node.classList.remove(SIDE_BY_SIDE_CLASS); } this._renderingLayoutChanged.emit((_a = this._renderingLayout) !== null && _a !== void 0 ? _a : 'default'); } /** * Dispose of the resources held by the widget. */ dispose() { var _a; // Do nothing if already disposed. if (this.isDisposed) { return; } this._notebookModel = null; (_a = this.layout.header) === null || _a === void 0 ? void 0 : _a.dispose(); super.dispose(); } /** * Move cells preserving widget view state. * * #### Notes * This is required because at the model level a move is a deletion * followed by an insertion. Hence the view state is not preserved. * * @param from The index of the cell to move * @param to The new index of the cell * @param n Number of cells to move */ moveCell(from, to, n = 1) { if (!this.model) { return; } const boundedTo = Math.min(this.model.cells.length - 1, Math.max(0, to)); if (boundedTo === from) { return; } const viewModel = new Array(n); let dirtyState = new Array(n); for (let i = 0; i < n; i++) { viewModel[i] = {}; const oldCell = this.widgets[from + i]; if (oldCell.model.type === 'markdown') { for (const k of ['rendered', 'headingCollapsed']) { // @ts-expect-error Cell has no index signature viewModel[i][k] = oldCell[k]; } } else if (oldCell.model.type === 'code') { const oldCodeCell = oldCell.model; dirtyState[i] = oldCodeCell.isDirty; } } this.model.sharedModel.moveCells(from, boundedTo, n); for (let i = 0; i < n; i++) { const newCell = this.widgets[to + i]; const view = viewModel[i]; for (const state in view) { // @ts-expect-error Cell has no index signature newCell[state] = view[state]; } if (from > to) { if (this.widgets[to + i].model.type === 'code') { this.widgets[to + i].model.isDirty = dirtyState[i]; } } else { if (this.widgets[to + i - n + 1].model.type === 'code') { this.widgets[to + i - n + 1].model.isDirty = dirtyState[i]; } } } } /** * Force rendering the cell outputs of a given cell if it is still a placeholder. * * #### Notes * The goal of this method is to allow search on cell outputs (that is based * on DOM tree introspection). * * @param index The cell index */ renderCellOutputs(index) { const cell = this.viewModel.widgetRenderer(index); if (cell instanceof CodeCell && cell.isPlaceholder()) { cell.dataset.windowedListIndex = `${index}`; this.layout.insertWidget(index, cell); if (this.notebookConfig.windowingMode === 'full') { // We need to delay slightly the removal to let codemirror properly initialize requestAnimationFrame(() => { this.layout.removeWidget(cell); }); } } } /** * Adds a message to the notebook as a header. */ addHeader() { const trans = this.translator.load('jupyterlab'); const info = new Widget(); info.node.textContent = trans.__('The notebook is empty. Click the + button on the toolbar to add a new cell.'); this.layout.header = info; } /** * Removes the header. */ removeHeader() { var _a; (_a = this.layout.header) === null || _a === void 0 ? void 0 : _a.dispose(); this.layout.header = null; } /** * Handle a new model. * * #### Notes * This method is called after the model change has been handled * internally and before the `modelChanged` signal is emitted. * The default implementation is a no-op. */ onModelChanged(oldValue, newValue) { // No-op. } /** * Handle changes to the notebook model content. * * #### Notes * The default implementation emits the `modelContentChanged` signal. */ onModelContentChanged(model, args) { this._modelContentChanged.emit(void 0); } /** * Handle changes to the notebook model metadata. * * #### Notes * The default implementation updates the mimetypes of the code cells * when the `language_info` metadata changes. */ onMetadataChanged(sender, args) { switch (args.key) { case 'language_info': this._updateMimetype(); break; default: break; } } /** * Handle a cell being inserted. * * The default implementation is a no-op */ onCellInserted(index, cell) { // This is a no-op. } /** * Handle a cell being removed. * * The default implementation is a no-op */ onCellRemoved(index, cell) { // This is a no-op. } /** * A message handler invoked on an `'update-request'` message. * * #### Notes * The default implementation of this handler is a no-op. */ onUpdateRequest(msg) { if (this.notebookConfig.windowingMode === 'defer') { void this._runOnIdleTime(); } else { super.onUpdateRequest(msg); } } /** * Handle a new model on the widget. */ _onModelChanged(oldValue, newValue) { var _a; if (oldValue) { oldValue.contentChanged.disconnect(this.onModelContentChanged, this); oldValue.metadataChanged.disconnect(this.onMetadataChanged, this); oldValue.cells.changed.disconnect(this._onCellsChanged, this); while (this.cellsArray.length) { this._removeCell(0); } } if (!newValue) { this._mimetype = IEditorMimeTypeService.defaultMimeType; return; } this._updateMimetype(); const cells = newValue.cells; const collab = (_a = newValue.collaborative) !== null && _a !== void 0 ? _a : false; if (!collab && !cells.length) { newValue.sharedModel.insertCell(0, { cell_type: this.notebookConfig.defaultCell, metadata: this.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created in empty notebook, thus is trusted trusted: true } : {} }); } let index = -1; for (const cell of cells) { this._insertCell(++index, cell); } newValue.cells.changed.connect(this._onCellsChanged, this); newValue.metadataChanged.connect(this.onMetadataChanged, this); newValue.contentChanged.connect(this.onModelContentChanged, this); } /** * Handle a change cells event. */ _onCellsChanged(sender, args) { this.removeHeader(); switch (args.type) { case 'add': { let index = 0; index = args.newIndex; for (const value of args.newValues) { this._insertCell(index++, value); } this._updateDataWindowedListIndex(args.newIndex, this.model.cells.length, args.newValues.length); break; } case 'remove': for (let length = args.oldValues.length; length > 0; length--) { this._removeCell(args.oldIndex); } this._updateDataWindowedListIndex(args.oldIndex, this.model.cells.length + args.oldValues.length, -1 * args.oldValues.length); // Add default cell if there are no cells remaining. if (!sender.length) { const model = this.model; // Add the cell in a new context to avoid triggering another // cell changed event during the handling of this signal. requestAnimationFrame(() => { if (model && !model.isDisposed && !model.sharedModel.cells.length) { model.sharedModel.insertCell(0, { cell_type: this.notebookConfig.defaultCell, metadata: this.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created in empty notebook, thus is trusted trusted: true } : {} }); } }); } break; default: return; } if (!this.model.sharedModel.cells.length) { this.addHeader(); } this.update(); } /** * Create a cell widget and insert into the notebook. */ _insertCell(index, cell) { let widget; switch (cell.type) { case 'code': widget = this._createCodeCell(cell); widget.model.mimeType = this._mimetype; break; case 'markdown': widget = this._createMarkdownCell(cell); if (cell.sharedModel.getSource() === '') { widget.rendered = false; } break; default: widget = this._createRawCell(cell); } widget.inViewportChanged.connect(this._onCellInViewportChanged, this); widget.addClass(NB_CELL_CLASS); ArrayExt.insert(this.cellsArray, index, widget); this.onCellInserted(index, widget); this._scheduleCellRenderOnIdle(); } /** * Create a code cell widget from a code cell model. */ _createCodeCell(model) { const rendermime = this.rendermime; const contentFactory = this.contentFactory; const editorConfig = this.editorConfig.code; const options = { contentFactory, editorConfig, inputHistoryScope: this.notebookConfig.inputHistoryScope, showInputPlaceholder: this.notebookConfig.showInputPlaceholder, maxNumberOutputs: this.notebookConfig.maxNumberOutputs, model, placeholder: this._notebookConfig.windowingMode !== 'none', rendermime, translator: this.translator }; const cell = this.contentFactory.createCodeCell(options); cell.syncCollapse = true; cell.syncEditable = true; cell.syncScrolled = true; cell.outputArea.inputRequested.connect((_, stdin) => { this._onInputRequested(cell).catch(reason => { console.error('Failed to scroll to cell requesting input.', reason); }); stdin.disposed.connect(() => { // The input field is removed from the DOM after the user presses Enter. // This causes focus to be lost if we don't explicitly re-focus // somewhere else. cell.node.focus(); }); }); return cell; } /** * Create a markdown cell widget from a markdown cell model. */ _createMarkdownCell(model) { const rendermime = this.rendermime; const contentFactory = this.contentFactory; const editorConfig = this.editorConfig.markdown; const options = { contentFactory, editorConfig, model, placeholder: this._notebookConfig.windowingMode !== 'none', rendermime, showEditorForReadOnlyMarkdown: this._notebookConfig.showEditorForReadOnlyMarkdown }; const cell = this.contentFactory.createMarkdownCell(options); cell.syncCollapse = true; cell.syncEditable = true; // Connect collapsed signal for each markdown cell widget cell.headingCollapsedChanged.connect(this._onCellCollapsed, this); return cell; } /** * Create a raw cell widget from a raw cell model. */ _createRawCell(model) { const contentFactory = this.contentFactory; const editorConfig = this.editorConfig.raw; const options = { editorConfig, model, contentFactory, placeholder: this._notebookConfig.windowingMode !== 'none' }; const cell = this.contentFactory.createRawCell(options); cell.syncCollapse = true; cell.syncEditable = true; return cell; } /** * Remove a cell widget. */ _removeCell(index) { const widget = this.cellsArray[index]; widget.parent = null; ArrayExt.removeAt(this.cellsArray, index); this.onCellRemoved(index, widget); widget.dispose(); } /** * Update the mimetype of the notebook. */ _updateMimetype() { var _a; const info = (_a = this._notebookModel) === null || _a === void 0 ? void 0 : _a.getMetadata('language_info'); if (!info) { return; } this._mimetype = this._mimetypeService.getMimeTypeByLanguage(info); for (const widget of this.widgets) { if (widget.model.type === 'code') { widget.model.mimeType = this._mimetype; } } } /** * Callback when a cell collapsed status changes. * * @param cell Cell changed * @param collapsed New collapsed status */ _onCellCollapsed(cell, collapsed) { NotebookActions.setHeadingCollapse(cell, collapsed, this); this._cellCollapsed.emit(cell); } /** * Callback when a cell viewport status changes. * * @param cell Cell changed */ _onCellInViewportChanged(cell) { this._cellInViewportChanged.emit(cell); } /** * Ensure to load in the DOM a cell requesting an user input * * @param cell Cell requesting an input */ async _onInputRequested(cell) { if (!cell.inViewport) { const cellIndex = this.widgets.findIndex(c => c === cell); if (cellIndex >= 0) { await this.scrollToItem(cellIndex); const inputEl = cell.node.querySelector('.jp-Stdin'); if (inputEl) { ElementExt.scrollIntoViewIfNeeded(this.node, inputEl); inputEl.focus(); } } } } _scheduleCellRenderOnIdle() { if (this.notebookConfig.windowingMode !== 'none' && !this.isDisposed) { if (!this._idleCallBack) { this._idleCallBack = requestIdleCallback((deadline) => { this._idleCallBack = null; // In case of timeout, render for some time even if it means freezing the UI // This avoids the cells to never be loaded. void this._runOnIdleTime(deadline.didTimeout ? MAXIMUM_TIME_REMAINING : deadline.timeRemaining()); }, { timeout: 3000 }); } } } _updateDataWindowedListIndex(start, end, delta) { for (let cellIdx = 0; cellIdx < this.viewportNode.childElementCount; cellIdx++) { const cell = this.viewportNode.children[cellIdx]; const globalIndex = parseInt(cell.dataset.windowedListIndex, 10); if (globalIndex >= start && globalIndex < end) { cell.dataset.windowedListIndex = `${globalIndex + delta}`; } } } /** * Update editor settings for notebook cells. */ _updateEditorConfig() { for (let i = 0; i < this.widgets.length; i++) { const cell = this.widgets[i]; let config = {}; switch (cell.model.type) { case 'code': config = this._editorConfig.code; break; case 'markdown': config = this._editorConfig.markdown; break; default: config = this._editorConfig.raw; break; } cell.updateEditorConfig({ ...config }); } } async _runOnIdleTime(remainingTime = MAXIMUM_TIME_REMAINING) { const startTime = Date.now(); let cellIdx = 0; while (Date.now() - startTime < remainingTime && cellIdx < this.cellsArray.length) { const cell = this.cellsArray[cellIdx]; if (cell.isPlaceholder()) { if (['defer', 'full'].includes(this.notebookConfig.windowingMode)) { await this._updateForDeferMode(cell, cellIdx); if (this.notebookConfig.windowingMode === 'full') { // We need to delay slightly the removal to let codemirror properly initialize requestAnimationFrame(() => { this.viewModel.setEstimatedWidgetSize(cell.model.id, cell.node.getBoundingClientRect().height); this.layout.removeWidget(cell); }); } } } cellIdx++; } // If the notebook is not fully rendered if (cellIdx < this.cellsArray.length) { // If we are deferring the cell rendering and the rendered cells do // not fill the viewport yet if (this.notebookConfig.windowingMode === 'defer' && this.viewportNode.clientHeight < this.node.clientHeight) { // Spend more time rendering cells to fill the viewport await this._runOnIdleTime(); } else { this._scheduleCellRenderOnIdle(); } } else { if (this._idleCallBack) { window.cancelIdleCallback(this._idleCallBack); this._idleCallBack = null; } } } async _updateForDeferMode(cell, cellIdx) { cell.dataset.windowedListIndex = `${cellIdx}`; this.layout.insertWidget(cellIdx, cell); await cell.ready; } /** * Apply updated notebook settings. */ _updateNotebookConfig() { // Apply scrollPastEnd setting. this.toggleClass('jp-mod-scrollPastEnd', this._notebookConfig.scrollPastEnd); // Control visibility of heading collapser UI this.toggleClass(HEADING_COLLAPSER_VISBILITY_CONTROL_CLASS, this._notebookConfig.showHiddenCellsButton); // Control editor visibility for read-only Markdown cells const showEditorForReadOnlyMarkdown = this._notebookConfig.showEditorForReadOnlyMarkdown; if (showEditorForReadOnlyMarkdown !== undefined) { for (const cell of this.cellsArray) { if (cell.model.type === 'markdown') { cell.showEditorForReadOnly = showEditorForReadOnlyMarkdown; } } } this.viewModel.windowingActive = this._notebookConfig.windowingMode === 'full'; } } /** * The namespace for the `StaticNotebook` class statics. */ (function (StaticNotebook) { /** * Default configuration options for cell editors. */ StaticNotebook.defaultEditorConfig = { code: { lineNumbers: false, lineWrap: false, matchBrackets: true, tabFocusable: false }, markdown: { lineNumbers: false, lineWrap: true, matchBrackets: false, tabFocusable: false }, raw: { lineNumbers: false, lineWrap: true, matchBrackets: false, tabFocusable: false } }; /** * Default configuration options for notebooks. */ StaticNotebook.defaultNotebookConfig = { enableKernelInitNotification: false, showHiddenCellsButton: true, scrollPastEnd: true, defaultCell: 'code', recordTiming: false, inputHistoryScope: 'global', maxNumberOutputs: 50, showEditorForReadOnlyMarkdown: true, disableDocumentWideUndoRedo: true, autoRenderMarkdownCells: false, renderingLayout: 'default', sideBySideLeftMarginOverride: '10px', sideBySideRightMarginOverride: '10px', sideBySideOutputRatio: 1, overscanCount: 1, windowingMode: 'full', accessKernelHistory: false, showInputPlaceholder: true }; /** * The default implementation of an `IContentFactory`. */ class ContentFactory extends Cell.ContentFactory { /** * Create a new code cell widget. * * #### Notes * If no cell content factory is passed in with the options, the one on the * notebook content factory is used. */ createCodeCell(options) { return new CodeCell(options).initializeState(); } /** * Create a new markdown cell widget. * * #### Notes * If no cell content factory is passed in with the options, the one on the * notebook content factory is used. */ createMarkdownCell(options) { return new MarkdownCell(options).initializeState(); } /** * Create a new raw cell widget. * * #### Notes * If no cell content factory is passed in with the options, the one on the * notebook content factory is used. */ createRawCell(options) { return new RawCell(options).initializeState(); } } StaticNotebook.ContentFactory = ContentFactory; })(StaticNotebook || (StaticNotebook = {})); /** * A virtual scrollbar item representing a notebook cell. */ class ScrollbarItem { /** * Construct a scrollbar item. */ constructor(options) { /** * Render the scrollbar item as an HTML element. */ this.render = (props) => { if (!this._element) { this._element = this._createElement(); this._notebook.activeCellChanged.connect(this._updateActive); this._notebook.selectionChanged.connect(this._updateSelection); if (this._model.type === 'code') { const model = this._model; model.outputs.changed.connect(this._updatePrompt); model.stateChanged.connect(this._updateState); } } // Add cell type (code/markdown/raw) if (this._model.type != this._element.dataset.type) { this._element.dataset.type = this._model.type; } const source = this._model.sharedModel.source; const trimmedSource = source.length > 10000 ? source.substring(0, 10000) : source; if (trimmedSource !== this._source.textContent) { this._source.textContent = trimmedSource; } this._updateActive(); this._updateSelection(); this._updatePrompt(); this._updateDirty(); return this._element; }; /** * Dispose of the resources held by the item. */ this.dispose = () => { this._isDisposed = true; this._notebook.activeCellChanged.disconnect(this._updateActive); this._notebook.selectionChanged.disconnect(this._updateSelection); if (this._model.type === 'code') { const model = this._model; if (model.outputs) { model.outputs.changed.disconnect(this._updatePrompt); model.stateChanged.disconnect(this._updateState); } } }; this._updateState = (_, change) => { switch (change.name) { case 'executionCount': case 'executionState': this._updatePrompt(); break; case 'isDirty': { this._updateDirty(); break; } } }; this._updatePrompt = () => { if (this._model.type !== 'code') { return; } const model = this._model; let hasError = false; for (let i = 0; i < model.outputs.length; i++) { const output = model.outputs.get(i); if (output.type === 'error') { hasError = true; break; } } let content; let state = ''; if (hasError) { content = '[!]'; state = 'error'; } else if (model.executionState == 'running') { content = '[*]'; } else if (model.executionCount) { content = `[${model.executionCount}]`; } else { content = '[ ]'; } if (this._executionIndicator.textContent !== content) { this._executionIndicator.textContent = content; } if (this._element.dataset.output !== state) { this._element.dataset.output = state; } }; this._updateActive = () => { var _a; if (!this._element) { this._element = this._createElement(); } const li = this._element; const wasActive = li.classList.contains(ACTIVE_CLASS); if (((_a = this._notebook.activeCell) === null || _a === void 0 ? void 0 : _a.model) === this._model) { if (!wasActive) { li.classList.add(ACTIVE_CLASS); } } else if (wasActive) { li.classList.remove(ACTIVE_CLASS); // Needed due to order in which selection and active changed signals fire li.classList.remove(SELECTED_CLASS); } }; this._updateSelection = () => { if (!this._element) { this._element = this._createElement(); } const li = this._element; const wasSelected = li.classList.contains(SELECTED_CLASS); if (this._notebook.selectedCells.some(cell => this._model === cell.model)) { if (!wasSelected) { li.classList.add(SELECTED_CLASS); } } else if (wasSelected) { li.classList.remove(SELECTED_CLASS); } }; this._isDisposed = false; this._element = null; // Note: there should be no DOM operations in the constructor this._model = options.model; this._notebook = options.notebook; } /** * Unique item key used for caching. */ get key() { return this._model.id; } /** * Test whether the item has been disposed. */ get isDisposed() { // Ensure the state is up-to-date in case if the model was disposed // (the model can be disposed when cells are moved/recreated). if (!this._isDisposed && this._model.isDisposed) { this.dispose(); } return this._isDisposed; } _updateDirty() { if (this._model.type !== 'code' || !this._element) { return; } const model = this._model; const wasDirty = this._element.classList.contains(DIRTY_CLASS); if (wasDirty !== model.isDirty) { if (model.isDirty) { this._element.classList.add(DIRTY_CLASS); } else { this._element.classList.remove(DIRTY_CLASS); } } } _createElement() { const li = document.createElement('li'); const executionIndicator = (this._executionIndicator = document.createElement('div')); executionIndicator.className = 'jp-scrollbarItem-executionIndicator'; const source = (this._source = document.createElement('div')); source.className = 'jp-scrollbarItem-source'; li.append(executionIndicator); li.append(source); return li; } } /** * A notebook widget that supports interactivity. */ export class Notebook extends StaticNotebook { /** * Construct a notebook widget. */ constructor(options) { super({ renderer: { createOuter() { return document.createElement('div'); }, createViewport() { const el = document.createElement('div'); el.setAttribute('role', 'feed'); el.setAttribute('aria-label', 'Cells'); return el; }, createScrollbar() { return document.createElement('ol'); }, createScrollbarViewportIndicator() { return document.createElement('div'); }, createScrollbarItem(notebook, _index, model) { return new ScrollbarItem({ notebook, model }); } }, ...options }); this._activeCellIndex = -1; this._activeCell = null; this._mode = 'command'; this._drag = null; this._dragData = null; this._selectData = null; this._mouseMode = null; this._activeCellChanged = new Signal(this); this._stateChanged = new Signal(this); this._selectionChanged = new Signal(this); this._checkCacheOnNextResize = false; this._lastClipboardInteraction = null; this._selectedCells = []; // Allow the node to scroll while dragging items. this.outerNode.setAttribute('data-lm-dragscroll', 'true'); this.activeCellChanged.connect(this._updateSelectedCells, this); this.jumped.connect((_, index) => (this.activeCellIndex = index)); this.selectionChanged.connect(this._updateSelectedCells, this); this.addFooter(); } /** * List of selected and active cells */ get selectedCells() { return this._selectedCells; } /** * Adds a footer to the notebook. */ addFooter() { const info = new NotebookFooter(this); this.layout.footer = info; } /** * Handle a change cells event. */ _onCellsChanged(sender, args) { var _a, _b; const activeCellId = (_a = this.activeCell) === null || _a === void 0 ? void 0 : _a.model.id; super._onCellsChanged(sender, args); if (activeCellId) { const newActiveCellIndex = (_b = this.model) === null || _b === void 0 ? void 0 : _b.sharedModel.cells.findIndex(cell => cell.getId() === activeCellId); if (newActiveCellIndex != null) { this.activeCellIndex = newActiveCellIndex; } } } /** * A signal emitted when the active cell changes. * * #### Notes * This can be due to the active index changing or the * cell at the active index changing. */ get activeCellChanged() { return this._activeCellChanged; } /** * A signal emitted when the state of the notebook changes. */ get stateChanged() { return this._stateChanged; } /** * A signal emitted when the selection state of the notebook changes. */ get selectionChanged() { return this._selectionChanged; } /** * The interactivity mode of the notebook. */ get mode() { return this._mode; } set mode(newValue) { this.setMode(newValue); } /** * Set the notebook mode. * * @param newValue Notebook mode * @param options Control mode side-effect * @param options.focus Whether to ensure focus (default) or not when setting the mode. */ setMode(newValue, options = {}) { var _a; const setFocus = (_a = options.focus) !== null && _a !== void 0 ? _a : true; const activeCell = this.activeCell; if (!activeCell) { newValue = 'command'; } if (newValue === this._mode) { if (setFocus) { this._ensureFocus(); } return; } // Post an update request. this.update(); const oldValue = this._mode; this._mode = newValue; if (newValue === 'edit') { // Edit mode deselects all cells. for (const widget of this.widgets) { this.deselect(widget); } // Edit mode unrenders an active markdown widget. if (activeCell instanceof MarkdownCell) { activeCell.rendered = false; } activeCell.inputHidden = false; } else { if (setFocus) { void NotebookActions.focusActiveCell(this, { // Do not await the active cell because that creates a bug. If the user // is editing a code cell and presses Accel Shift C to open the command // palette, then the command palette opens before // activeCell.node.focus() is called, which closes the command palette. // To the end user, it looks as if all the keyboard shortcut did was // move focus from the cell editor to the cell as a whole. waitUntilReady: false, preventScroll: true }); } } this._stateChanged.emit({ name: 'mode', oldValue, newValue }); if (setFocus) { this._ensureFocus(); } } /** * The active cell index of the notebook. * * #### Notes * The index will be clamped to the bounds of the notebook cells. */ get activeCellIndex() { if (!this.model) { return -1; } return this.widgets.length ? this._activeCellIndex : -1; } set activeCellIndex(newValue) { var _a, _b; const oldValue = this._activeCellIndex; if (!this.model || !this.widgets.length) { newValue = -1; } else { newValue = Math.max(newValue, 0); newValue = Math.min(newValue, this.widgets.length - 1); } this._activeCellIndex = newValue; const oldCell = (_a = this.widgets[oldValue]) !== null && _a !== void 0 ? _a : null; const cell = (_b = this.widgets[newValue]) !== null && _b !== void 0 ? _b : null; this.layout.activeCell = cell; const cellChanged = cell !== this._activeCell; if (cellChanged) { // Post an update request. this.update(); this._activeCell = cell; } if (cellChanged || newValue != oldValue) { this._activeCellChanged.emit(cell); } if (this.mode === 'edit') { if (cell instanceof MarkdownCell) { cell.rendered = false; } if (this.notebookConfig.autoRenderMarkdownCells && cellChanged && oldCell instanceof MarkdownCell) { oldCell.rendered = true; } } this._ensureFocus(); if (newValue === oldValue) { return; } this._trimSelections(); this._stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue }); } /** * Get the active cell widget. * * #### Notes * This is a cell or `null` if there is no active cell. */ get activeCell() { return this._activeCell; } get lastClipboardInteraction() { return this._lastClipboardInteraction; } set lastClipboardInteraction(newValue) { this._lastClipboardInteraction = newValue; } /** * Dispose of the resources held by the widget. */ dispose() { if (this.isDisposed) { return; } this._activeCell = null; super.dispose(); } /** * Move cells preserving widget view state. * * #### Notes * This is required because at the model level a move is a deletion * followed by an insertion. Hence the view state is not preserved. * * @param from The index of the cell to move * @param to The new index of the cell * @param n Number of cells to move */ moveCell(from, to, n = 1) { // Save active cell id to be restored const newActiveCellIndex = from <= this.activeCellIndex && this.activeCellIndex < from + n ? this.activeCellIndex + to - from - (from > to ? 0 : n - 1) : -1; const isSelected = this.widgets .slice(from, from + n) .map(w => this.isSelected(w)); super.moveCell(from, to, n); if (newActiveCellIndex >= 0) { this.activeCellIndex = newActiveCellIndex; } if (from > to) { isSelected.forEach((selected, idx) => { if (selected) { this.select(this.widgets[to + idx]); } }); } else { isSelected.forEach((selected, idx) => { if (selected) { this.select(this.widgets[to - n + 1 + idx]); } }); } } /** * Select a cell widget. * * #### Notes * It is a no-op if the value does not change. * It will emit the `selectionChanged` signal. */ select(widget) { if (Private.selectedProperty.get(widget)) { return; } Private.selectedProperty.set(widget, true); this._selectionChanged.emit(void 0); this.update(); } /** * Deselect a cell widget. * * #### Notes * It is a no-op if the value does not change. * It will emit the `selectionChanged` signal. */ deselect(widget) { if (!Private.selectedProperty.get(widget)) { return; } Private.selectedProperty.set(widget, false); this._selectionChanged.emit(void 0); this.update(); } /** * Whether a cell is selected. */ isSelected(widget) { return Private.selectedProperty.get(widget); } /** * Whether a cell is selected or is the active cell. */ isSelectedOrActive(widget) { if (widget === this._activeCell) { return true; } return Private.selectedProperty.get(widget); } /** * Deselect all of the cells. */ deselectAll() { let changed = false; for (const widget of this.widgets) { if (Private.selectedProperty.get(widget)) { changed = true; } Private.selectedProperty.set(widget, false); } if (changed) { this._selectionChanged.emit(void 0); } // Make sure we have a valid active cell. this.activeCellIndex = this.activeCellIndex; // eslint-disable-line thi