@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.
- package/LICENSE +29 -0
- package/README.md +101 -0
- package/lib/appletview.d.ts +7 -0
- package/lib/appletview.js +333 -0
- package/lib/avoutputarea.d.ts +149 -0
- package/lib/avoutputarea.js +751 -0
- package/lib/avtoolbarextension.d.ts +36 -0
- package/lib/avtoolbarextension.js +213 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +35 -0
- package/lib/splitviewnotebookpanel.d.ts +31 -0
- package/lib/splitviewnotebookpanel.js +127 -0
- package/package.json +226 -0
- package/schema/plugin.json +31 -0
- package/schema/toolbar.json +8 -0
- package/src/appletview.ts +395 -0
- package/src/avoutputarea.ts +966 -0
- package/src/avtoolbarextension.ts +280 -0
- package/src/index.ts +47 -0
- package/src/splitviewnotebookpanel.ts +186 -0
- package/style/base.css +5 -0
- package/style/index.css +163 -0
- package/style/index.js +2 -0
|
@@ -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
|
+
}
|