|
@@ -144,6 +144,11 @@ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
|
|
|
*/
|
|
|
const DRAG_THRESHOLD = 5;
|
|
|
|
|
|
+/*
|
|
|
+ * The type of cell insert provided via signal.
|
|
|
+ */
|
|
|
+type InsertType = 'push' | 'insert' | 'set';
|
|
|
+
|
|
|
/**
|
|
|
* The interactivity modes for the notebook.
|
|
|
*/
|
|
@@ -152,19 +157,19 @@ export type NotebookMode = 'command' | 'edit';
|
|
|
if ((window as any).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
|
|
|
- (window as any).requestIdleCallback = function(handler: Function) {
|
|
|
+ (window as any).requestIdleCallback = function (handler: Function) {
|
|
|
let startTime = Date.now();
|
|
|
- return setTimeout(function() {
|
|
|
+ return setTimeout(function () {
|
|
|
handler({
|
|
|
didTimeout: false,
|
|
|
- timeRemaining: function() {
|
|
|
+ timeRemaining: function () {
|
|
|
return Math.max(0, 50.0 - (Date.now() - startTime));
|
|
|
}
|
|
|
});
|
|
|
}, 1);
|
|
|
};
|
|
|
|
|
|
- (window as any).cancelIdleCallback = function(id: number) {
|
|
|
+ (window as any).cancelIdleCallback = function (id: number) {
|
|
|
clearTimeout(id);
|
|
|
};
|
|
|
}
|
|
@@ -196,6 +201,46 @@ export class StaticNotebook extends Widget {
|
|
|
this.notebookConfig =
|
|
|
options.notebookConfig || StaticNotebook.defaultNotebookConfig;
|
|
|
this._mimetypeService = options.mimeTypeService;
|
|
|
+
|
|
|
+ // Section for the virtual-notebook behavior.
|
|
|
+ this._toRenderMap = new Map<string, { index: number; cell: Cell }>();
|
|
|
+ this._cellsArray = new Array<Cell>();
|
|
|
+ if ('IntersectionObserver' in window) {
|
|
|
+ this._observer = new IntersectionObserver(
|
|
|
+ (entries, observer) => {
|
|
|
+ entries.forEach(o => {
|
|
|
+ if (o.isIntersecting) {
|
|
|
+ observer.unobserve(o.target);
|
|
|
+ const ci = this._toRenderMap.get(o.target.id);
|
|
|
+ if (ci) {
|
|
|
+ const { cell, index } = ci;
|
|
|
+ this._renderPlaceholderCell(cell, index);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ {
|
|
|
+ root: this.node,
|
|
|
+ threshold: 1,
|
|
|
+ rootMargin:
|
|
|
+ '0px 0px ' + this.notebookConfig.nonObservedBottomMargin + ' 0px'
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A signal emitted when the notebook is fully rendered.
|
|
|
+ */
|
|
|
+ get fullyRendered(): ISignal<this, boolean> {
|
|
|
+ return this._fullyRendered;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A signal emitted when the a placeholder cell is rendered.
|
|
|
+ */
|
|
|
+ get placeholderCellRendered(): ISignal<this, Cell> {
|
|
|
+ return this._placeholderCellRendered;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -415,7 +460,7 @@ export class StaticNotebook extends Widget {
|
|
|
}
|
|
|
|
|
|
each(cells, (cell: ICellModel, i: number) => {
|
|
|
- this._insertCell(i, cell);
|
|
|
+ this._insertCell(i, cell, 'set');
|
|
|
});
|
|
|
cells.changed.connect(this._onCellsChanged, this);
|
|
|
newValue.contentChanged.connect(this.onModelContentChanged, this);
|
|
@@ -433,8 +478,10 @@ export class StaticNotebook extends Widget {
|
|
|
switch (args.type) {
|
|
|
case 'add':
|
|
|
index = args.newIndex;
|
|
|
+ // eslint-disable-next-line no-case-declarations
|
|
|
+ const insertType: InsertType = args.oldIndex == -1 ? 'push' : 'insert';
|
|
|
each(args.newValues, value => {
|
|
|
- this._insertCell(index++, value);
|
|
|
+ this._insertCell(index++, value, insertType);
|
|
|
});
|
|
|
break;
|
|
|
case 'move':
|
|
@@ -468,7 +515,7 @@ export class StaticNotebook extends Widget {
|
|
|
// Note: this ordering (insert then remove)
|
|
|
// is important for getting the active cell
|
|
|
// index for the editable notebook correct.
|
|
|
- this._insertCell(index, value);
|
|
|
+ this._insertCell(index, value, 'set');
|
|
|
this._removeCell(index + 1);
|
|
|
index++;
|
|
|
});
|
|
@@ -481,7 +528,11 @@ export class StaticNotebook extends Widget {
|
|
|
/**
|
|
|
* Create a cell widget and insert into the notebook.
|
|
|
*/
|
|
|
- private _insertCell(index: number, cell: ICellModel): void {
|
|
|
+ private _insertCell(
|
|
|
+ index: number,
|
|
|
+ cell: ICellModel,
|
|
|
+ insertType: InsertType
|
|
|
+ ): void {
|
|
|
let widget: Cell;
|
|
|
switch (cell.type) {
|
|
|
case 'code':
|
|
@@ -498,9 +549,63 @@ export class StaticNotebook extends Widget {
|
|
|
widget = this._createRawCell(cell as IRawCellModel);
|
|
|
}
|
|
|
widget.addClass(NB_CELL_CLASS);
|
|
|
+
|
|
|
const layout = this.layout as PanelLayout;
|
|
|
- layout.insertWidget(index, widget);
|
|
|
- this.onCellInserted(index, widget);
|
|
|
+ this._cellsArray.push(widget);
|
|
|
+ if (
|
|
|
+ this._observer &&
|
|
|
+ insertType === 'push' &&
|
|
|
+ this._renderedCellsCount > this.notebookConfig.numberCellsToRenderDirectly
|
|
|
+ ) {
|
|
|
+ // We have an observer and we are have been asked to push (not to insert).
|
|
|
+ // and we are above the number of cells to render directly, then
|
|
|
+ // we will add a placeholder and let the instersection observer or the
|
|
|
+ // idle browser render those placeholder cells.
|
|
|
+ this._toRenderMap.set(widget.model.id, { index: index, cell: widget });
|
|
|
+ const placeholder = this._createPlaceholderCell(
|
|
|
+ cell as IRawCellModel,
|
|
|
+ index
|
|
|
+ );
|
|
|
+ placeholder.node.id = widget.model.id;
|
|
|
+ layout.insertWidget(index, placeholder);
|
|
|
+ this.onCellInserted(index, placeholder);
|
|
|
+ this._fullyRendered.emit(false);
|
|
|
+ this._observer.observe(placeholder.node);
|
|
|
+ } else {
|
|
|
+ // We have no intersection observer, or we insert, or we are below
|
|
|
+ // the number of cells to render directly, so we render directly.
|
|
|
+ layout.insertWidget(index, widget);
|
|
|
+ this._incrementRenderedCount();
|
|
|
+ this.onCellInserted(index, widget);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this._observer && this.notebookConfig.renderCellOnIdle) {
|
|
|
+ const renderPlaceholderCells = this._renderPlaceholderCells.bind(this);
|
|
|
+ (window as any).requestIdleCallback(renderPlaceholderCells, {
|
|
|
+ timeout: 1000
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private _renderPlaceholderCells(deadline: any) {
|
|
|
+ if (
|
|
|
+ this._renderedCellsCount < this._cellsArray.length &&
|
|
|
+ this._renderedCellsCount >=
|
|
|
+ this.notebookConfig.numberCellsToRenderDirectly
|
|
|
+ ) {
|
|
|
+ const index = this._renderedCellsCount;
|
|
|
+ const cell = this._cellsArray[index];
|
|
|
+ this._renderPlaceholderCell(cell, index - 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private _renderPlaceholderCell(cell: Cell, index: number) {
|
|
|
+ const pl = this.layout as PanelLayout;
|
|
|
+ pl.removeWidgetAt(index);
|
|
|
+ pl.insertWidget(index, cell);
|
|
|
+ this._toRenderMap.delete(cell.model.id);
|
|
|
+ this._incrementRenderedCount();
|
|
|
+ this._placeholderCellRendered.emit(cell);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -538,7 +643,8 @@ export class StaticNotebook extends Widget {
|
|
|
model,
|
|
|
rendermime,
|
|
|
contentFactory,
|
|
|
- updateEditorOnShow: false
|
|
|
+ updateEditorOnShow: false,
|
|
|
+ placeholder: false
|
|
|
};
|
|
|
const cell = this.contentFactory.createMarkdownCell(options, this);
|
|
|
cell.syncCollapse = true;
|
|
@@ -546,6 +652,40 @@ export class StaticNotebook extends Widget {
|
|
|
return cell;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Create a placeholder cell widget from a raw cell model.
|
|
|
+ */
|
|
|
+ private _createPlaceholderCell(model: IRawCellModel, index: number): RawCell {
|
|
|
+ const contentFactory = this.contentFactory;
|
|
|
+ const editorConfig = this.editorConfig.raw;
|
|
|
+ const options = {
|
|
|
+ editorConfig,
|
|
|
+ model,
|
|
|
+ contentFactory,
|
|
|
+ updateEditorOnShow: false,
|
|
|
+ placeholder: true
|
|
|
+ };
|
|
|
+ const cell = this.contentFactory.createRawCell(options, this);
|
|
|
+ cell.node.innerHTML = `
|
|
|
+ <div class="jp-Cell-Placeholder">
|
|
|
+ <div class="jp-Cell-Placeholder-wrapper">
|
|
|
+ <div class="jp-Cell-Placeholder-wrapper-inner">
|
|
|
+ <div class="jp-Cell-Placeholder-wrapper-body">
|
|
|
+ <div class="jp-Cell-Placeholder-h1"></div>
|
|
|
+ <div class="jp-Cell-Placeholder-h2"></div>
|
|
|
+ <div class="jp-Cell-Placeholder-content-1"></div>
|
|
|
+ <div class="jp-Cell-Placeholder-content-2"></div>
|
|
|
+ <div class="jp-Cell-Placeholder-content-3"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
+ cell.inputHidden = true;
|
|
|
+ cell.syncCollapse = true;
|
|
|
+ cell.syncEditable = true;
|
|
|
+ return cell;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Create a raw cell widget from a raw cell model.
|
|
|
*/
|
|
@@ -556,7 +696,8 @@ export class StaticNotebook extends Widget {
|
|
|
editorConfig,
|
|
|
model,
|
|
|
contentFactory,
|
|
|
- updateEditorOnShow: false
|
|
|
+ updateEditorOnShow: false,
|
|
|
+ placeholder: false
|
|
|
};
|
|
|
const cell = this.contentFactory.createRawCell(options, this);
|
|
|
cell.syncCollapse = true;
|
|
@@ -656,6 +797,13 @@ export class StaticNotebook extends Widget {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ private _incrementRenderedCount() {
|
|
|
+ if (this._toRenderMap.size === 0) {
|
|
|
+ this._fullyRendered.emit(true);
|
|
|
+ }
|
|
|
+ this._renderedCellsCount++;
|
|
|
+ }
|
|
|
+
|
|
|
private _editorConfig = StaticNotebook.defaultEditorConfig;
|
|
|
private _notebookConfig = StaticNotebook.defaultNotebookConfig;
|
|
|
private _mimetype = 'text/plain';
|
|
@@ -663,6 +811,12 @@ export class StaticNotebook extends Widget {
|
|
|
private _mimetypeService: IEditorMimeTypeService;
|
|
|
private _modelChanged = new Signal<this, void>(this);
|
|
|
private _modelContentChanged = new Signal<this, void>(this);
|
|
|
+ private _fullyRendered = new Signal<this, boolean>(this);
|
|
|
+ private _placeholderCellRendered = new Signal<this, Cell>(this);
|
|
|
+ private _observer: IntersectionObserver;
|
|
|
+ private _renderedCellsCount = 0;
|
|
|
+ private _toRenderMap: Map<string, { index: number; cell: Cell }>;
|
|
|
+ private _cellsArray: Array<Cell>;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -796,6 +950,25 @@ export namespace StaticNotebook {
|
|
|
*/
|
|
|
recordTiming: boolean;
|
|
|
|
|
|
+ /*
|
|
|
+ * Number of cells to render directly when virtual
|
|
|
+ * notebook intersection observer is available.
|
|
|
+ */
|
|
|
+ numberCellsToRenderDirectly: number;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Defines if the placeholder cells should be rendered
|
|
|
+ * when the browser is idle.
|
|
|
+ */
|
|
|
+ renderCellOnIdle: boolean;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Defines the non-observed bottom margin for the
|
|
|
+ * virtual notebook, set a positive number of pixels
|
|
|
+ * to render cells below the visible view.
|
|
|
+ */
|
|
|
+ nonObservedBottomMargin: string;
|
|
|
+
|
|
|
/**
|
|
|
* Defines the maximum number of outputs per cell.
|
|
|
*/
|
|
@@ -808,7 +981,10 @@ export namespace StaticNotebook {
|
|
|
scrollPastEnd: true,
|
|
|
defaultCell: 'code',
|
|
|
recordTiming: false,
|
|
|
- maxNumberOutputs: 50
|
|
|
+ maxNumberOutputs: 50,
|
|
|
+ numberCellsToRenderDirectly: 10,
|
|
|
+ renderCellOnIdle: true,
|
|
|
+ nonObservedBottomMargin: '0px'
|
|
|
};
|
|
|
|
|
|
/**
|