@abi-software/flatmap-viewer 2.3.0-a.3 → 2.3.0-a.5
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/README.rst +1 -1
- package/package.json +2 -1
- package/src/annotation.js +169 -129
package/README.rst
CHANGED
|
@@ -38,7 +38,7 @@ The map server endpoint is specified as ``MAP_ENDPOINT`` in ``src/main.js``. It
|
|
|
38
38
|
Package Installation
|
|
39
39
|
====================
|
|
40
40
|
|
|
41
|
-
* ``npm install @abi-software/flatmap-viewer@2.3.0-a.
|
|
41
|
+
* ``npm install @abi-software/flatmap-viewer@2.3.0-a.5``
|
|
42
42
|
|
|
43
43
|
Documentation
|
|
44
44
|
-------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abi-software/flatmap-viewer",
|
|
3
|
-
"version": "2.3.0-a.
|
|
3
|
+
"version": "2.3.0-a.5",
|
|
4
4
|
"description": "Flatmap viewer using Maplibre GL",
|
|
5
5
|
"repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
|
|
6
6
|
"main": "src/main.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"css-loader": "^6.5.1",
|
|
42
42
|
"eslint": "^8.7.0",
|
|
43
43
|
"express": "^4.17.1",
|
|
44
|
+
"file-loader": "^6.2.0",
|
|
44
45
|
"html-webpack-plugin": "^4.5.2",
|
|
45
46
|
"strip-ansi": "^7.0.1",
|
|
46
47
|
"style-loader": "^1.0.0",
|
package/src/annotation.js
CHANGED
|
@@ -31,9 +31,9 @@ import 'jspanel4/dist/jspanel.css';
|
|
|
31
31
|
//==============================================================================
|
|
32
32
|
|
|
33
33
|
const FETCH_TIMEOUT = 3000; // 3 seconds
|
|
34
|
-
const UPDATE_TIMEOUT =
|
|
34
|
+
const UPDATE_TIMEOUT = 3000; // 5 seconds
|
|
35
35
|
const LOGIN_TIMEOUT = 30000; // 30 seconds
|
|
36
|
-
const LOGOUT_TIMEOUT =
|
|
36
|
+
const LOGOUT_TIMEOUT = 3000; // 5 seconds
|
|
37
37
|
|
|
38
38
|
const STATUS_MESSAGE_TIMEOUT = 3000;
|
|
39
39
|
|
|
@@ -68,6 +68,18 @@ const ANNOTATION_FIELDS = [
|
|
|
68
68
|
|
|
69
69
|
//==============================================================================
|
|
70
70
|
|
|
71
|
+
function startSpinner(panel)
|
|
72
|
+
{
|
|
73
|
+
panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stopSpinner(panel)
|
|
77
|
+
{
|
|
78
|
+
panel.headerlogo.innerHTML = '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//==============================================================================
|
|
82
|
+
|
|
71
83
|
export class Annotator
|
|
72
84
|
{
|
|
73
85
|
constructor(flatmap)
|
|
@@ -101,58 +113,51 @@ export class Annotator
|
|
|
101
113
|
this.__setStatusMessage('', 0);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
__authorise(panel
|
|
105
|
-
|
|
116
|
+
async __authorise(panel)
|
|
117
|
+
//======================
|
|
106
118
|
{
|
|
119
|
+
/*
|
|
120
|
+
const testUser = {name: 'Testing...'};
|
|
121
|
+
this.__setUser(testUser);
|
|
122
|
+
callback(testUser);
|
|
123
|
+
|
|
124
|
+
*/
|
|
107
125
|
const abortController = new AbortController();
|
|
108
|
-
const url = `${this.__flatmap._baseUrl}login`;
|
|
109
|
-
panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
|
|
110
|
-
fetch(url, {
|
|
111
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
112
|
-
signal: abortController.signal
|
|
113
|
-
}).then((response) => {
|
|
114
|
-
panel.headerlogo.innerHTML = '';
|
|
115
|
-
if (response.ok) {
|
|
116
|
-
const creator = response.json();
|
|
117
|
-
if ('error' in creator) {
|
|
118
|
-
callback({error: creator.error});
|
|
119
|
-
} else {
|
|
120
|
-
this.__setUser(creator);
|
|
121
|
-
this.__authorised = true;
|
|
122
|
-
callback(creator);
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
callback({error: `${response.status} ${response.statusText}`});
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
126
|
setTimeout((panel) => {
|
|
129
127
|
if (this.user === 'undefined') {
|
|
130
128
|
console.log("Aborting login...");
|
|
131
129
|
abortController.abort();
|
|
132
|
-
panel
|
|
130
|
+
stopSpinner(panel);
|
|
133
131
|
this.__setStatusMessage('Unable to login...');
|
|
134
132
|
}
|
|
135
133
|
},
|
|
136
134
|
LOGIN_TIMEOUT, panel);
|
|
137
|
-
}
|
|
138
135
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.__clearUser();
|
|
143
|
-
const abortController = new AbortController();
|
|
144
|
-
const url = `${this.__flatmap._baseUrl}logout`;
|
|
145
|
-
fetch(url, {
|
|
136
|
+
const url = `${this.__flatmap._baseUrl}login`;
|
|
137
|
+
startSpinner(panel);
|
|
138
|
+
const response = await fetch(url, {
|
|
146
139
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
147
140
|
signal: abortController.signal
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
});
|
|
142
|
+
stopSpinner(panel);
|
|
143
|
+
if (response.ok) {
|
|
144
|
+
const user_data = await response.json();
|
|
145
|
+
if ('error' in user_data) {
|
|
146
|
+
return Promise.resolve({error: response.error});
|
|
152
147
|
} else {
|
|
153
|
-
|
|
148
|
+
this.__setUser(user_data);
|
|
149
|
+
this.__authorised = true;
|
|
150
|
+
return user_data;
|
|
154
151
|
}
|
|
155
|
-
}
|
|
152
|
+
} else {
|
|
153
|
+
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async __unauthorise()
|
|
158
|
+
//===================
|
|
159
|
+
{
|
|
160
|
+
const abortController = new AbortController();
|
|
156
161
|
setTimeout(() => {
|
|
157
162
|
if (this.__authorised) {
|
|
158
163
|
console.log("Aborting logout...");
|
|
@@ -161,6 +166,18 @@ export class Annotator
|
|
|
161
166
|
}
|
|
162
167
|
},
|
|
163
168
|
LOGOUT_TIMEOUT);
|
|
169
|
+
|
|
170
|
+
const url = `${this.__flatmap._baseUrl}logout`;
|
|
171
|
+
const response = fetch(url, {
|
|
172
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
173
|
+
signal: abortController.signal
|
|
174
|
+
});
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
this.__authorised = false;
|
|
177
|
+
return await response.json();
|
|
178
|
+
} else {
|
|
179
|
+
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
180
|
+
}
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
__setStatusMessage(message, timeout=STATUS_MESSAGE_TIMEOUT)
|
|
@@ -220,15 +237,15 @@ export class Annotator
|
|
|
220
237
|
return html.join('\n');
|
|
221
238
|
}
|
|
222
239
|
|
|
223
|
-
__editFormHtml(
|
|
224
|
-
|
|
240
|
+
__editFormHtml(provenanceData)
|
|
241
|
+
//============================
|
|
225
242
|
{
|
|
226
243
|
const html = [];
|
|
227
244
|
html.push('<div id="flatmap-annotation-formdata">');
|
|
228
245
|
for (const field of ANNOTATION_FIELDS) {
|
|
229
246
|
html.push('<div class="flatmap-annotation-entry">');
|
|
230
247
|
html.push(` <label for="${field.key}">${field.prompt}:</label>`);
|
|
231
|
-
const value = field.update ?
|
|
248
|
+
const value = field.update ? provenanceData[field.key] || '' : '';
|
|
232
249
|
if (field.kind === 'textbox') {
|
|
233
250
|
html.push(` <textarea rows="5" cols="40" id="${field.key}" name="${field.key}">${value.trim()}</textarea>`)
|
|
234
251
|
} else if (!('kind' in field) || field.kind !== 'list') {
|
|
@@ -248,30 +265,51 @@ export class Annotator
|
|
|
248
265
|
return html.join('\n');
|
|
249
266
|
}
|
|
250
267
|
|
|
251
|
-
|
|
268
|
+
__provenanceData(annotations)
|
|
269
|
+
//===========================
|
|
270
|
+
{
|
|
271
|
+
const provenanceData = {};
|
|
272
|
+
for (const annotation of annotations) { // In order of most recent to oldest
|
|
273
|
+
if (annotation['rdf:type'] === 'prov:Entity') {
|
|
274
|
+
for (const field of ANNOTATION_FIELDS) {
|
|
275
|
+
if (field.update) {
|
|
276
|
+
const value = annotation[field.key];
|
|
277
|
+
if (value !== undefined && !(field.key in provenanceData)) {
|
|
278
|
+
provenanceData[field.key] = value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return provenanceData;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
__changedAnnotation(provenanceData)
|
|
252
288
|
//=================================
|
|
253
289
|
{
|
|
254
290
|
const newProperties = {};
|
|
255
291
|
let propertiesChanged = false;
|
|
256
292
|
for (const field of ANNOTATION_FIELDS) {
|
|
257
|
-
const lastValue = field.update ?
|
|
293
|
+
const lastValue = field.update ? provenanceData[field.key] || '' : '';
|
|
258
294
|
if (!('kind' in field) || field.kind !== 'list') {
|
|
259
295
|
const inputField = document.getElementById(field.key);
|
|
260
|
-
|
|
261
|
-
if (
|
|
296
|
+
const newValue = inputField.value.trim();
|
|
297
|
+
if (newValue !== lastValue.trim()) {
|
|
298
|
+
newProperties[field.key] = newValue;
|
|
262
299
|
propertiesChanged = true;
|
|
263
300
|
}
|
|
264
301
|
} else { // field.kind === 'list'
|
|
265
|
-
|
|
266
|
-
const changedList = false;
|
|
302
|
+
const listValues = [];
|
|
267
303
|
for (let n = 1; n <= field.size; n++) {
|
|
268
|
-
const lastListValue = (n <= lastValue.length) ? lastValue[n-1].trim() : '';
|
|
269
304
|
const inputField = document.getElementById(`${field.key}_${n}`);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
listValues.push(inputField.value.trim());
|
|
306
|
+
}
|
|
307
|
+
const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort();
|
|
308
|
+
const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort();
|
|
309
|
+
if (oldValues.length !== newValues.length
|
|
310
|
+
|| oldValues.filter(v => !newValues.includes(v)).length > 0) {
|
|
311
|
+
newProperties[field.key] = newValues;
|
|
312
|
+
propertiesChanged = true;
|
|
275
313
|
}
|
|
276
314
|
}
|
|
277
315
|
}
|
|
@@ -281,30 +319,38 @@ export class Annotator
|
|
|
281
319
|
}
|
|
282
320
|
}
|
|
283
321
|
|
|
284
|
-
__updateRemoteAnnotation(
|
|
285
|
-
|
|
322
|
+
async __updateRemoteAnnotation(panel, annotation)
|
|
323
|
+
//===============================================
|
|
286
324
|
{
|
|
287
325
|
const abortController = new AbortController();
|
|
326
|
+
|
|
327
|
+
setTimeout((panel) => {
|
|
328
|
+
if (panel.status !== 'closed') {
|
|
329
|
+
console.log("Aborting remote update...");
|
|
330
|
+
abortController.abort();
|
|
331
|
+
stopSpinner(panel);
|
|
332
|
+
this.__setStatusMessage('Cannot update annotation...');
|
|
333
|
+
}
|
|
334
|
+
}, UPDATE_TIMEOUT, panel);
|
|
335
|
+
|
|
288
336
|
const url = this.__flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`);
|
|
289
|
-
fetch(url, {
|
|
337
|
+
const response = await fetch(url, {
|
|
290
338
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
291
339
|
method: 'POST',
|
|
292
340
|
body: JSON.stringify(annotation),
|
|
293
341
|
signal: abortController.signal
|
|
294
|
-
}).then((response) => {
|
|
295
|
-
if (response.ok) {
|
|
296
|
-
callback(response.json());
|
|
297
|
-
} else {
|
|
298
|
-
callback({error: `${response.status} ${response.statusText}`});
|
|
299
|
-
}
|
|
300
342
|
});
|
|
301
|
-
|
|
343
|
+
if (response.ok) {
|
|
344
|
+
return await response.json();
|
|
345
|
+
} else {
|
|
346
|
+
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
347
|
+
}
|
|
302
348
|
}
|
|
303
349
|
|
|
304
|
-
__saveAnnotation(panel,
|
|
305
|
-
|
|
350
|
+
async __saveAnnotation(panel, provenanceData)
|
|
351
|
+
//===========================================
|
|
306
352
|
{
|
|
307
|
-
const changedProperties = this.__changedAnnotation(
|
|
353
|
+
const changedProperties = this.__changedAnnotation(provenanceData);
|
|
308
354
|
if (this.__currentFeatureId !== undefined && changedProperties.changed) {
|
|
309
355
|
const annotation = {
|
|
310
356
|
...changedProperties.properties,
|
|
@@ -312,26 +358,15 @@ export class Annotator
|
|
|
312
358
|
'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
|
|
313
359
|
'dct:creator': this.user
|
|
314
360
|
}
|
|
315
|
-
panel
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
setTimeout((panel) => {
|
|
326
|
-
if (panel.status !== 'closed') {
|
|
327
|
-
console.log("Aborting remote update...");
|
|
328
|
-
remoteUpdate.abort();
|
|
329
|
-
panel.headerlogo.innerHTML = '';
|
|
330
|
-
this.__setStatusMessage('Cannot update annotation...');
|
|
331
|
-
}
|
|
332
|
-
}, UPDATE_TIMEOUT, panel);
|
|
361
|
+
startSpinner(panel);
|
|
362
|
+
const response = await this.__updateRemoteAnnotation(panel, annotation);
|
|
363
|
+
stopSpinner(panel);
|
|
364
|
+
if ('error' in response) {
|
|
365
|
+
this.__setStatusMessage(response.error);
|
|
366
|
+
} else {
|
|
367
|
+
panel.close();
|
|
368
|
+
}
|
|
333
369
|
} else {
|
|
334
|
-
this.__
|
|
335
370
|
this.__setStatusMessage('No changes to save...');
|
|
336
371
|
}
|
|
337
372
|
}
|
|
@@ -340,9 +375,9 @@ export class Annotator
|
|
|
340
375
|
//====================================
|
|
341
376
|
{
|
|
342
377
|
this.__haveAnnotation = true;
|
|
378
|
+
const provenanceData = this.__provenanceData(response);
|
|
343
379
|
this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
|
|
344
|
-
|
|
345
|
-
this.__annotationForm.innerHTML = this.__editFormHtml(lastAnnotation);
|
|
380
|
+
this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
|
|
346
381
|
|
|
347
382
|
// Lock focus to focusable elements within the panel
|
|
348
383
|
const inputElements = panel.content.querySelectorAll('input, textarea, button');
|
|
@@ -365,16 +400,52 @@ export class Annotator
|
|
|
365
400
|
}
|
|
366
401
|
} else if (e.key === 'Enter') {
|
|
367
402
|
if (e.target === saveButton) {
|
|
368
|
-
this.__saveAnnotation(panel,
|
|
403
|
+
this.__saveAnnotation(panel, provenanceData);
|
|
369
404
|
}
|
|
370
405
|
}
|
|
371
406
|
}.bind(this));
|
|
372
407
|
|
|
373
408
|
saveButton.addEventListener('mousedown', function (e) {
|
|
374
|
-
this.__saveAnnotation(panel,
|
|
409
|
+
this.__saveAnnotation(panel, provenanceData);
|
|
375
410
|
}.bind(this));
|
|
376
411
|
}
|
|
377
412
|
|
|
413
|
+
__panelCallback(panel)
|
|
414
|
+
//====================
|
|
415
|
+
{
|
|
416
|
+
this.__annotationForm = document.getElementById('flatmap-annotation-form');
|
|
417
|
+
// Data entry only once authorised
|
|
418
|
+
this.__annotationForm.hidden = true;
|
|
419
|
+
|
|
420
|
+
// Populate once we have content from server
|
|
421
|
+
this.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
|
|
422
|
+
this.__statusMessage = document.getElementById('flatmap-annotation-status');
|
|
423
|
+
|
|
424
|
+
this.__authoriseLock = document.getElementById('flatmap-annotation-lock');
|
|
425
|
+
this.__authoriseLock.addEventListener('click', (e) => {
|
|
426
|
+
const lockClasses = this.__authoriseLock.classList;
|
|
427
|
+
if (lockClasses.contains('fa-lock')) {
|
|
428
|
+
this.__authorise(panel).then((response) => {
|
|
429
|
+
if ('error' in response) {
|
|
430
|
+
this.__setStatusMessage(response.error);
|
|
431
|
+
} else {
|
|
432
|
+
this.__annotationForm.hidden = false;
|
|
433
|
+
this.__firstInputField.focus();
|
|
434
|
+
lockClasses.remove('fa-lock');
|
|
435
|
+
lockClasses.add('fa-unlock');
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
this.__unauthorise().then((response) => {
|
|
440
|
+
console.log(`Annotator logout: ${response}`);
|
|
441
|
+
});
|
|
442
|
+
this.__annotationForm.hidden = true;
|
|
443
|
+
lockClasses.remove('fa-unlock');
|
|
444
|
+
lockClasses.add('fa-lock');
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
378
449
|
annotate(feature, closedCallback)
|
|
379
450
|
//===============================
|
|
380
451
|
{
|
|
@@ -394,8 +465,8 @@ export class Annotator
|
|
|
394
465
|
panelContent.push(' <div id="flatmap-annotation-existing"></div>');
|
|
395
466
|
panelContent.push('</div>');
|
|
396
467
|
|
|
397
|
-
const annotator = this; // To use in panel code
|
|
398
|
-
const flatmap = this.__flatmap; // To use in panel code
|
|
468
|
+
const annotator = this; // To use in panel creation code
|
|
469
|
+
const flatmap = this.__flatmap; // To use in panel creation code
|
|
399
470
|
const contentFetchAbort = new AbortController();
|
|
400
471
|
this.__panel = jsPanel.create({
|
|
401
472
|
theme: 'light',
|
|
@@ -428,12 +499,12 @@ export class Annotator
|
|
|
428
499
|
},
|
|
429
500
|
bodyMethod: 'json',
|
|
430
501
|
beforeSend: (fetchConfig, panel) => {
|
|
431
|
-
panel
|
|
502
|
+
startSpinner(panel);
|
|
432
503
|
setTimeout((panel) => {
|
|
433
504
|
if (!annotator.__haveAnnotation) {
|
|
434
505
|
console.log("Aborting content fetch...");
|
|
435
506
|
contentFetchAbort.abort();
|
|
436
|
-
panel
|
|
507
|
+
stopSpinner(panel);
|
|
437
508
|
annotator.__setStatusMessage('Cannot fetch annotation...');
|
|
438
509
|
annotator.__authoriseLock.className = '';
|
|
439
510
|
}
|
|
@@ -441,45 +512,14 @@ export class Annotator
|
|
|
441
512
|
},
|
|
442
513
|
done: (response, panel) => {
|
|
443
514
|
annotator.__finishPanelContent(panel, response);
|
|
444
|
-
panel
|
|
515
|
+
stopSpinner(panel);
|
|
445
516
|
}
|
|
446
517
|
},
|
|
447
|
-
callback: (
|
|
448
|
-
annotator.__annotationForm = document.getElementById('flatmap-annotation-form');
|
|
449
|
-
// Data entry only once authorised
|
|
450
|
-
annotator.__annotationForm.hidden = true;
|
|
451
|
-
|
|
452
|
-
// Populate once we have content from server
|
|
453
|
-
annotator.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
|
|
454
|
-
annotator.__statusMessage = document.getElementById('flatmap-annotation-status');
|
|
455
|
-
|
|
456
|
-
annotator.__authoriseLock = document.getElementById('flatmap-annotation-lock');
|
|
457
|
-
annotator.__authoriseLock.addEventListener('click', (e) => {
|
|
458
|
-
const lockClasses = annotator.__authoriseLock.classList;
|
|
459
|
-
if (lockClasses.contains('fa-lock')) {
|
|
460
|
-
annotator.__authorise(panel, (response) => {
|
|
461
|
-
if ('error' in response) {
|
|
462
|
-
annotator.__setStatusMessage(response.error);
|
|
463
|
-
} else {
|
|
464
|
-
annotator.__annotationForm.hidden = false;
|
|
465
|
-
annotator.__firstInputField.focus();
|
|
466
|
-
lockClasses.remove('fa-lock');
|
|
467
|
-
lockClasses.add('fa-unlock');
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
} else {
|
|
471
|
-
annotator.__unauthorise();
|
|
472
|
-
annotator.__annotationForm.hidden = true;
|
|
473
|
-
lockClasses.remove('fa-unlock');
|
|
474
|
-
lockClasses.add('fa-lock');
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// should we warn if unsaved changes when closing??
|
|
479
|
-
document.addEventListener('jspanelclosed', closedCallback, false);
|
|
480
|
-
}
|
|
518
|
+
callback: annotator.__panelCallback.bind(annotator)
|
|
481
519
|
});
|
|
482
520
|
|
|
521
|
+
// should we warn if unsaved changes when closing??
|
|
522
|
+
document.addEventListener('jspanelclosed', closedCallback, false);
|
|
483
523
|
}
|
|
484
524
|
|
|
485
525
|
}
|