neo.mjs 5.16.4 → 5.17.0
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/apps/ServiceWorker.mjs +2 -2
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/component/statusbadge/MainContainer.mjs +80 -0
- package/examples/component/statusbadge/app.mjs +6 -0
- package/examples/component/statusbadge/index.html +11 -0
- package/examples/component/statusbadge/neo-config.json +6 -0
- package/examples/form/field/fileupload/MainContainer.mjs +59 -12
- package/examples/treeSelectionModel/MainContainer.mjs +142 -0
- package/examples/treeSelectionModel/app.mjs +6 -0
- package/examples/treeSelectionModel/index.html +11 -0
- package/examples/treeSelectionModel/neo-config.json +7 -0
- package/examples/treeSelectionModel/tree.json +112 -0
- package/package.json +3 -3
- package/resources/scss/src/component/StatusBadge.scss +9 -0
- package/resources/scss/src/examples/treeSelectionModel/MainContainer.scss +24 -0
- package/resources/scss/src/form/field/FileUpload.scss +19 -4
- package/resources/scss/src/tree/Accordion.scss +128 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/component/StatusBadge.mjs +73 -0
- package/src/core/Base.mjs +6 -0
- package/src/form/field/FileUpload.mjs +274 -35
- package/src/form/field/Text.mjs +1 -1
- package/src/selection/TreeAccordionModel.mjs +293 -0
- package/src/selection/TreeModel.mjs +3 -3
- package/src/tree/Accordion.mjs +280 -0
- package/src/tree/List.mjs +31 -22
@@ -14,7 +14,7 @@ const
|
|
14
14
|
* An accessible file uploading widget which automatically commences an upload as soon as
|
15
15
|
* a file is selected using the UI.
|
16
16
|
*
|
17
|
-
* The URL to which the file must be uploaded is specified in the {@link #
|
17
|
+
* The URL to which the file must be uploaded is specified in the {@link config#uploadUrl} property.
|
18
18
|
* This service must return a JSON status response in the following form for successful uploads:
|
19
19
|
*
|
20
20
|
* ```json
|
@@ -130,6 +130,42 @@ class FileUpload extends Base {
|
|
130
130
|
|
131
131
|
cls : [],
|
132
132
|
|
133
|
+
/**
|
134
|
+
* An Object containing a default set of headers to be passed to the server on every HTTP request.
|
135
|
+
* @member {Object} headers
|
136
|
+
*/
|
137
|
+
headers : {},
|
138
|
+
|
139
|
+
/**
|
140
|
+
* An Object which allows the status text returned from the {@link #property-documentStatusUrl} to be
|
141
|
+
* mapped to the corresponding next widget state.
|
142
|
+
* @member {Object} documentStatusMap
|
143
|
+
*/
|
144
|
+
documentStatusMap : {
|
145
|
+
SCANNING : 'scanning',
|
146
|
+
|
147
|
+
// The server doing its own secondary upload to the final storage location may return this.
|
148
|
+
// We enter the same state as scanning. A spinner shows for the duration of this state
|
149
|
+
UPLOADING : 'scanning',
|
150
|
+
|
151
|
+
MALWARE_DETECTED : 'scan-failed',
|
152
|
+
UN_DOWNLOADABLE : 'not-downloadable',
|
153
|
+
DOWNLOADABLE : 'downloadable',
|
154
|
+
DELETED : 'deleted'
|
155
|
+
},
|
156
|
+
|
157
|
+
document_ : null,
|
158
|
+
|
159
|
+
/**
|
160
|
+
* If this widget should reference an existing document, configure the widget with a documentId
|
161
|
+
* so that it can initialize in the correct "uploaded" state.
|
162
|
+
*
|
163
|
+
* If this is *not* configured, then this property will be set after a successful upload to
|
164
|
+
* the id returned from the {@link #property-uploadUrl}.
|
165
|
+
* @member {String|Number} documentId
|
166
|
+
*/
|
167
|
+
documentId : null,
|
168
|
+
|
133
169
|
/**
|
134
170
|
* The URL of the file upload service to which the selected file is sent.
|
135
171
|
*
|
@@ -149,12 +185,14 @@ class FileUpload extends Base {
|
|
149
185
|
* The document status request URL must be configured in {@link #member-documentStatusUrl}
|
150
186
|
* @member {String} uploadUrl
|
151
187
|
*/
|
152
|
-
|
188
|
+
uploadUrl : null,
|
153
189
|
|
154
190
|
/**
|
155
191
|
* The name of the JSON property in which the document id is returned in the upload response
|
156
|
-
* JSON packet and the
|
157
|
-
* deletion.
|
192
|
+
* JSON packet and the token string which is substituted for the document id when requesting
|
193
|
+
* a malware scan and a document deletion.
|
194
|
+
*
|
195
|
+
* Defaults fro `documentId`
|
158
196
|
*
|
159
197
|
* @member {String} downloadUrl
|
160
198
|
*/
|
@@ -163,16 +201,38 @@ class FileUpload extends Base {
|
|
163
201
|
/**
|
164
202
|
* The URL from which the file may be downloaded after it has finished its scan.
|
165
203
|
*
|
204
|
+
* This must contain a substitution token named the same as the {@link #property-documentIdParameter}
|
205
|
+
* which is used when creating a URL
|
206
|
+
*
|
207
|
+
* for example:
|
208
|
+
*
|
209
|
+
* ```json
|
210
|
+
* {
|
211
|
+
* downloadUrl : '/getDocument/${documentId}'
|
212
|
+
* }
|
213
|
+
* ```
|
214
|
+
*
|
166
215
|
* The document id returned from the {@link #member-uploadUrl upload} is passed in the parameter named
|
167
216
|
* by the {@link #member-documentIdParameter}. It defaults to `'documentId'`.
|
168
217
|
*
|
169
218
|
* @member {String} downloadUrl
|
170
219
|
*/
|
171
|
-
|
220
|
+
downloadUrl : null,
|
172
221
|
|
173
222
|
/**
|
174
223
|
* The URL of the file status reporting service.
|
175
224
|
*
|
225
|
+
* This must contain a substitution token named the same as the {@link #property-documentIdParameter}
|
226
|
+
* which is used when creating a URL
|
227
|
+
*
|
228
|
+
* for example:
|
229
|
+
*
|
230
|
+
* ```json
|
231
|
+
* {
|
232
|
+
* documentStatusUrl : '/getDocumentStatus/${documentId}'
|
233
|
+
* }
|
234
|
+
* ```
|
235
|
+
*
|
176
236
|
* This widget will use this service after a successful upload to determine its next
|
177
237
|
* state.
|
178
238
|
*
|
@@ -188,9 +248,30 @@ class FileUpload extends Base {
|
|
188
248
|
*/
|
189
249
|
documentStatusUrl : null,
|
190
250
|
|
251
|
+
/**
|
252
|
+
* The polling interval *in milliseconds* to wait between asking the server how the document scan
|
253
|
+
* is proceeding.
|
254
|
+
*
|
255
|
+
* Defaults to 2000ms
|
256
|
+
*
|
257
|
+
* @member {String} documentDeleteUrl
|
258
|
+
*/
|
259
|
+
statusScanInterval : 2000,
|
260
|
+
|
191
261
|
/**
|
192
262
|
* The URL of the file deletion service.
|
193
263
|
*
|
264
|
+
* This must contain a substitution token named the same as the {@link #property-documentIdParameter}
|
265
|
+
* which is used when creating a URL
|
266
|
+
*
|
267
|
+
* for example:
|
268
|
+
*
|
269
|
+
* ```json
|
270
|
+
* {
|
271
|
+
* documentDeleteUrl : '/deleteDocument/${documentId}'
|
272
|
+
* }
|
273
|
+
* ```
|
274
|
+
*
|
194
275
|
* This widget will use this service after a successful upload to determine its next
|
195
276
|
* state.
|
196
277
|
*
|
@@ -200,8 +281,6 @@ class FileUpload extends Base {
|
|
200
281
|
*/
|
201
282
|
documentDeleteUrl : null,
|
202
283
|
|
203
|
-
headers_ : {},
|
204
|
-
|
205
284
|
/**
|
206
285
|
* @member {String} state_=null
|
207
286
|
*/
|
@@ -215,7 +294,27 @@ class FileUpload extends Base {
|
|
215
294
|
/**
|
216
295
|
* @member {String|Number} maxSize
|
217
296
|
*/
|
218
|
-
maxSize_: null
|
297
|
+
maxSize_: null,
|
298
|
+
|
299
|
+
/**
|
300
|
+
* The error text to show below the widget
|
301
|
+
* @member {String} error
|
302
|
+
*/
|
303
|
+
error_ : null,
|
304
|
+
|
305
|
+
// UI strings which can be overridden for other languages
|
306
|
+
documentText : 'Document',
|
307
|
+
pleaseUseTheseTypes : 'Please use these file types',
|
308
|
+
fileSizeMoreThan : 'File size exceeds',
|
309
|
+
documentDeleteError : 'Document delete service error',
|
310
|
+
isNoLongerAvailable : 'is no longer available',
|
311
|
+
documentStatusError : 'Document status service error',
|
312
|
+
uploadFailed : 'Upload failed',
|
313
|
+
scanning : 'Scanning',
|
314
|
+
malwareFoundInFile : 'Malware found in file',
|
315
|
+
pleaseCheck : 'Please check the file and try again',
|
316
|
+
successfullyUploaded : 'Successfully uploaded',
|
317
|
+
fileWasDeleted : 'File was deleted'
|
219
318
|
}
|
220
319
|
|
221
320
|
/**
|
@@ -232,6 +331,13 @@ class FileUpload extends Base {
|
|
232
331
|
]);
|
233
332
|
}
|
234
333
|
|
334
|
+
/**
|
335
|
+
* @returns {Object}
|
336
|
+
*/
|
337
|
+
getInputEl() {
|
338
|
+
return this.vdom.cn[3];
|
339
|
+
}
|
340
|
+
|
235
341
|
async clear() {
|
236
342
|
const me = this;
|
237
343
|
|
@@ -246,7 +352,7 @@ class FileUpload extends Base {
|
|
246
352
|
|
247
353
|
// We have to wait for the DOM to have changed, and the input field to be visible
|
248
354
|
await new Promise(resolve => setTimeout(resolve, 100));
|
249
|
-
me.focus(me.
|
355
|
+
me.focus(me.getInputEl().id);
|
250
356
|
}
|
251
357
|
|
252
358
|
/**
|
@@ -265,10 +371,10 @@ class FileUpload extends Base {
|
|
265
371
|
type = pointPos > -1 ? file.name.slice(pointPos + 1) : '';
|
266
372
|
|
267
373
|
if (me.types && !types[type]) {
|
268
|
-
me.error =
|
374
|
+
me.error = `${me.pleaseUseTheseTypes}: .${Object.keys(types).join(' .')}`;
|
269
375
|
}
|
270
376
|
else if (file.size > me.maxSize) {
|
271
|
-
me.error =
|
377
|
+
me.error = `${me.fileSizeMoreThan} ${String(me._maxSize).toUpperCase()}`;
|
272
378
|
}
|
273
379
|
// If it passes the type and maxSize check, upload it
|
274
380
|
else {
|
@@ -288,7 +394,8 @@ class FileUpload extends Base {
|
|
288
394
|
me = this,
|
289
395
|
xhr = me.xhr = new XMLHttpRequest(),
|
290
396
|
{ upload } = xhr,
|
291
|
-
fileData = new FormData()
|
397
|
+
fileData = new FormData(),
|
398
|
+
headers = { ...me.headers };
|
292
399
|
|
293
400
|
// Show the action button
|
294
401
|
me.state = 'starting';
|
@@ -311,6 +418,22 @@ class FileUpload extends Base {
|
|
311
418
|
|
312
419
|
xhr.open("POST", me.uploadUrl, true);
|
313
420
|
|
421
|
+
/**
|
422
|
+
* This event fires before every HTTP request is sent to the server via any of the configured URLs.
|
423
|
+
*
|
424
|
+
* @event beforeRequest
|
425
|
+
* @param {Object} event The event
|
426
|
+
* @param {Object} event.headers An object containing the configured {@link #property-headers}
|
427
|
+
* for this widget, into which new headers may be injected.
|
428
|
+
* @returns {Object}
|
429
|
+
*/
|
430
|
+
me.fire('beforeRequest', {
|
431
|
+
headers
|
432
|
+
});
|
433
|
+
for (const header in headers) {
|
434
|
+
xhr.setRequestHeader(header, headers[header]);
|
435
|
+
}
|
436
|
+
|
314
437
|
xhr.send(fileData);
|
315
438
|
}
|
316
439
|
|
@@ -381,6 +504,10 @@ class FileUpload extends Base {
|
|
381
504
|
me.abortUpload();
|
382
505
|
break;
|
383
506
|
|
507
|
+
// While processing we just have to wait until it's succeeded or failed..
|
508
|
+
case 'processing':
|
509
|
+
break;
|
510
|
+
|
384
511
|
// If the upload or the scan failed, the document will not have been
|
385
512
|
// saved, so we just go back to ready state
|
386
513
|
case 'upload-failed':
|
@@ -389,13 +516,17 @@ class FileUpload extends Base {
|
|
389
516
|
me.state = 'ready';
|
390
517
|
break;
|
391
518
|
|
392
|
-
//
|
519
|
+
// For stored documents, we need to tell the server the document
|
393
520
|
// is not required.
|
394
521
|
case 'processing':
|
395
522
|
case 'downloadable':
|
396
523
|
case 'not-downloadable':
|
397
524
|
me.deleteDocument();
|
398
525
|
break;
|
526
|
+
case 'deleted':
|
527
|
+
me.clear();
|
528
|
+
me.state = 'ready';
|
529
|
+
break;
|
399
530
|
}
|
400
531
|
}
|
401
532
|
|
@@ -404,43 +535,88 @@ class FileUpload extends Base {
|
|
404
535
|
}
|
405
536
|
|
406
537
|
async deleteDocument() {
|
538
|
+
const
|
539
|
+
me = this,
|
540
|
+
{ headers } = me;
|
541
|
+
|
542
|
+
me.fire('beforeRequest', {
|
543
|
+
headers
|
544
|
+
});
|
545
|
+
|
407
546
|
// We ask the server to delete using our this.documentId
|
408
|
-
const statusResponse = await fetch(
|
547
|
+
const statusResponse = await fetch(me.documentDeleteUrl, {
|
548
|
+
headers
|
549
|
+
});
|
409
550
|
|
410
551
|
// Success
|
411
552
|
if (String(statusResponse.status).slice(0, 1) === '2') {
|
412
|
-
|
413
|
-
|
553
|
+
me.clear();
|
554
|
+
me.state = 'ready';
|
414
555
|
}
|
415
556
|
else {
|
416
|
-
|
557
|
+
me.error = `${me.documentDeleteError}: ${statusResponse.statusText}`;
|
417
558
|
}
|
418
559
|
}
|
419
560
|
|
420
561
|
async checkDocumentStatus() {
|
421
|
-
const
|
562
|
+
const
|
563
|
+
me = this,
|
564
|
+
{ headers } = me;
|
422
565
|
|
423
|
-
if (
|
424
|
-
|
566
|
+
if (me.state === 'processing') {
|
567
|
+
me.fire('beforeRequest', {
|
568
|
+
headers
|
569
|
+
});
|
570
|
+
|
571
|
+
const statusResponse = await fetch(me.documentStatusUrl, {
|
572
|
+
headers
|
573
|
+
});
|
425
574
|
|
426
575
|
// Success
|
427
576
|
if (String(statusResponse.status).slice(0, 1) === '2') {
|
428
|
-
const
|
577
|
+
const
|
578
|
+
serverJson = await statusResponse.json(),
|
579
|
+
serverStatus = serverJson.status,
|
580
|
+
// Map the server's states codes to our own status codes
|
581
|
+
status = me.documentStatusMap[serverStatus] || serverStatus;
|
429
582
|
|
430
583
|
switch (status) {
|
431
584
|
case 'scanning':
|
432
|
-
setTimeout(() => me.checkDocumentStatus(),
|
585
|
+
setTimeout(() => me.checkDocumentStatus(), me.statusScanInterval);
|
586
|
+
break;
|
587
|
+
case 'deleted':
|
588
|
+
me.error = `${me.documentText} ${me.documentId} ${isNoLongerAvailable}`;
|
589
|
+
me.state = 'ready';
|
433
590
|
break;
|
434
591
|
default:
|
592
|
+
const { fileName, size } = serverJson;
|
593
|
+
|
594
|
+
if (fileName) {
|
595
|
+
me.vdom.cn[1].cn[0].innerHTML = fileName;
|
596
|
+
me.fileSize = me.formatSize(size);
|
597
|
+
}
|
435
598
|
me.state = status;
|
436
599
|
}
|
437
600
|
}
|
438
601
|
else {
|
439
|
-
|
602
|
+
me.error = `${documentStatusError}: ${statusResponse.statusText}`;
|
440
603
|
}
|
441
604
|
}
|
442
605
|
}
|
443
606
|
|
607
|
+
afterSetDocument(document) {
|
608
|
+
if (document) {
|
609
|
+
const
|
610
|
+
me = this;
|
611
|
+
|
612
|
+
me.preExistingDocument = true;
|
613
|
+
me.documentId = document.id;
|
614
|
+
me.fileSize = me.formatSize(document.size);
|
615
|
+
me.vdom.cn[1].cn[0].innerHTML = document.fileName;
|
616
|
+
me.state = me.documentStatusMap[document.status];
|
617
|
+
}
|
618
|
+
}
|
619
|
+
|
444
620
|
/**
|
445
621
|
* Triggered after the state config got changed
|
446
622
|
* @param {String} value
|
@@ -456,30 +632,40 @@ class FileUpload extends Base {
|
|
456
632
|
anchor = vdom.cn[1].cn[0],
|
457
633
|
status = vdom.cn[1].cn[1];
|
458
634
|
|
635
|
+
delete vdom.inert;
|
636
|
+
|
459
637
|
switch (value) {
|
460
638
|
case 'ready':
|
461
639
|
anchor.tag = 'div';
|
462
640
|
anchor.href = '';
|
463
641
|
break;
|
464
642
|
case 'upload-failed':
|
465
|
-
status.innerHTML =
|
643
|
+
status.innerHTML = `${me.uploadFailed}... (${Math.round(me.progress * 100)}%)`;
|
466
644
|
break;
|
467
645
|
case 'processing':
|
468
|
-
status.innerHTML =
|
646
|
+
status.innerHTML = `${me.scanning}... (${me.formatSize(me.uploadSize)})`;
|
647
|
+
vdom.inert = true;
|
469
648
|
break;
|
470
649
|
case 'scan-failed':
|
471
|
-
status.innerHTML =
|
472
|
-
me.error =
|
650
|
+
status.innerHTML = `${me.malwareFoundInFile}. \u2022 ${me.fileSize}`;
|
651
|
+
me.error = me.pleaseCheck;
|
473
652
|
break;
|
474
653
|
case 'downloadable':
|
475
654
|
anchor.tag = 'a';
|
476
|
-
anchor.href =
|
655
|
+
anchor.href = me.createUrl(me.downloadUrl, {
|
656
|
+
[me.documentIdParameter] : me.documentId
|
657
|
+
});
|
477
658
|
status.innerHTML = me.fileSize;
|
478
659
|
break;
|
479
660
|
case 'not-downloadable':
|
480
|
-
status.innerHTML =
|
661
|
+
status.innerHTML = me.preExistingDocument ?
|
662
|
+
me.fileSize : `${successfullyUploaded} \u2022 ${me.fileSize}`;
|
663
|
+
break;
|
664
|
+
case 'deleted':
|
665
|
+
status.innerHTML = me.fileWasDeleted;
|
481
666
|
}
|
482
667
|
|
668
|
+
me.validate();
|
483
669
|
me.update();
|
484
670
|
|
485
671
|
// Processing above may mutate cls
|
@@ -490,6 +676,41 @@ class FileUpload extends Base {
|
|
490
676
|
me.cls = cls;
|
491
677
|
}
|
492
678
|
|
679
|
+
/**
|
680
|
+
* Creates a URL substituting the passed parameter names in at the places where the name
|
681
|
+
* occurs within `{}` in the pattern.
|
682
|
+
* @param {String} urlPattern
|
683
|
+
* @param {Object} params
|
684
|
+
*/
|
685
|
+
createUrl(urlPattern, params) {
|
686
|
+
for (const paramName in params) {
|
687
|
+
urlPattern = urlPattern.replace(`{${paramName}}`, params[paramName]);
|
688
|
+
}
|
689
|
+
return urlPattern;
|
690
|
+
}
|
691
|
+
|
692
|
+
beforeGetHeaders(headers) {
|
693
|
+
return { ...(headers || {}) }
|
694
|
+
}
|
695
|
+
|
696
|
+
beforeGetDocumentStatusUrl(documentStatusUrl) {
|
697
|
+
return typeof documentStatusUrl === 'function'? documentStatusUrl.call(me, me) : me.createUrl(documentStatusUrl, {
|
698
|
+
[me.documentIdParameter] : me.documentId
|
699
|
+
});
|
700
|
+
}
|
701
|
+
|
702
|
+
beforeGetDocumentDeleteUrl(documentDeleteUrl) {
|
703
|
+
return typeof documentDeleteUrl === 'function'? documentDeleteUrl.call(me, me) : me.createUrl(documentDeleteUrl, {
|
704
|
+
[me.documentIdParameter] : me.documentId
|
705
|
+
});
|
706
|
+
}
|
707
|
+
|
708
|
+
beforeGetDownloadUrl(downloadUrl) {
|
709
|
+
return typeof downloadUrl === 'function'? downloadUrl.call(me, me) : me.createUrl(downloadUrl, {
|
710
|
+
[me.documentIdParameter] : me.documentId
|
711
|
+
});
|
712
|
+
}
|
713
|
+
|
493
714
|
beforeGetMaxSize(maxSize) {
|
494
715
|
// Not configured means no limit
|
495
716
|
if (maxSize == null) {
|
@@ -507,21 +728,18 @@ class FileUpload extends Base {
|
|
507
728
|
}
|
508
729
|
}
|
509
730
|
|
510
|
-
|
511
|
-
const { cls } = this;
|
512
|
-
|
731
|
+
afterSetError(text) {
|
513
732
|
if (text) {
|
514
733
|
this.vdom.cn[4].cn = [{
|
515
734
|
vtype : 'text',
|
516
735
|
html : text
|
517
736
|
}];
|
518
|
-
NeoArray.add(cls, 'neo-invalid');
|
519
737
|
}
|
520
738
|
else {
|
521
|
-
|
739
|
+
this.vdom.cn[4].cn = [];
|
522
740
|
}
|
523
741
|
|
524
|
-
this.
|
742
|
+
this.validate();
|
525
743
|
this.update();
|
526
744
|
}
|
527
745
|
|
@@ -535,6 +753,27 @@ class FileUpload extends Base {
|
|
535
753
|
}
|
536
754
|
return 'n/a';
|
537
755
|
}
|
756
|
+
|
757
|
+
/**
|
758
|
+
* @returns {Boolean}
|
759
|
+
*/
|
760
|
+
validate() {
|
761
|
+
const { isValid, cls } = this;
|
762
|
+
|
763
|
+
NeoArray.toggle(cls, 'neo-invalid', !isValid);
|
764
|
+
this.cls = cls;
|
765
|
+
|
766
|
+
return isValid;
|
767
|
+
}
|
768
|
+
|
769
|
+
get isValid() {
|
770
|
+
const me = this;
|
771
|
+
|
772
|
+
return !me.error &&
|
773
|
+
((me.state === 'ready' && !me.required) ||
|
774
|
+
(me.state === 'downloadable') ||
|
775
|
+
(me.state === 'not-downloadable'));
|
776
|
+
}
|
538
777
|
}
|
539
778
|
|
540
779
|
Neo.applyClassConfig(FileUpload);
|
package/src/form/field/Text.mjs
CHANGED
@@ -665,7 +665,7 @@ class Text extends Base {
|
|
665
665
|
|
666
666
|
me.silentVdomUpdate = true;
|
667
667
|
|
668
|
-
me.validate(false);
|
668
|
+
!me.clean && me.validate(false);
|
669
669
|
me.changeInputElKey('required', value ? value : null);
|
670
670
|
me.labelText = me.labelText; // apply the optional text if needed
|
671
671
|
|