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.
@@ -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 #member-uploadUrl} property.
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
- uploadUrl_ : null,
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 HTTP parameter which is used when requesting a malware scan and a document
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
- downloadUrl_ : null,
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.vdom.cn[3].id);
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 = `Please use these file types: .${Object.keys(types).join(' .')}`;
374
+ me.error = `${me.pleaseUseTheseTypes}: .${Object.keys(types).join(' .')}`;
269
375
  }
270
376
  else if (file.size > me.maxSize) {
271
- me.error = `File size exceeds ${String(me._maxSize).toUpperCase()}`;
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
- // During scanning and for stored documents, we need to tell the server the document
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(`${this.documentDeleteUrl}?${this.documentIdParameter}=${this.documentId}`);
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
- this.clear();
413
- this.state = 'ready';
553
+ me.clear();
554
+ me.state = 'ready';
414
555
  }
415
556
  else {
416
- this.error = `Document delete service error: ${statusResponse.statusText}`;
557
+ me.error = `${me.documentDeleteError}: ${statusResponse.statusText}`;
417
558
  }
418
559
  }
419
560
 
420
561
  async checkDocumentStatus() {
421
- const me = this;
562
+ const
563
+ me = this,
564
+ { headers } = me;
422
565
 
423
- if (this.state === 'processing') {
424
- const statusResponse = await fetch(`${this.documentStatusUrl}?${me.documentIdParameter}=${this.documentId}`);
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 status = (await statusResponse.json()).status;
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(), 2000);
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
- this.error = `Document status service error: ${statusResponse.statusText}`;
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 = `Upload failed... (${Math.round(me.progress * 100)}%)`;
643
+ status.innerHTML = `${me.uploadFailed}... (${Math.round(me.progress * 100)}%)`;
466
644
  break;
467
645
  case 'processing':
468
- status.innerHTML = `Scanning... (${me.formatSize(me.uploadSize)})`;
646
+ status.innerHTML = `${me.scanning}... (${me.formatSize(me.uploadSize)})`;
647
+ vdom.inert = true;
469
648
  break;
470
649
  case 'scan-failed':
471
- status.innerHTML = `Malware found in file. \u2022 ${me.fileSize}`;
472
- me.error = 'Please check the file and try again';
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 = `${me.downloadUrl}?${me.documentIdParameter}=${me.documentId}`;
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 = `Successfully uploaded \u2022 ${me.fileSize}`;
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
- set error(text) {
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
- NeoArray.remove(cls, 'neo-invalid');
739
+ this.vdom.cn[4].cn = [];
522
740
  }
523
741
 
524
- this.cls = cls;
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);
@@ -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