UNPKG

15.3 kBJavaScriptView Raw
1/*
2 * Copyright (c) Jupyter Development Team.
3 * Distributed under the terms of the Modified BSD License.
4 */
5import { CodeCell } from '@jupyterlab/cells';
6import { WindowedLayout, WindowedListModel } from '@jupyterlab/ui-components';
7import { MessageLoop } from '@lumino/messaging';
8import { Widget } from '@lumino/widgets';
9import { DROP_SOURCE_CLASS, DROP_TARGET_CLASS } from './constants';
10/**
11 * Notebook view model for the windowed list.
12 */
13export class NotebookViewModel extends WindowedListModel {
14 /**
15 * Construct a notebook windowed list model.
16 */
17 constructor(cells, options) {
18 super(options);
19 this.cells = cells;
20 /**
21 * Cell size estimator
22 *
23 * @param index Cell index
24 * @returns Cell height in pixels
25 */
26 this.estimateWidgetSize = (index) => {
27 // TODO could be improved, takes only into account the editor height
28 const nLines = this.cells[index].model.sharedModel
29 .getSource()
30 .split('\n').length;
31 return (NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT * nLines +
32 NotebookViewModel.DEFAULT_CELL_MARGIN);
33 };
34 /**
35 * Render the cell at index.
36 *
37 * @param index Cell index
38 * @returns Cell widget
39 */
40 this.widgetRenderer = (index) => {
41 return this.cells[index];
42 };
43 /**
44 * Threshold used to decide if the cell should be scrolled to in the `smart` mode.
45 * Defaults to scrolling when less than a full line of the cell is visible.
46 */
47 this.scrollDownThreshold = NotebookViewModel.DEFAULT_CELL_MARGIN / 2 +
48 NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT;
49 /**
50 * Threshold used to decide if the cell should be scrolled to in the `smart` mode.
51 * Defaults to scrolling when the cell margin or more is invisible.
52 */
53 this.scrollUpThreshold = NotebookViewModel.DEFAULT_CELL_MARGIN / 2;
54 // Set default cell size
55 this._estimatedWidgetSize = NotebookViewModel.DEFAULT_CELL_SIZE;
56 }
57}
58/**
59 * Default cell height
60 */
61NotebookViewModel.DEFAULT_CELL_SIZE = 39;
62/**
63 * Default editor line height
64 */
65NotebookViewModel.DEFAULT_EDITOR_LINE_HEIGHT = 17;
66/**
67 * Default cell margin (top + bottom)
68 */
69NotebookViewModel.DEFAULT_CELL_MARGIN = 22;
70/**
71 * Windowed list layout for the notebook.
72 */
73export class NotebookWindowedLayout extends WindowedLayout {
74 constructor() {
75 super(...arguments);
76 this._header = null;
77 this._footer = null;
78 this._willBeRemoved = null;
79 this._topHiddenCodeCells = -1;
80 }
81 /**
82 * Notebook's header
83 */
84 get header() {
85 return this._header;
86 }
87 set header(header) {
88 var _a;
89 if (this._header && this._header.isAttached) {
90 Widget.detach(this._header);
91 }
92 this._header = header;
93 if (this._header && ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isAttached)) {
94 Widget.attach(this._header, this.parent.node);
95 }
96 }
97 /**
98 * Notebook widget's footer
99 */
100 get footer() {
101 return this._footer;
102 }
103 set footer(footer) {
104 var _a;
105 if (this._footer && this._footer.isAttached) {
106 Widget.detach(this._footer);
107 }
108 this._footer = footer;
109 if (this._footer && ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isAttached)) {
110 Widget.attach(this._footer, this.parent.outerNode);
111 }
112 }
113 /**
114 * Notebook's active cell
115 */
116 get activeCell() {
117 return this._activeCell;
118 }
119 set activeCell(widget) {
120 this._activeCell = widget;
121 }
122 /**
123 * Dispose the layout
124 * */
125 dispose() {
126 var _a, _b;
127 if (this.isDisposed) {
128 return;
129 }
130 (_a = this._header) === null || _a === void 0 ? void 0 : _a.dispose();
131 (_b = this._footer) === null || _b === void 0 ? void 0 : _b.dispose();
132 super.dispose();
133 }
134 /**
135 * * A message handler invoked on a `'child-removed'` message.
136 * *
137 * @param widget - The widget to remove from the layout.
138 *
139 * #### Notes
140 * A widget is automatically removed from the layout when its `parent`
141 * is set to `null`. This method should only be invoked directly when
142 * removing a widget from a layout which has yet to be installed on a
143 * parent widget.
144 *
145 * This method does *not* modify the widget's `parent`.
146 */
147 removeWidget(widget) {
148 const index = this.widgets.indexOf(widget);
149 // We need to deal with code cell widget not in viewport (aka not in this.widgets) but still
150 // partly attached
151 if (index >= 0) {
152 this.removeWidgetAt(index);
153 } // If the layout is parented, detach the widget from the DOM.
154 else if (widget === this._willBeRemoved && this.parent) {
155 this.detachWidget(index, widget);
156 }
157 }
158 /**
159 * Attach a widget to the parent's DOM node.
160 *
161 * @param index - The current index of the widget in the layout.
162 *
163 * @param widget - The widget to attach to the parent.
164 *
165 * #### Notes
166 * This method is called automatically by the panel layout at the
167 * appropriate time. It should not be called directly by user code.
168 *
169 * The default implementation adds the widgets's node to the parent's
170 * node at the proper location, and sends the appropriate attach
171 * messages to the widget if the parent is attached to the DOM.
172 *
173 * Subclasses may reimplement this method to control how the widget's
174 * node is added to the parent's node.
175 */
176 attachWidget(index, widget) {
177 // Status may change in onBeforeAttach
178 const wasPlaceholder = widget.isPlaceholder();
179 // Initialized sub-widgets or attached them for CodeCell
180 // Because this reattaches all sub-widget to the DOM which leads
181 // to a loss of focus, we do not call it for soft-hidden cells.
182 const isSoftHidden = this._isSoftHidden(widget);
183 if (this.parent.isAttached && !isSoftHidden) {
184 MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
185 }
186 if (isSoftHidden) {
187 // Restore visibility for active, or previously active cell
188 this._toggleSoftVisibility(widget, true);
189 }
190 if (!wasPlaceholder &&
191 widget instanceof CodeCell &&
192 widget.node.parentElement) {
193 // We don't remove code cells to preserve outputs internal state
194 widget.node.style.display = '';
195 // Reset cache
196 this._topHiddenCodeCells = -1;
197 }
198 else if (!isSoftHidden) {
199 // Look up the next sibling reference node.
200 const siblingIndex = this._findNearestChildBinarySearch(this.parent.viewportNode.childElementCount - 1, 0, parseInt(widget.dataset.windowedListIndex, 10) + 1);
201 let ref = this.parent.viewportNode.children[siblingIndex];
202 // Insert the widget's node before the sibling.
203 this.parent.viewportNode.insertBefore(widget.node, ref);
204 // Send an `'after-attach'` message if the parent is attached.
205 // Event listeners will be added here
206 // Some widgets are updating/resetting when attached, so
207 // we should not recall this each time a cell move into the
208 // viewport.
209 if (this.parent.isAttached) {
210 MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
211 }
212 }
213 widget.inViewport = true;
214 }
215 /**
216 * Detach a widget from the parent's DOM node.
217 *
218 * @param index - The previous index of the widget in the layout.
219 *
220 * @param widget - The widget to detach from the parent.
221 *
222 * #### Notes
223 * This method is called automatically by the panel layout at the
224 * appropriate time. It should not be called directly by user code.
225 *
226 * The default implementation removes the widget's node from the
227 * parent's node, and sends the appropriate detach messages to the
228 * widget if the parent is attached to the DOM.
229 *
230 * Subclasses may reimplement this method to control how the widget's
231 * node is removed from the parent's node.
232 */
233 detachWidget(index, widget) {
234 widget.inViewport = false;
235 // Note: `index` is relative to the displayed cells, not all cells,
236 // hence we compare with the widget itself.
237 if (widget === this.activeCell) {
238 // Do not change display of the active cell to allow user to continue providing input
239 // into the code mirror editor when out of view. We still hide the cell so to prevent
240 // minor visual glitches when scrolling.
241 this._toggleSoftVisibility(widget, false);
242 // Return before sending "AfterDetach" message to CodeCell
243 // to prevent removing contents of the active cell.
244 return;
245 }
246 // TODO we could improve this further by discarding also the code cell without outputs
247 if (
248 // We detach the code cell currently dragged otherwise it won't be attached at the correct position
249 widget instanceof CodeCell &&
250 !widget.node.classList.contains(DROP_SOURCE_CLASS) &&
251 widget !== this._willBeRemoved) {
252 // We don't remove code cells to preserve outputs internal state
253 // Transform does not work because the widget height is kept (at least in FF)
254 widget.node.style.display = 'none';
255 // Reset cache
256 this._topHiddenCodeCells = -1;
257 }
258 else {
259 // Send a `'before-detach'` message if the parent is attached.
260 // This should not be called every time a cell leaves the viewport
261 // as it will remove listeners that won't be added back as afterAttach
262 // is shunted to avoid unwanted update/reset.
263 if (this.parent.isAttached) {
264 // Event listeners will be removed here
265 MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
266 }
267 // Remove the widget's node from the parent.
268 this.parent.viewportNode.removeChild(widget.node);
269 // Ensure to clean up drop target class if the widget move out of the viewport
270 widget.node.classList.remove(DROP_TARGET_CLASS);
271 }
272 if (this.parent.isAttached) {
273 // Detach sub widget of CodeCell except the OutputAreaWrapper
274 MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
275 }
276 }
277 /**
278 * Move a widget in the parent's DOM node.
279 *
280 * @param fromIndex - The previous index of the widget in the layout.
281 *
282 * @param toIndex - The current index of the widget in the layout.
283 *
284 * @param widget - The widget to move in the parent.
285 *
286 * #### Notes
287 * This method is called automatically by the panel layout at the
288 * appropriate time. It should not be called directly by user code.
289 *
290 * The default implementation moves the widget's node to the proper
291 * location in the parent's node and sends the appropriate attach and
292 * detach messages to the widget if the parent is attached to the DOM.
293 *
294 * Subclasses may reimplement this method to control how the widget's
295 * node is moved in the parent's node.
296 */
297 moveWidget(fromIndex, toIndex, widget) {
298 // Optimize move without de-/attaching as motion appends with parent attached
299 // Case fromIndex === toIndex, already checked in PanelLayout.insertWidget
300 if (this._topHiddenCodeCells < 0) {
301 this._topHiddenCodeCells = 0;
302 for (let idx = 0; idx < this.parent.viewportNode.children.length; idx++) {
303 const n = this.parent.viewportNode.children[idx];
304 if (n.style.display == 'none') {
305 this._topHiddenCodeCells++;
306 }
307 else {
308 break;
309 }
310 }
311 }
312 const ref = this.parent.viewportNode.children[toIndex + this._topHiddenCodeCells];
313 if (fromIndex < toIndex) {
314 ref.insertAdjacentElement('afterend', widget.node);
315 }
316 else {
317 ref.insertAdjacentElement('beforebegin', widget.node);
318 }
319 }
320 onAfterAttach(msg) {
321 super.onAfterAttach(msg);
322 if (this._header && !this._header.isAttached) {
323 Widget.attach(this._header, this.parent.node, this.parent.node.firstElementChild);
324 }
325 if (this._footer && !this._footer.isAttached) {
326 Widget.attach(this._footer, this.parent.outerNode);
327 }
328 }
329 onBeforeDetach(msg) {
330 var _a, _b;
331 if ((_a = this._header) === null || _a === void 0 ? void 0 : _a.isAttached) {
332 Widget.detach(this._header);
333 }
334 if ((_b = this._footer) === null || _b === void 0 ? void 0 : _b.isAttached) {
335 Widget.detach(this._footer);
336 }
337 super.onBeforeDetach(msg);
338 }
339 /**
340 * A message handler invoked on a `'child-removed'` message.
341 *
342 * @param msg Message
343 */
344 onChildRemoved(msg) {
345 this._willBeRemoved = msg.child;
346 super.onChildRemoved(msg);
347 this._willBeRemoved = null;
348 }
349 /**
350 * Toggle "soft" visibility of the widget.
351 *
352 * #### Notes
353 * To ensure that user events reach the CodeMirror editor, this method
354 * does not toggle `display` nor `visibility` which have side effects,
355 * but instead hides it in the compositor and ensures that the bounding
356 * box is has an area equal to zero.
357 * To ensure we do not trigger style recalculation, we set the styles
358 * directly on the node instead of using a class.
359 */
360 _toggleSoftVisibility(widget, show) {
361 if (show) {
362 widget.node.style.opacity = '';
363 widget.node.style.height = '';
364 widget.node.style.padding = '';
365 }
366 else {
367 widget.node.style.opacity = '0';
368 // Both padding and height need to be set to zero
369 // to ensure bounding box collapses to invisible.
370 widget.node.style.height = '0';
371 widget.node.style.padding = '0';
372 }
373 }
374 _isSoftHidden(widget) {
375 return widget.node.style.opacity === '0';
376 }
377 _findNearestChildBinarySearch(high, low, index) {
378 while (low <= high) {
379 const middle = low + Math.floor((high - low) / 2);
380 const currentIndex = parseInt(this.parent.viewportNode.children[middle].dataset
381 .windowedListIndex, 10);
382 if (currentIndex === index) {
383 return middle;
384 }
385 else if (currentIndex < index) {
386 low = middle + 1;
387 }
388 else if (currentIndex > index) {
389 high = middle - 1;
390 }
391 }
392 if (low > 0) {
393 return low;
394 }
395 else {
396 return 0;
397 }
398 }
399}
400//# sourceMappingURL=windowing.js.map
\No newline at end of file