@jupyterlab/application 4.6.0-alpha.5 → 4.6.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ import { DockPanelSvg } from '@jupyterlab/ui-components';
2
+ import type { Widget } from '@lumino/widgets';
3
+
4
+ const DEFAULT_NODE_THRESHOLD = 1000;
5
+ const DEFAULT_TEXT_LENGTH_THRESHOLD = 25000;
6
+
7
+ /**
8
+ * A dock panel that freezes heavy panel dimensions during handle drags to
9
+ * reduce layout reflow and improve resize performance.
10
+ */
11
+ export class OptimizedDockPanelSvg extends DockPanelSvg {
12
+ /**
13
+ * Handle the DOM events for the dock panel.
14
+ *
15
+ * @param event - The DOM event sent to the panel.
16
+ *
17
+ * #### Notes
18
+ * This method implements the DOM `EventListener` interface and is
19
+ * called in response to events registered for the node. It should
20
+ * not be called directly by user code.
21
+ */
22
+ override handleEvent(event: Event): void {
23
+ if (event.type === 'pointerdown') {
24
+ this._isResizeDragActive = this._isHandlePointerDown(event);
25
+ }
26
+
27
+ // Unfreeze before super processes the drag-release events so Lumino
28
+ // measures natural sizes when it finalises the split position.
29
+ if (this._frozenGroups.length > 0 && this._isResizeDragActive) {
30
+ const t = event.type;
31
+ if (t === 'pointerup' || t === 'pointercancel' || t === 'keydown') {
32
+ this._isResizeDragActive = false;
33
+ this._unfreezeElements();
34
+ }
35
+ }
36
+
37
+ super.handleEvent(event);
38
+
39
+ if (!this._optimizeResize) {
40
+ return;
41
+ }
42
+
43
+ if (event.type === 'pointerdown' && this._isResizeDragActive) {
44
+ this._freezeHeavyLeaves();
45
+ } else if (event.type === 'pointermove' && this._isResizeDragActive) {
46
+ this._scheduleRefresh();
47
+ }
48
+ }
49
+
50
+ private _isHandlePointerDown(event: Event): boolean {
51
+ const pointerEvent = event as PointerEvent;
52
+ if (pointerEvent.button !== 0) {
53
+ return false;
54
+ }
55
+ const target = pointerEvent.target;
56
+ if (!(target instanceof HTMLElement)) {
57
+ return false;
58
+ }
59
+ for (const handle of this.handles()) {
60
+ if (handle.contains(target)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Whether resize optimizations are enabled.
69
+ *
70
+ * When `true` (the default), panels with heavy DOM content have their
71
+ * dimensions frozen during a handle drag, avoiding repeated reflows.
72
+ * Setting this to `false` immediately unfreezes any frozen panels.
73
+ */
74
+ get optimizeResize(): boolean {
75
+ return this._optimizeResize;
76
+ }
77
+
78
+ set optimizeResize(enabled: boolean) {
79
+ this._optimizeResize = enabled;
80
+ if (!enabled) {
81
+ this._unfreezeElements();
82
+ }
83
+ }
84
+
85
+ private _freezeHeavyLeaves(): void {
86
+ if (this._frozenGroups.length > 0) {
87
+ return;
88
+ }
89
+
90
+ const targets: Widget[] = [];
91
+ for (const child of this.widgets()) {
92
+ this._collectHeavyWidgets(child, targets);
93
+ }
94
+
95
+ for (const target of targets) {
96
+ const head = target.node;
97
+ const elements: HTMLElement[] = [head];
98
+ for (let i = 0; i < head.children.length; i++) {
99
+ elements.push(head.children[i] as HTMLElement);
100
+ }
101
+
102
+ // Read all rects before writing to avoid layout thrashing.
103
+ const rects = elements.map(el => el.getBoundingClientRect());
104
+
105
+ const frozenGroup: Private.IFrozenElement[] = elements.map((el, i) => ({
106
+ element: el,
107
+ isHead: i === 0,
108
+ prevWidth: el.style.width,
109
+ prevMaxWidth: el.style.maxWidth,
110
+ prevHeight: el.style.height,
111
+ prevMaxHeight: el.style.maxHeight
112
+ }));
113
+
114
+ for (let i = 0; i < frozenGroup.length; i++) {
115
+ const el = frozenGroup[i].element;
116
+ const rect = rects[i];
117
+ el.style.width = `${rect.width}px`;
118
+ el.style.maxWidth = `${rect.width}px`;
119
+ el.style.maxHeight = `${rect.height}px`;
120
+ if (frozenGroup[i].isHead) {
121
+ el.style.height = `${rect.height}px`;
122
+ }
123
+ }
124
+ this._frozenGroups.push(frozenGroup);
125
+ }
126
+
127
+ if (this._frozenGroups.length > 0 && this._intervalId === 0) {
128
+ this._intervalId = window.setInterval(() => {
129
+ this._refreshFrozenElements();
130
+ }, 3000);
131
+ }
132
+ }
133
+
134
+ private _scheduleRefresh(): void {
135
+ if (this._frozenGroups.length === 0) {
136
+ return;
137
+ }
138
+ if (this._refreshTimerId !== 0) {
139
+ clearTimeout(this._refreshTimerId);
140
+ }
141
+ this._refreshTimerId = window.setTimeout(() => {
142
+ this._refreshTimerId = 0;
143
+ this._refreshFrozenElements();
144
+ }, 300);
145
+ }
146
+
147
+ private _refreshFrozenElements(): void {
148
+ let g = 0;
149
+ const step = () => {
150
+ if (g >= this._frozenGroups.length) {
151
+ this._refreshRAFId = 0;
152
+ return;
153
+ }
154
+
155
+ let group = this._frozenGroups[g];
156
+ for (let entry of group) {
157
+ let el = entry.element;
158
+ el.style.width = '';
159
+ el.style.maxWidth = '';
160
+ el.style.height = '';
161
+ el.style.maxHeight = '';
162
+ }
163
+
164
+ this._refreshRAFId = requestAnimationFrame(() => {
165
+ let rects = group.map(entry => entry.element.getBoundingClientRect());
166
+ for (let i = 0; i < group.length; i++) {
167
+ let entry = group[i];
168
+ let rect = rects[i];
169
+ let el = entry.element;
170
+ el.style.width = `${rect.width}px`;
171
+ el.style.maxWidth = `${rect.width}px`;
172
+ el.style.maxHeight = `${rect.height}px`;
173
+ if (entry.isHead) {
174
+ el.style.height = `${rect.height}px`;
175
+ }
176
+ }
177
+ g++;
178
+ this._refreshRAFId = requestAnimationFrame(step);
179
+ });
180
+ };
181
+
182
+ this._refreshRAFId = requestAnimationFrame(step);
183
+ }
184
+
185
+ private _unfreezeElements(): void {
186
+ if (this._refreshTimerId !== 0) {
187
+ clearTimeout(this._refreshTimerId);
188
+ this._refreshTimerId = 0;
189
+ }
190
+ if (this._refreshRAFId !== 0) {
191
+ cancelAnimationFrame(this._refreshRAFId);
192
+ this._refreshRAFId = 0;
193
+ }
194
+ if (this._intervalId !== 0) {
195
+ clearInterval(this._intervalId);
196
+ this._intervalId = 0;
197
+ }
198
+
199
+ for (let group of this._frozenGroups) {
200
+ for (let entry of group) {
201
+ entry.element.style.width = entry.prevWidth;
202
+ entry.element.style.maxWidth = entry.prevMaxWidth;
203
+ entry.element.style.height = entry.prevHeight;
204
+ entry.element.style.maxHeight = entry.prevMaxHeight;
205
+ }
206
+ }
207
+
208
+ this._frozenGroups = [];
209
+ }
210
+
211
+ private _collectHeavyWidgets(widget: Widget, result: Widget[]): void {
212
+ if (!this._isDOMHeavy(widget.node)) {
213
+ return;
214
+ }
215
+
216
+ let layout = widget.layout;
217
+ if (!layout) {
218
+ result.push(widget);
219
+ return;
220
+ }
221
+
222
+ let anyChildHeavy = false;
223
+ for (let child of layout) {
224
+ if (this._isDOMHeavy(child.node)) {
225
+ anyChildHeavy = true;
226
+ break;
227
+ }
228
+ }
229
+
230
+ if (anyChildHeavy) {
231
+ for (let child of layout) {
232
+ this._collectHeavyWidgets(child, result);
233
+ }
234
+ } else {
235
+ result.push(widget);
236
+ }
237
+ }
238
+
239
+ private _isDOMHeavy(el: HTMLElement): boolean {
240
+ if (el.querySelectorAll('*').length >= DEFAULT_NODE_THRESHOLD) {
241
+ return true;
242
+ }
243
+ if ((el.textContent?.length ?? 0) >= DEFAULT_TEXT_LENGTH_THRESHOLD) {
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+
249
+ private _optimizeResize = true;
250
+ private _isResizeDragActive = false;
251
+ private _frozenGroups: Private.IFrozenElement[][] = [];
252
+ private _refreshTimerId = 0;
253
+ private _refreshRAFId = 0;
254
+ private _intervalId = 0;
255
+ }
256
+
257
+ /** Namespace for OptimizedDockPanelSvg statics */
258
+ namespace Private {
259
+ export interface IFrozenElement {
260
+ element: HTMLElement;
261
+ isHead: boolean;
262
+ prevWidth: string;
263
+ prevMaxWidth: string;
264
+ prevHeight: string;
265
+ prevMaxHeight: string;
266
+ }
267
+ }
package/src/frontend.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
4
 
4
5
  import { CommandLinker } from '@jupyterlab/apputils';
5
6
  import { DocumentRegistry } from '@jupyterlab/docregistry';
package/src/index.ts CHANGED
@@ -6,11 +6,8 @@
6
6
  */
7
7
 
8
8
  export { ConnectionLost } from './connectionlost';
9
- export {
10
- JupyterFrontEnd,
11
- JupyterFrontEndContextMenu,
12
- JupyterFrontEndPlugin
13
- } from './frontend';
9
+ export { JupyterFrontEnd, JupyterFrontEndContextMenu } from './frontend';
10
+ export type { JupyterFrontEndPlugin } from './frontend';
14
11
  export { JupyterLab } from './lab';
15
12
  export { ILayoutRestorer, LayoutRestorer } from './layoutrestorer';
16
13
  export {
package/src/lab.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
4
 
4
5
  import { PageConfig } from '@jupyterlab/coreutils';
5
6
  import { Base64ModelFactory } from '@jupyterlab/docregistry';
@@ -2,6 +2,7 @@
2
2
  | Copyright (c) Jupyter Development Team.
3
3
  | Distributed under the terms of the Modified BSD License.
4
4
  |----------------------------------------------------------------------------*/
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
6
 
6
7
  import type { WidgetTracker } from '@jupyterlab/apputils';
7
8
  import type { IDataConnector, IRestorer } from '@jupyterlab/statedb';
@@ -398,6 +399,10 @@ export class LayoutRestorer implements ILayoutRestorer {
398
399
  size: area.size
399
400
  };
400
401
 
402
+ if (area.collapsed !== undefined) {
403
+ dehydrated.collapsed = area.collapsed;
404
+ }
405
+
401
406
  if (area.currentWidget) {
402
407
  const current = Private.nameProperty.get(area.currentWidget);
403
408
  if (current) {
@@ -441,6 +446,9 @@ export class LayoutRestorer implements ILayoutRestorer {
441
446
  )
442
447
  .filter(widget => !!widget);
443
448
  return {
449
+ ...(typeof area.collapsed === 'boolean'
450
+ ? { collapsed: area.collapsed }
451
+ : {}),
444
452
  currentWidget: currentWidget!,
445
453
  size: area.size ?? 0.0,
446
454
  widgets: widgets as Widget[] | null
@@ -737,6 +745,11 @@ namespace Private {
737
745
  * The restorable description of the down area in the user interface
738
746
  */
739
747
  export interface IDownArea extends PartialJSONObject {
748
+ /**
749
+ * Whether the down area is collapsed.
750
+ */
751
+ collapsed?: boolean | null;
752
+
740
753
  /**
741
754
  * The current widget that has application focus.
742
755
  */
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
4
 
4
5
  import type { IWidgetTracker } from '@jupyterlab/apputils';
5
6
  import { WidgetTracker } from '@jupyterlab/apputils';