@jupyterlab/notebook
Version:
JupyterLab - Notebook
345 lines • 13 kB
JavaScript
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
import { CodeCell } from '@jupyterlab/cells';
import { WindowedLayout, WindowedListModel } from '@jupyterlab/ui-components';
import { MessageLoop } from '@lumino/messaging';
import { Widget } from '@lumino/widgets';
import { DROP_SOURCE_CLASS, DROP_TARGET_CLASS } from './constants';
/**
* Notebook view model for the windowed list.
*/
class NotebookViewModel extends WindowedListModel {
/**
* Construct a notebook windowed list model.
*/
constructor(cells, options) {
super(options);
this.cells = cells;
/**
* Cell size estimator
*
* @param index Cell index
* @returns Cell height in pixels
*/
this.estimateWidgetSize = (index) => {
// TODO could be improved, takes only into account the editor height
const nLines = this.cells[index].model.sharedModel
.getSource()
.split('\n').length;
return (NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT * nLines +
NotebookViewModel.DEFAULT_CELL_MARGIN);
};
/**
* Render the cell at index.
*
* @param index Cell index
* @returns Cell widget
*/
this.widgetRenderer = (index) => {
return this.cells[index];
};
/**
* Threshold used to decide if the cell should be scrolled to in the `smart` mode.
* Defaults to scrolling when less than a full line of the cell is visible.
*/
this.scrollDownThreshold = NotebookViewModel.DEFAULT_CELL_MARGIN / 2 +
NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT;
/**
* Threshold used to decide if the cell should be scrolled to in the `smart` mode.
* Defaults to scrolling when the cell margin or more is invisible.
*/
this.scrollUpThreshold = NotebookViewModel.DEFAULT_CELL_MARGIN / 2;
// Set default cell size
this._estimatedWidgetSize = NotebookViewModel.DEFAULT_CELL_SIZE;
}
}
/**
* Default cell height
*/
NotebookViewModel.DEFAULT_CELL_SIZE = 39;
/**
* Default editor line height
*/
NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT = 17;
/**
* Default cell margin (top + bottom)
*/
NotebookViewModel.DEFAULT_CELL_MARGIN = 22;
export { NotebookViewModel };
/**
* Windowed list layout for the notebook.
*/
export class NotebookWindowedLayout extends WindowedLayout {
constructor() {
super(...arguments);
this._header = null;
this._footer = null;
this._willBeRemoved = null;
this._topHiddenCodeCells = -1;
}
/**
* Notebook's header
*/
get header() {
return this._header;
}
set header(header) {
var _a;
if (this._header && this._header.isAttached) {
Widget.detach(this._header);
}
this._header = header;
if (this._header && ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isAttached)) {
Widget.attach(this._header, this.parent.node);
}
}
/**
* Notebook widget's footer
*/
get footer() {
return this._footer;
}
set footer(footer) {
var _a;
if (this._footer && this._footer.isAttached) {
Widget.detach(this._footer);
}
this._footer = footer;
if (this._footer && ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isAttached)) {
Widget.attach(this._footer, this.parent.node);
}
}
/**
* Dispose the layout
* */
dispose() {
var _a, _b;
if (this.isDisposed) {
return;
}
(_a = this._header) === null || _a === void 0 ? void 0 : _a.dispose();
(_b = this._footer) === null || _b === void 0 ? void 0 : _b.dispose();
super.dispose();
}
/**
* * A message handler invoked on a `'child-removed'` message.
* *
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget) {
const index = this.widgets.indexOf(widget);
// We need to deal with code cell widget not in viewport (aka not in this.widgets) but still
// partly attached
if (index >= 0) {
this.removeWidgetAt(index);
} // If the layout is parented, detach the widget from the DOM.
else if (widget === this._willBeRemoved && this.parent) {
this.detachWidget(index, widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation adds the widgets's node to the parent's
* node at the proper location, and sends the appropriate attach
* messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is added to the parent's node.
*/
attachWidget(index, widget) {
// Status may change in onBeforeAttach
const wasPlaceholder = widget.isPlaceholder();
// Initialized sub-widgets or attached them for CodeCell
if (this.parent.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
if (!wasPlaceholder &&
widget instanceof CodeCell &&
widget.node.parentElement) {
// We don't remove code cells to preserve outputs internal state
widget.node.style.display = '';
// Reset cache
this._topHiddenCodeCells = -1;
}
else {
// Look up the next sibling reference node.
const siblingIndex = this._findNearestChildBinarySearch(this.parent.viewportNode.childElementCount - 1, 0, parseInt(widget.dataset.windowedListIndex, 10) + 1);
let ref = this.parent.viewportNode.children[siblingIndex];
// Insert the widget's node before the sibling.
this.parent.viewportNode.insertBefore(widget.node, ref);
// Send an `'after-attach'` message if the parent is attached.
// Event listeners will be added here
// Some widgets are updating/resetting when attached, so
// we should not recall this each time a cell move into the
// viewport.
if (this.parent.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
widget.inViewport = true;
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation removes the widget's node from the
* parent's node, and sends the appropriate detach messages to the
* widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is removed from the parent's node.
*/
detachWidget(index, widget) {
widget.inViewport = false;
// TODO we could improve this further by discarding also the code cell without outputs
if (widget instanceof CodeCell &&
// We detach the code cell currently dragged otherwise it won't be attached at the correct position
!widget.node.classList.contains(DROP_SOURCE_CLASS) &&
widget !== this._willBeRemoved) {
// We don't remove code cells to preserve outputs internal state
// Transform does not work because the widget height is kept (at lease in FF)
widget.node.style.display = 'none';
// Reset cache
this._topHiddenCodeCells = -1;
}
else {
// Send a `'before-detach'` message if the parent is attached.
// This should not be called every time a cell leaves the viewport
// as it will remove listeners that won't be added back as afterAttach
// is shunted to avoid unwanted update/reset.
if (this.parent.isAttached) {
// Event listeners will be removed here
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.viewportNode.removeChild(widget.node);
// Ensure to clean up drop target class if the widget move out of the viewport
widget.node.classList.remove(DROP_TARGET_CLASS);
}
if (this.parent.isAttached) {
// Detach sub widget of CodeCell except the OutputAreaWrapper
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation moves the widget's node to the proper
* location in the parent's node and sends the appropriate attach and
* detach messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is moved in the parent's node.
*/
moveWidget(fromIndex, toIndex, widget) {
// Optimize move without de-/attaching as motion appends with parent attached
// Case fromIndex === toIndex, already checked in PanelLayout.insertWidget
if (this._topHiddenCodeCells < 0) {
this._topHiddenCodeCells = 0;
for (let idx = 0; idx < this.parent.viewportNode.children.length; idx++) {
const n = this.parent.viewportNode.children[idx];
if (n.style.display == 'none') {
this._topHiddenCodeCells++;
}
else {
break;
}
}
}
const ref = this.parent.viewportNode.children[toIndex + this._topHiddenCodeCells];
if (fromIndex < toIndex) {
ref.insertAdjacentElement('afterend', widget.node);
}
else {
ref.insertAdjacentElement('beforebegin', widget.node);
}
}
onAfterAttach(msg) {
super.onAfterAttach(msg);
if (this._header && !this._header.isAttached) {
Widget.attach(this._header, this.parent.node, this.parent.node.firstElementChild);
}
if (this._footer && !this._footer.isAttached) {
Widget.attach(this._footer, this.parent.node);
}
}
onBeforeDetach(msg) {
var _a, _b;
if ((_a = this._header) === null || _a === void 0 ? void 0 : _a.isAttached) {
Widget.detach(this._header);
}
if ((_b = this._footer) === null || _b === void 0 ? void 0 : _b.isAttached) {
Widget.detach(this._footer);
}
super.onBeforeDetach(msg);
}
/**
* A message handler invoked on a `'child-removed'` message.
*
* @param msg Message
*/
onChildRemoved(msg) {
this._willBeRemoved = msg.child;
super.onChildRemoved(msg);
this._willBeRemoved = null;
}
_findNearestChildBinarySearch(high, low, index) {
while (low <= high) {
const middle = low + Math.floor((high - low) / 2);
const currentIndex = parseInt(this.parent.viewportNode.children[middle].dataset
.windowedListIndex, 10);
if (currentIndex === index) {
return middle;
}
else if (currentIndex < index) {
low = middle + 1;
}
else if (currentIndex > index) {
high = middle - 1;
}
}
if (low > 0) {
return low;
}
else {
return 0;
}
}
}
//# sourceMappingURL=windowing.js.map