@fails-components/jupyter-applet-view 0.0.1-alpha.10

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,966 @@
1
+ import {
2
+ LabWidgetManager,
3
+ WidgetRenderer
4
+ } from '@jupyter-widgets/jupyterlab-manager';
5
+ import { Cell, CodeCell } from '@jupyterlab/cells';
6
+ import { CellList, NotebookPanel } from '@jupyterlab/notebook';
7
+ import { IOutputAreaModel, OutputArea } from '@jupyterlab/outputarea';
8
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9
+ import { notebookIcon } from '@jupyterlab/ui-components';
10
+ import { ArrayExt } from '@lumino/algorithm';
11
+ import { UUID } from '@lumino/coreutils';
12
+ import { ISignal, Signal } from '@lumino/signaling';
13
+ import {
14
+ AccordionPanel,
15
+ Widget,
16
+ Panel,
17
+ BoxLayout,
18
+ PanelLayout,
19
+ AccordionLayout,
20
+ Title
21
+ } from '@lumino/widgets';
22
+ import { MainAreaWidget } from '@jupyterlab/apputils';
23
+ import { domToBlob } from 'modern-screenshot';
24
+ import { SplitViewNotebookPanel } from './splitviewnotebookpanel';
25
+ import { IFailsInterceptor } from '@fails-components/jupyter-interceptor';
26
+ import { IScreenShotOpts } from '@fails-components/jupyter-launcher';
27
+
28
+ // portions used from Jupyterlab:
29
+ /* -----------------------------------------------------------------------------
30
+ | Copyright (c) Jupyter Development Team.
31
+ | Distributed under the terms of the Modified BSD License.
32
+ |----------------------------------------------------------------------------*/
33
+ // This code contains portions from or is inspired by Jupyter lab's notebook extension, especially the createOutputView part
34
+ // Also a lot is taken from the cell toolbar related parts.
35
+
36
+ /**
37
+ * A widget hosting applet views
38
+ */
39
+
40
+ export class AppletViewOutputArea extends AccordionPanel {
41
+ constructor(options: AppletViewOutputArea.IOptions) {
42
+ super({ renderer: new AppletViewRenderer() });
43
+ const trans = (options.translator || nullTranslator).load('jupyterlab');
44
+ this._notebook = options.notebook;
45
+ this._inLecture = false;
46
+ this._interceptor = options.interceptor;
47
+ if (options.applets !== undefined) {
48
+ this._applets = options.applets.map(
49
+ ({ parts, appid: oldAppid, appname }, index) => {
50
+ const appid = oldAppid ?? UUID.uuid4();
51
+ return {
52
+ appid,
53
+ appname: appname || 'Applet ' + (index + 1),
54
+ observer: new ResizeObserver(
55
+ (entries: ResizeObserverEntry[], observer: ResizeObserver) =>
56
+ this.resizeEvent(appid, entries, observer)
57
+ ),
58
+ parts: parts.map(
59
+ el =>
60
+ new AppletViewOutputAreaPart({
61
+ index: el.index ?? -1,
62
+ cell: el.cell || undefined,
63
+ notebook: this._notebook
64
+ })
65
+ )
66
+ };
67
+ }
68
+ );
69
+ } else {
70
+ const appid = UUID.uuid4();
71
+ this._applets = [];
72
+ this.addApplet({ appid, appname: 'Applet 1' });
73
+ }
74
+ this.id = `AppletView-${UUID.uuid4()}`;
75
+ this.title.label = 'Applets Preview';
76
+ this.title.icon = notebookIcon;
77
+ this.title.caption = this._notebook.title.label
78
+ ? trans.__('For Notebook: %1', this._notebook.title.label)
79
+ : trans.__('For Notebook:');
80
+ this.addClass('fl-jp-AppletView');
81
+
82
+ // Wait for the notebook to be loaded before
83
+ // cloning the output area.
84
+ void this._notebook.context.ready.then(() => {
85
+ this._applets.forEach(({ parts, appid }) => {
86
+ // TODO: Count applets
87
+ parts.forEach((part, index) => {
88
+ if (
89
+ !part.cell &&
90
+ typeof part.index !== 'undefined' &&
91
+ part.index >= 0
92
+ ) {
93
+ const currentcell = this._notebook.content.widgets[
94
+ part.index
95
+ ] as Cell;
96
+ part.cell = currentcell;
97
+ const codeCell = part.cell as CodeCell;
98
+ const outputAreaModel: IOutputAreaModel = codeCell.outputArea.model;
99
+ for (let i = 0; i < outputAreaModel.length; i++) {
100
+ const cur = outputAreaModel.get(i);
101
+ cur.changed.connect(() => {
102
+ // console.log('Model changed', i, cur, outputAreaModel.get(i));
103
+ });
104
+ }
105
+ }
106
+ if (!part.cell /* || part.cell.model.type !== 'code' */) {
107
+ // this.dispose(); // no dispose, just do not add
108
+ return;
109
+ }
110
+ if (part.added) {
111
+ return; // already added
112
+ }
113
+ part.clone = this.addCell(appid, part.cell, part.id || 'undefinedid');
114
+ if (part.cell.model.type === 'code') {
115
+ let managerProm: Promise<LabWidgetManager> | undefined;
116
+ for (const codecell of (part.cell as CodeCell).outputArea.widgets) {
117
+ // We use Array.from instead of using Lumino 2 (JLab 4) iterator
118
+ // This is to support Lumino 1 (JLab 3) as well
119
+ for (const output of Array.from(codecell.children())) {
120
+ if (output instanceof WidgetRenderer) {
121
+ if (output['_manager']) {
122
+ managerProm = output['_manager'].promise;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ managerProm?.then(manager => {
128
+ for (const codecell of (part.clone as OutputArea).widgets) {
129
+ for (const output of Array.from(codecell.children())) {
130
+ if (output instanceof WidgetRenderer) {
131
+ output.manager = manager;
132
+ }
133
+ }
134
+ }
135
+ });
136
+ }
137
+ });
138
+ });
139
+ this._viewChanged.emit();
140
+ });
141
+ }
142
+
143
+ cloneCell(cell: Cell, cellid: string): Widget {
144
+ if (cell.model.type === 'code') {
145
+ const codeCell = cell as CodeCell;
146
+ const clone = codeCell.cloneOutputArea();
147
+ if (this._interceptor) {
148
+ let unsupported = false;
149
+ const outputs = codeCell.model.outputs;
150
+ for (let i = 0; i < outputs.length; i++) {
151
+ const outputModel = outputs.get(i);
152
+ const keys = Object.keys(outputModel.data);
153
+ if (keys.some(key => !this._interceptor?.isMimeTypeSupported(key))) {
154
+ unsupported = true;
155
+ break;
156
+ }
157
+ }
158
+ if (unsupported) {
159
+ clone.addClass('fl-jl-cell-interceptor-unsupported');
160
+ } else {
161
+ clone.removeClass('fl-jl-cell-interceptor-unsupported');
162
+ }
163
+ }
164
+
165
+ // @ts-expect-error cellid does not exist on type
166
+ clone.cellid = cellid;
167
+ // @ts-expect-error cellid does not exist on type
168
+ clone.widgetid = UUID.uuid4();
169
+ return clone;
170
+ } else {
171
+ const clone = cell.clone();
172
+ // @ts-expect-error cellid does not exist on type
173
+ clone.cellid = cellid;
174
+ // @ts-expect-error cellid does not exist on type
175
+ clone.widgetid = UUID.uuid4();
176
+ return clone;
177
+ }
178
+ }
179
+
180
+ getWidgetAppId(widgetid: string): string | undefined {
181
+ const index = this.widgets.findIndex(el =>
182
+ // @ts-expect-error widgetid does not exist on type
183
+ Array.from(el.children()).some(el => el.widgetid === widgetid)
184
+ );
185
+ if (index === -1) {
186
+ return;
187
+ }
188
+ return this._applets[index].appid;
189
+ }
190
+
191
+ addToObserver(appIndex: number, widget: Widget) {
192
+ const observer = this._applets[appIndex].observer;
193
+ observer.observe(widget.node, { box: 'border-box' });
194
+ }
195
+
196
+ removeFromObserver(appIndex: number, widget: Widget) {
197
+ const observer = this._applets[appIndex].observer;
198
+ observer.unobserve(widget.node);
199
+ }
200
+
201
+ addCell(appid: string, cell: Cell, cellid: string): Widget {
202
+ const appIndex = this._applets.findIndex(applet => applet.appid === appid);
203
+ if (appIndex === -1) {
204
+ throw new Error('Applet not found in addcell');
205
+ }
206
+ const app = this.widgets[appIndex] as Panel;
207
+ const clone = this.cloneCell(cell, cellid);
208
+ this.addToObserver(appIndex, clone);
209
+ app.addWidget(clone);
210
+
211
+ // this.informResize(this._applets[appIndex]) // not neccessary
212
+ // trigger an update ?
213
+ this._viewChanged.emit();
214
+ return clone;
215
+ }
216
+
217
+ insertCell(
218
+ appid: string,
219
+ index: number,
220
+ cell: Cell,
221
+ cellid: string
222
+ ): Widget | undefined {
223
+ const appIndex = this._applets.findIndex(applet => applet.appid === appid);
224
+ if (appIndex === -1) {
225
+ return;
226
+ }
227
+ const clone = this.cloneCell(cell, cellid);
228
+ const app = this.widgets[appIndex] as Panel;
229
+ this.addToObserver(appIndex, clone);
230
+ const layout = app.layout as BoxLayout;
231
+ layout.insertWidget(index, clone);
232
+
233
+ // this.informResize(this._applets[appIndex]) // not neccessary
234
+ // trigger an update ?
235
+ this._viewChanged.emit();
236
+ return clone;
237
+ }
238
+
239
+ deletePart(appid: string, cellid: string) {
240
+ const appIndex = this._applets.findIndex(applet => applet.appid === appid);
241
+ if (appIndex === -1) {
242
+ return;
243
+ }
244
+ const applet = this._applets[appIndex];
245
+ const todeleteIndex = applet.parts.findIndex(part => part.id === cellid);
246
+ if (todeleteIndex === -1) {
247
+ return;
248
+ }
249
+ const removedPart = applet.parts.splice(todeleteIndex, 1);
250
+ if (removedPart.length > 0) {
251
+ const cell = removedPart[0].cell;
252
+ if (typeof cell !== 'undefined') {
253
+ this.removeFromObserver(appIndex, cell);
254
+ }
255
+ }
256
+
257
+ const app = this.widgets[appIndex];
258
+ const layout = app.layout as BoxLayout;
259
+
260
+ layout.removeWidgetAt(todeleteIndex);
261
+
262
+ this.informResize(this._applets[appIndex]);
263
+ // trigger an update ?
264
+ this._viewChanged.emit();
265
+ }
266
+
267
+ movePart(appid: string, cellid: string, delta: number) {
268
+ const appIndex = this._applets.findIndex(applet => applet.appid === appid);
269
+ if (appIndex === -1) {
270
+ return;
271
+ }
272
+ const applet = this._applets[appIndex];
273
+ const tomoveIndex = applet.parts.findIndex(part => part.id === cellid);
274
+ if (tomoveIndex === -1) {
275
+ return;
276
+ }
277
+ if (tomoveIndex + delta < 0) {
278
+ return;
279
+ }
280
+ if (tomoveIndex + delta >= applet.parts.length) {
281
+ return;
282
+ }
283
+ const [moveme] = applet.parts.splice(tomoveIndex, 1);
284
+ applet.parts.splice(tomoveIndex + delta + (delta > 1 ? -1 : 0), 0, moveme);
285
+ const app = this.widgets[appIndex] as Panel;
286
+ const layout = app.layout as BoxLayout;
287
+ layout.insertWidget(tomoveIndex + delta, layout.widgets[tomoveIndex]);
288
+ this.informResize(this._applets[appIndex]);
289
+ // trigger an update ?
290
+ this._viewChanged.emit();
291
+ }
292
+ /*
293
+ canMoveApp(appid: string, cellid: string, delta: number): boolean {
294
+ console.log('canmoveapp debug');
295
+ const appIndex = this._applets.findIndex(
296
+ applet => applet.appid === appid
297
+ );
298
+ if (appIndex + delta < 0) {
299
+ return false;
300
+ }
301
+ if (appIndex + delta >= this._applets.length) {
302
+ // only add new apps, if current app will not be empty
303
+ if (this._applets[appIndex].parts.length <= 1) {
304
+ return false;
305
+ }
306
+ }
307
+ return true;
308
+ }
309
+ */
310
+ moveApp(appid: string, cellid: string, delta: number) {
311
+ const appIndex = this._applets.findIndex(applet => applet.appid === appid);
312
+ if (appIndex === -1) {
313
+ return;
314
+ }
315
+ const applet = this._applets[appIndex];
316
+ const partIndex = applet.parts.findIndex(part => part.id === cellid);
317
+ if (partIndex === -1) {
318
+ return;
319
+ }
320
+ if (delta === 0) {
321
+ return;
322
+ }
323
+ if (appIndex + delta < 0) {
324
+ return;
325
+ }
326
+ if (appIndex + delta >= this._applets.length) {
327
+ // only add new apps, if current app will not be empty
328
+ if (this._applets[appIndex].parts.length <= 1) {
329
+ return false;
330
+ }
331
+ // in this case we create a new app
332
+ this.addApplet({ appid: UUID.uuid4() });
333
+ }
334
+ const destApplet = this._applets[appIndex + delta];
335
+ if (destApplet.parts.some(el => el.id === cellid)) {
336
+ // per convention an elment can not be added twice to an app
337
+ return;
338
+ }
339
+ let destPartIndex = 0;
340
+ if (delta < 0) {
341
+ destPartIndex = destApplet.parts.length;
342
+ }
343
+ /*console.log(
344
+ 'app move me debug 0',
345
+ applet.parts.map(el => el.id).join(',')
346
+ );
347
+ console.log(
348
+ 'app move me debug 0 dst',
349
+ destApplet.parts.map(el => el.id).join(',')
350
+ );*/
351
+ const [moveme] = applet.parts.splice(partIndex, 1); // remove
352
+
353
+ destApplet.parts.splice(destPartIndex, 0, moveme);
354
+ /* console.log(
355
+ 'app move me debug 1',
356
+ applet.parts.map(el => el.id).join(',')
357
+ );
358
+ console.log(
359
+ 'app move me debug 1 dst',
360
+ destApplet.parts.map(el => el.id).join(',')
361
+ );*/
362
+ const srcApp = this.widgets[appIndex] as Panel;
363
+ const destApp = this.widgets[appIndex + delta] as Panel;
364
+ const srcLayout = srcApp.layout as BoxLayout;
365
+ const destLayout = destApp.layout as BoxLayout;
366
+ const widget = srcLayout.widgets[partIndex];
367
+ this.removeFromObserver(appIndex, widget);
368
+ this.addToObserver(appIndex + delta, widget);
369
+ destLayout.insertWidget(destPartIndex, widget);
370
+ // srcLayout.removeWidgetAt(partIndex); // not necessary
371
+ if (appIndex === this._applets.length - 1 && applet.parts.length === 0) {
372
+ // if the last applet is empty, we remove it
373
+ this._applets.splice(appIndex, 1);
374
+ const appSrcLayout = this.layout as BoxLayout;
375
+ appSrcLayout.removeWidgetAt(appIndex);
376
+ }
377
+ this.informResize(this._applets[appIndex]);
378
+ this.informResize(this._applets[appIndex + delta]);
379
+ // trigger an update ?
380
+ this._viewChanged.emit();
381
+ }
382
+
383
+ saveData() {
384
+ const applets = this._applets.map(applet => ({
385
+ parts: applet.parts.map(part => ({
386
+ index: part.index,
387
+ id: part.id
388
+ })),
389
+ appid: applet.appid,
390
+ appname: applet.appname
391
+ }));
392
+ return { applets };
393
+ }
394
+
395
+ loadData(data: any): void {
396
+ if (!data) {
397
+ return;
398
+ }
399
+ let applets = data.applets as AppletViewOutputArea.IApplet[];
400
+ if (data.parts && typeof applets === 'undefined') {
401
+ applets = [{ appid: UUID.uuid4(), parts: data.parts }];
402
+ }
403
+ if (
404
+ !Array.isArray(applets) ||
405
+ applets.some(({ parts }) => !Array.isArray(parts))
406
+ ) {
407
+ return;
408
+ }
409
+ // clear applets
410
+ this._applets = [];
411
+ if (this.layout) {
412
+ (this.layout as PanelLayout).widgets.forEach((widget: Widget) =>
413
+ this.layout?.removeWidget(widget)
414
+ );
415
+ }
416
+
417
+ if (applets.length === 0) {
418
+ // we need a minimum of 1 applet!
419
+ const appid = UUID.uuid4();
420
+ this.addApplet({ appid, appname: 'Applet 1' });
421
+ return;
422
+ }
423
+ for (const applet of applets) {
424
+ const appid = applet.appid ?? UUID.uuid4();
425
+ const appname = applet.appname;
426
+ this.addApplet({ appid, appname });
427
+ if (
428
+ typeof this._selectedAppid !== 'undefined' &&
429
+ appid !== this._selectedAppid
430
+ ) {
431
+ this.collapse(this._applets.length - 1);
432
+ }
433
+
434
+ for (const part of applet.parts) {
435
+ if (typeof part.index !== 'undefined' || part.id) {
436
+ this.addPart(appid, {
437
+ index: part.index,
438
+ id: part.id
439
+ });
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ addApplet({ appid, appname }: { appid: string; appname?: string }): Panel {
446
+ // figure out, if it is already added
447
+ let appletIndex = this._applets.findIndex(applet => applet.appid === appid);
448
+ if (appletIndex !== -1) {
449
+ return this.widgets[appletIndex] as Panel;
450
+ }
451
+ // TODO add element to widgets
452
+ appletIndex = this._applets.length;
453
+ appname = appname || 'Applet ' + Math.random().toString(36).slice(2, 6);
454
+ this._applets.push({
455
+ appid,
456
+ appname: appname,
457
+ observer: new ResizeObserver(
458
+ (entries: ResizeObserverEntry[], observer: ResizeObserver) =>
459
+ this.resizeEvent(appid, entries, observer)
460
+ ),
461
+ parts: []
462
+ });
463
+ const layout = this.layout as PanelLayout;
464
+ const panel = new Panel({});
465
+ BoxLayout.setStretch(panel, 1);
466
+ panel.addClass('fl-jp-Applet');
467
+ panel.title.label = appname;
468
+ panel.title.caption = panel.title.label;
469
+ panel.title.changed.connect((title: Title<Widget>) => {
470
+ this._applets[appletIndex].appname = title.label;
471
+ this._viewChanged.emit();
472
+ });
473
+ layout.insertWidget(appletIndex, panel);
474
+
475
+ return panel;
476
+ }
477
+
478
+ addPart(
479
+ appidOrUndefined: string | undefined,
480
+ part: AppletViewOutputArea.IAppletPart
481
+ ) {
482
+ const topush: IViewPart = new AppletViewOutputAreaPart({
483
+ index: part.index !== undefined ? part.index : -1,
484
+ cell: part.cell || undefined,
485
+ id: part.id || undefined,
486
+ notebook: this._notebook
487
+ });
488
+ let appletIndex =
489
+ typeof appidOrUndefined === 'undefined'
490
+ ? 0
491
+ : this._applets.findIndex(applet => applet.appid === appidOrUndefined);
492
+ if (appletIndex === -1) {
493
+ appletIndex = 0;
494
+ }
495
+ const appid = this._applets[appletIndex].appid;
496
+
497
+ const applet = this._applets[appletIndex];
498
+ // we need to figure out, if it is already added
499
+ if (
500
+ applet.parts.some(
501
+ el =>
502
+ el.id === topush.id ||
503
+ (typeof el.cell !== 'undefined' && el.cell === topush.cell)
504
+ )
505
+ ) {
506
+ return;
507
+ }
508
+ this._notebook.content.model?.cells.changed.connect((sender: CellList) => {
509
+ for (const cell of sender) {
510
+ if (cell.id === topush.id) {
511
+ // we found it and are happy that it is still there
512
+ // but is it still the same
513
+ const index = ArrayExt.findFirstIndex(
514
+ this._notebook.content.widgets,
515
+ wcell => wcell === topush.cell
516
+ );
517
+ if (index !== -1) {
518
+ return;
519
+ } // still the same cell
520
+ const oldclone = topush.clone;
521
+ const partind = applet.parts.indexOf(topush); // our position in the list
522
+ const newindex = ArrayExt.findFirstIndex(
523
+ this._notebook.content.widgets,
524
+ wcell => wcell.id === topush.cell?.id
525
+ );
526
+ if (newindex === -1) {
527
+ throw new Error('Cell does not exist');
528
+ }
529
+ topush.cell = this._notebook.content.widgets[newindex] as Cell;
530
+ oldclone?.dispose();
531
+ topush.clone = this.insertCell(
532
+ applet.appid,
533
+ partind,
534
+ topush.cell,
535
+ topush.id
536
+ );
537
+
538
+ return;
539
+ }
540
+ }
541
+ // not found case, it is gone forever so remove from parts and dispose
542
+ const appIndex = this._applets.findIndex(
543
+ applet => applet.appid === appid
544
+ );
545
+ if (appIndex === -1) {
546
+ return;
547
+ }
548
+ const apps = this._applets[appIndex];
549
+ const ind = apps.parts.indexOf(topush);
550
+ if (ind !== -1) {
551
+ apps.parts.splice(ind, 1);
552
+ }
553
+ topush.clone?.dispose();
554
+ });
555
+ applet.parts.push(topush);
556
+ if (this._notebook.context.isReady) {
557
+ // it is already ready, so we can not rely on the global code for adding to the view
558
+ if (
559
+ !topush.cell &&
560
+ typeof part.index !== 'undefined' &&
561
+ part.index >= 0
562
+ ) {
563
+ topush.cell = this._notebook.content.widgets[part.index] as CodeCell;
564
+ }
565
+ if (topush.cell) {
566
+ topush.clone = this.addCell(
567
+ appid,
568
+ topush.cell,
569
+ topush.id || 'undefinedid'
570
+ );
571
+ }
572
+ }
573
+ // trigger an update ?
574
+ this._viewChanged.emit();
575
+ }
576
+
577
+ firstHasIndex(index: number): boolean {
578
+ if (this._applets.length === 0) {
579
+ return false;
580
+ }
581
+ return this._applets[0].parts.some(el => el.index === index);
582
+ }
583
+
584
+ selectApplet(selectedAppid: string) {
585
+ this._selectedAppid = selectedAppid;
586
+ for (let i = 0; i < this._applets.length; i++) {
587
+ const applet = this._applets[i];
588
+ if (applet.appid === selectedAppid) {
589
+ this.expand(i);
590
+ } else {
591
+ this.collapse(i);
592
+ }
593
+ }
594
+ }
595
+
596
+ unselectApplet() {
597
+ for (let i = 0; i < this._applets.length; i++) {
598
+ this.expand(i);
599
+ }
600
+ }
601
+
602
+ async takeAppScreenshot(opts: IScreenShotOpts): Promise<Blob | undefined> {
603
+ if (typeof this._selectedAppid === 'undefined') {
604
+ throw new Error('No app selected');
605
+ }
606
+ const { dpi = undefined } = opts;
607
+ const appletIndex = this._applets.findIndex(
608
+ applet => applet.appid === this._selectedAppid
609
+ );
610
+ if (appletIndex === -1) {
611
+ throw new Error('No invalid app selected');
612
+ }
613
+ try {
614
+ const blob = await domToBlob(this.widgets[appletIndex].node, {
615
+ maximumCanvasSize: 4096,
616
+ scale: (dpi && dpi / 96) || undefined
617
+ });
618
+
619
+ if (blob === null) {
620
+ return undefined;
621
+ }
622
+ return blob;
623
+ } catch (error) {
624
+ //only throw if not returned
625
+ console.log('takeAppScreenshot error', error);
626
+ }
627
+ }
628
+
629
+ informResize(applet: IViewApplet) {
630
+ // inform about the new sizes
631
+ let width = 0;
632
+ let height = 0;
633
+ for (const part of applet.parts) {
634
+ if (typeof part.sizes === 'undefined') {
635
+ continue;
636
+ }
637
+ const { width: ewidth, height: eheight } = part.sizes;
638
+ height += eheight;
639
+ width = Math.max(ewidth, width);
640
+ }
641
+ this._notebook.appletResizeinfo({
642
+ appid: applet.appid,
643
+ width,
644
+ height
645
+ });
646
+ }
647
+
648
+ resizeEvent(
649
+ appid: string,
650
+ entries: ResizeObserverEntry[],
651
+ observer: ResizeObserver
652
+ ): void {
653
+ const applet = this._applets.find(applet => applet.appid === appid);
654
+ if (typeof applet === 'undefined') {
655
+ return;
656
+ }
657
+ let updated = false;
658
+ for (const entry of entries) {
659
+ const part = applet.parts.find(
660
+ part => part?.clone?.node === entry.target
661
+ );
662
+ if (!part) {
663
+ continue;
664
+ }
665
+ if (!entry.borderBoxSize[0]) {
666
+ continue;
667
+ }
668
+ const size = entry.borderBoxSize[0];
669
+ if (size.inlineSize === 0 || size.blockSize === 0) {
670
+ continue;
671
+ } // do not store collapsed values
672
+ part.sizes = {
673
+ width: size.inlineSize,
674
+ height: size.blockSize
675
+ };
676
+ updated = true;
677
+ }
678
+ if (!updated) {
679
+ return;
680
+ }
681
+ this.informResize(applet);
682
+ }
683
+
684
+ /* hasId(id: string): boolean {
685
+ return this._parts.some(el => el.id === id);
686
+ } */
687
+ get applets(): IViewApplet[] {
688
+ return this._applets;
689
+ }
690
+
691
+ /**
692
+ * The index of the cell in the notebook.
693
+ */
694
+ /*
695
+ get index(): number {
696
+ return this._cell
697
+ ? ArrayExt.findFirstIndex(
698
+ this._notebook.content.widgets,
699
+ c => c === this._cell
700
+ )
701
+ : this._index;
702
+ }
703
+ */
704
+ /**
705
+ * The path of the notebook for the cloned output area.
706
+ */
707
+ get path(): string {
708
+ return this._notebook.context.path;
709
+ }
710
+
711
+ get viewChanged(): ISignal<this, void> {
712
+ return this._viewChanged;
713
+ }
714
+
715
+ set inLecture(value: boolean) {
716
+ if (value === this._inLecture) {
717
+ return;
718
+ }
719
+ this._inLecture = value;
720
+ const splitLayout = this.layout as AccordionLayout;
721
+ if (value) {
722
+ splitLayout.titleSpace = 0;
723
+ } else {
724
+ splitLayout.titleSpace = 22;
725
+ }
726
+ }
727
+
728
+ // override base class
729
+ handleEvent(event: Event): void {
730
+ if (event.type === 'click') {
731
+ const target = event.target as HTMLElement;
732
+ if (target.tagName === 'SPAN' || target.tagName === 'INPUT') {
733
+ return;
734
+ }
735
+ }
736
+ if (['keydown', 'keypress', 'keyup'].includes(event.type)) {
737
+ const target = event.target as HTMLElement;
738
+ if (target.tagName === 'INPUT') {
739
+ return; // do not call preventDefault
740
+ }
741
+ }
742
+ super.handleEvent(event);
743
+ }
744
+
745
+ private _notebook: SplitViewNotebookPanel;
746
+ private _applets: IViewApplet[];
747
+ private _selectedAppid: string | undefined;
748
+ private _viewChanged = new Signal<this, void>(this);
749
+ private _inLecture: boolean;
750
+ private _interceptor: IFailsInterceptor | undefined;
751
+ }
752
+ /**
753
+ * AppletViewOutputArea statics.
754
+ */
755
+
756
+ export namespace AppletViewOutputArea {
757
+ export interface IAppletPart {
758
+ /**
759
+ * The cell for which to clone the output area.
760
+ */
761
+ cell?: Cell;
762
+
763
+ /**
764
+ * The cell id to uniquely identify the cell
765
+ */
766
+ id?: string;
767
+
768
+ /**
769
+ * The cell index if the id is not set yet
770
+ */
771
+ index?: number;
772
+ }
773
+ export interface IApplet {
774
+ appid?: string; // should be always, present, but if not it is randomly generated
775
+ appname?: string; // A user readable string identifiying the app
776
+ parts: IAppletPart[];
777
+ }
778
+
779
+ export interface IOptions {
780
+ /**
781
+ * The notebook associated with the cloned output area.
782
+ */
783
+ notebook: SplitViewNotebookPanel;
784
+
785
+ applets?: IApplet[];
786
+
787
+ translator?: ITranslator;
788
+
789
+ interceptor?: IFailsInterceptor;
790
+ }
791
+ }
792
+ export interface IViewPartBase extends AppletViewOutputArea.IAppletPart {
793
+ added?: boolean;
794
+ clone?: Widget;
795
+ }
796
+ export interface IViewPartSize {
797
+ width: number;
798
+ height: number;
799
+ }
800
+
801
+ export interface IViewPart extends IViewPartBase {
802
+ added?: boolean;
803
+ clone?: Widget;
804
+ cloned: ISignal<IViewPart, void>;
805
+ sizes?: IViewPartSize;
806
+ }
807
+ export interface IViewApplet {
808
+ appid: string;
809
+ appname: string;
810
+ parts: IViewPart[];
811
+ observer: ResizeObserver;
812
+ }
813
+ export interface IAppletPartOptions extends IViewPartBase {
814
+ notebook: NotebookPanel;
815
+ }
816
+ export interface IAppletViewOutputAreasStore {
817
+ [key: string]: AppletViewOutputArea;
818
+ }
819
+ export interface IAppletViewMainAreaWidgetStore {
820
+ [key: string]: MainAreaWidget<AppletViewOutputArea>;
821
+ }
822
+ export class AppletViewOutputAreaPart
823
+ implements AppletViewOutputArea.IAppletPart
824
+ {
825
+ constructor(args: IAppletPartOptions) {
826
+ this._cell = args.cell;
827
+ this._index = args.index ?? -1;
828
+ this._id = args.id || this._cell?.model.id;
829
+ this._notebook = args.notebook;
830
+ }
831
+
832
+ /**
833
+ * The index of the cell in the notebook.
834
+ */
835
+ get index(): number {
836
+ if (this._id) {
837
+ const ind = ArrayExt.findFirstIndex(
838
+ this._notebook.content.widgets,
839
+ c => c.model.id === this._id
840
+ );
841
+ if (ind !== -1) {
842
+ return ind;
843
+ }
844
+ }
845
+ return this._cell
846
+ ? ArrayExt.findFirstIndex(
847
+ this._notebook.content.widgets,
848
+ c => c === this._cell
849
+ )
850
+ : this._index;
851
+ }
852
+
853
+ get cell(): Cell | undefined {
854
+ return this._cell;
855
+ }
856
+
857
+ set cell(value: Cell | undefined) {
858
+ if (value?.model.id !== this._id) {
859
+ // throw new Error('Can not assign a cell with different id');
860
+ /* console.log(
861
+ 'ASSIGNING CELL with different id',
862
+ value?.model.id,
863
+ this._id
864
+ ); */
865
+ }
866
+ /* if (this._cell?.model.id !== value?.model.id) {
867
+ console.log('ASSIGNING CELL id change', value?.model.id, this._id);
868
+ } */
869
+ this._cell = value;
870
+ }
871
+
872
+ get added(): boolean {
873
+ return !!this._clone;
874
+ }
875
+
876
+ get id(): string | undefined {
877
+ return this._id;
878
+ }
879
+
880
+ set clone(value: Widget | undefined) {
881
+ this._clone = value;
882
+ this._cloned.emit(); // inform that we have been cloned
883
+ }
884
+
885
+ get clone(): Widget | undefined {
886
+ return this._clone;
887
+ }
888
+
889
+ get path(): string {
890
+ return this._notebook.context.path;
891
+ }
892
+
893
+ get cloned(): ISignal<this, void> {
894
+ return this._cloned;
895
+ }
896
+
897
+ private _cell: Cell | undefined;
898
+ private _id: string | undefined;
899
+ private _index: number;
900
+ private _notebook: NotebookPanel;
901
+ private _clone: Widget | undefined;
902
+ private _cloned = new Signal<this, void>(this);
903
+ }
904
+
905
+ export class AppletViewRenderer extends AccordionPanel.Renderer {
906
+ createSectionTitle(data: Title<Widget>): HTMLElement {
907
+ const handle = document.createElement('h3');
908
+ handle.setAttribute('tabindex', '0');
909
+ handle.id = this.createTitleKey(data);
910
+ handle.className = this.titleClassName;
911
+ for (const aData in data.dataset) {
912
+ handle.dataset[aData] = data.dataset[aData];
913
+ }
914
+
915
+ const collapser = handle.appendChild(this.createCollapseIcon(data));
916
+ collapser.className = 'lm-AccordionPanel-titleCollapser';
917
+
918
+ let title = data.caption || data.label;
919
+
920
+ const staticLabel = document.createElement('span');
921
+ staticLabel.className = 'lm-AccordionPanel-titleLabel';
922
+ staticLabel.textContent = data.label;
923
+ staticLabel.title = title;
924
+
925
+ handle.appendChild(staticLabel);
926
+
927
+ const editLabel = document.createElement('input');
928
+ editLabel.className = 'lm-AccordionPanel-titleLabelEdit';
929
+ editLabel.type = 'text';
930
+ editLabel.value = data.label;
931
+ editLabel.title = title;
932
+
933
+ staticLabel.addEventListener('click', (ev: MouseEvent) => {
934
+ handle.removeChild(staticLabel);
935
+ handle.appendChild(editLabel);
936
+ });
937
+ editLabel.addEventListener('blur', (ev: FocusEvent) => {
938
+ handle.removeChild(editLabel);
939
+ handle.appendChild(staticLabel);
940
+ });
941
+ editLabel.addEventListener('keydown', (ev: KeyboardEvent) => {
942
+ if (ev.key === 'Enter') {
943
+ handle.removeChild(editLabel);
944
+ handle.appendChild(staticLabel);
945
+ }
946
+ return true;
947
+ });
948
+ editLabel.addEventListener('change', event => {
949
+ const target = event.target as HTMLInputElement | null;
950
+ if (target === editLabel) {
951
+ const newValue = target?.value;
952
+ if (newValue && newValue !== title) {
953
+ title = newValue;
954
+ data.caption = title;
955
+ data.label = title;
956
+ staticLabel.title = title;
957
+ staticLabel.textContent = title;
958
+ }
959
+ }
960
+ handle.removeChild(editLabel);
961
+ handle.appendChild(staticLabel);
962
+ });
963
+
964
+ return handle;
965
+ }
966
+ }