neo.mjs 5.16.5 → 5.17.1

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.
@@ -143,12 +143,19 @@ class FileUpload extends Base {
143
143
  */
144
144
  documentStatusMap : {
145
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
+
146
151
  MALWARE_DETECTED : 'scan-failed',
147
152
  UN_DOWNLOADABLE : 'not-downloadable',
148
153
  DOWNLOADABLE : 'downloadable',
149
154
  DELETED : 'deleted'
150
155
  },
151
156
 
157
+ document_ : null,
158
+
152
159
  /**
153
160
  * If this widget should reference an existing document, configure the widget with a documentId
154
161
  * so that it can initialize in the correct "uploaded" state.
@@ -178,7 +185,7 @@ class FileUpload extends Base {
178
185
  * The document status request URL must be configured in {@link #member-documentStatusUrl}
179
186
  * @member {String} uploadUrl
180
187
  */
181
- uploadUrl_ : null,
188
+ uploadUrl : null,
182
189
 
183
190
  /**
184
191
  * The name of the JSON property in which the document id is returned in the upload response
@@ -239,7 +246,7 @@ class FileUpload extends Base {
239
246
  *
240
247
  * @member {String} documentStatusUrl
241
248
  */
242
- documentStatusUrl : null,
249
+ documentStatusUrl_ : null,
243
250
 
244
251
  /**
245
252
  * The polling interval *in milliseconds* to wait between asking the server how the document scan
@@ -272,9 +279,7 @@ class FileUpload extends Base {
272
279
  *
273
280
  * @member {String} documentDeleteUrl
274
281
  */
275
- documentDeleteUrl : null,
276
-
277
- headers_ : {},
282
+ documentDeleteUrl_ : null,
278
283
 
279
284
  /**
280
285
  * @member {String} state_=null
@@ -289,7 +294,27 @@ class FileUpload extends Base {
289
294
  /**
290
295
  * @member {String|Number} maxSize
291
296
  */
292
- 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'
293
318
  }
294
319
 
295
320
  /**
@@ -304,13 +329,13 @@ class FileUpload extends Base {
304
329
  { input : me.onInputValueChange, scope: me},
305
330
  { click : me.onActionButtonClick, delegate : '.neo-file-upload-action-button', scope : me}
306
331
  ]);
332
+ }
307
333
 
308
- // If we are to reference an existing document, start by asking the server about its
309
- // state. Widget state will proceed from there.
310
- if (me.documentId) {
311
- me.state = 'processing';
312
- me.checkDocumentStatus();
313
- }
334
+ /**
335
+ * @returns {Object}
336
+ */
337
+ getInputEl() {
338
+ return this.vdom.cn[3];
314
339
  }
315
340
 
316
341
  async clear() {
@@ -327,7 +352,7 @@ class FileUpload extends Base {
327
352
 
328
353
  // We have to wait for the DOM to have changed, and the input field to be visible
329
354
  await new Promise(resolve => setTimeout(resolve, 100));
330
- me.focus(me.vdom.cn[3].id);
355
+ me.focus(me.getInputEl().id);
331
356
  }
332
357
 
333
358
  /**
@@ -346,10 +371,10 @@ class FileUpload extends Base {
346
371
  type = pointPos > -1 ? file.name.slice(pointPos + 1) : '';
347
372
 
348
373
  if (me.types && !types[type]) {
349
- me.error = `Please use these file types: .${Object.keys(types).join(' .')}`;
374
+ me.error = `${me.pleaseUseTheseTypes}: .${Object.keys(types).join(' .')}`;
350
375
  }
351
376
  else if (file.size > me.maxSize) {
352
- me.error = `File size exceeds ${String(me._maxSize).toUpperCase()}`;
377
+ me.error = `${me.fileSizeMoreThan} ${String(me._maxSize).toUpperCase()}`;
353
378
  }
354
379
  // If it passes the type and maxSize check, upload it
355
380
  else {
@@ -395,7 +420,6 @@ class FileUpload extends Base {
395
420
 
396
421
  /**
397
422
  * This event fires before every HTTP request is sent to the server via any of the configured URLs.
398
- *
399
423
  *
400
424
  * @event beforeRequest
401
425
  * @param {Object} event The event
@@ -480,6 +504,10 @@ class FileUpload extends Base {
480
504
  me.abortUpload();
481
505
  break;
482
506
 
507
+ // While processing we just have to wait until it's succeeded or failed..
508
+ case 'processing':
509
+ break;
510
+
483
511
  // If the upload or the scan failed, the document will not have been
484
512
  // saved, so we just go back to ready state
485
513
  case 'upload-failed':
@@ -488,13 +516,17 @@ class FileUpload extends Base {
488
516
  me.state = 'ready';
489
517
  break;
490
518
 
491
- // 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
492
520
  // is not required.
493
521
  case 'processing':
494
522
  case 'downloadable':
495
523
  case 'not-downloadable':
496
524
  me.deleteDocument();
497
525
  break;
526
+ case 'deleted':
527
+ me.clear();
528
+ me.state = 'ready';
529
+ break;
498
530
  }
499
531
  }
500
532
 
@@ -504,18 +536,15 @@ class FileUpload extends Base {
504
536
 
505
537
  async deleteDocument() {
506
538
  const
507
- me = this,
508
- headers = { ...me.headers },
509
- url = me.createUrl(me.documentDeleteUrl, {
510
- [me.documentIdParameter] : me.documentId
511
- });
539
+ me = this,
540
+ { headers } = me;
512
541
 
513
542
  me.fire('beforeRequest', {
514
543
  headers
515
544
  });
516
545
 
517
546
  // We ask the server to delete using our this.documentId
518
- const statusResponse = await fetch(url, {
547
+ const statusResponse = await fetch(me.documentDeleteUrl, {
519
548
  headers
520
549
  });
521
550
 
@@ -525,24 +554,21 @@ class FileUpload extends Base {
525
554
  me.state = 'ready';
526
555
  }
527
556
  else {
528
- me.error = `Document delete service error: ${statusResponse.statusText}`;
557
+ me.error = `${me.documentDeleteError}: ${statusResponse.statusText}`;
529
558
  }
530
559
  }
531
560
 
532
561
  async checkDocumentStatus() {
533
562
  const
534
- me = this,
535
- headers = { ...me.headers },
536
- url = me.createUrl(me.documentStatusUrl, {
537
- [me.documentIdParameter] : me.documentId
538
- });
563
+ me = this,
564
+ { headers } = me;
539
565
 
540
566
  if (me.state === 'processing') {
541
567
  me.fire('beforeRequest', {
542
568
  headers
543
569
  });
544
570
 
545
- const statusResponse = await fetch(url, {
571
+ const statusResponse = await fetch(me.documentStatusUrl, {
546
572
  headers
547
573
  });
548
574
 
@@ -559,25 +585,33 @@ class FileUpload extends Base {
559
585
  setTimeout(() => me.checkDocumentStatus(), me.statusScanInterval);
560
586
  break;
561
587
  case 'deleted':
562
- me.error = `Document ${me.documentId} is no longer available`;
588
+ me.error = `${me.documentText} ${me.documentId} ${isNoLongerAvailable}`;
563
589
  me.state = 'ready';
564
590
  break;
565
591
  default:
566
- const { fileName, size } = serverJson;
567
-
568
- if (fileName) {
569
- me.vdom.cn[1].cn[0].innerHTML = fileName;
570
- me.fileSize = me.formatSize(size);
571
- }
572
592
  me.state = status;
573
593
  }
574
594
  }
575
595
  else {
576
- me.error = `Document status service error: ${statusResponse.statusText}`;
596
+ me.error = `${me.documentStatusError}: ${statusResponse.statusText || `Server error ${statusResponse.status}`}`;
597
+ me.state = 'deleted';
577
598
  }
578
599
  }
579
600
  }
580
601
 
602
+ afterSetDocument(document) {
603
+ if (document) {
604
+ const
605
+ me = this;
606
+
607
+ me.preExistingDocument = true;
608
+ me.documentId = document.id;
609
+ me.fileSize = me.formatSize(document.size);
610
+ me.vdom.cn[1].cn[0].innerHTML = document.fileName;
611
+ me.state = me.documentStatusMap[document.status];
612
+ }
613
+ }
614
+
581
615
  /**
582
616
  * Triggered after the state config got changed
583
617
  * @param {String} value
@@ -593,20 +627,23 @@ class FileUpload extends Base {
593
627
  anchor = vdom.cn[1].cn[0],
594
628
  status = vdom.cn[1].cn[1];
595
629
 
630
+ delete vdom.inert;
631
+
596
632
  switch (value) {
597
633
  case 'ready':
598
634
  anchor.tag = 'div';
599
635
  anchor.href = '';
600
636
  break;
601
637
  case 'upload-failed':
602
- status.innerHTML = `Upload failed... (${Math.round(me.progress * 100)}%)`;
638
+ status.innerHTML = `${me.uploadFailed}... (${Math.round(me.progress * 100)}%)`;
603
639
  break;
604
640
  case 'processing':
605
- status.innerHTML = `Scanning... (${me.formatSize(me.uploadSize)})`;
641
+ status.innerHTML = `${me.scanning}... (${me.formatSize(me.uploadSize)})`;
642
+ vdom.inert = true;
606
643
  break;
607
644
  case 'scan-failed':
608
- status.innerHTML = `Malware found in file. \u2022 ${me.fileSize}`;
609
- me.error = 'Please check the file and try again';
645
+ status.innerHTML = `${me.malwareFoundInFile}. \u2022 ${me.fileSize}`;
646
+ me.error = me.pleaseCheck;
610
647
  break;
611
648
  case 'downloadable':
612
649
  anchor.tag = 'a';
@@ -616,9 +653,14 @@ class FileUpload extends Base {
616
653
  status.innerHTML = me.fileSize;
617
654
  break;
618
655
  case 'not-downloadable':
619
- status.innerHTML = `Successfully uploaded \u2022 ${me.fileSize}`;
656
+ status.innerHTML = me.preExistingDocument ?
657
+ me.fileSize : `${me.successfullyUploaded} \u2022 ${me.fileSize}`;
658
+ break;
659
+ case 'deleted':
660
+ status.innerHTML = me.fileWasDeleted;
620
661
  }
621
662
 
663
+ me.validate();
622
664
  me.update();
623
665
 
624
666
  // Processing above may mutate cls
@@ -642,6 +684,34 @@ class FileUpload extends Base {
642
684
  return urlPattern;
643
685
  }
644
686
 
687
+ beforeGetHeaders(headers) {
688
+ return { ...(headers || {}) }
689
+ }
690
+
691
+ beforeGetDocumentStatusUrl(documentStatusUrl) {
692
+ const me = this;
693
+
694
+ return typeof documentStatusUrl === 'function'? documentStatusUrl.call(me, me) : me.createUrl(documentStatusUrl, {
695
+ [me.documentIdParameter] : me.documentId
696
+ });
697
+ }
698
+
699
+ beforeGetDocumentDeleteUrl(documentDeleteUrl) {
700
+ const me = this;
701
+
702
+ return typeof documentDeleteUrl === 'function'? documentDeleteUrl.call(me, me) : me.createUrl(documentDeleteUrl, {
703
+ [me.documentIdParameter] : me.documentId
704
+ });
705
+ }
706
+
707
+ beforeGetDownloadUrl(downloadUrl) {
708
+ const me = this;
709
+
710
+ return typeof downloadUrl === 'function'? downloadUrl.call(me, me) : me.createUrl(downloadUrl, {
711
+ [me.documentIdParameter] : me.documentId
712
+ });
713
+ }
714
+
645
715
  beforeGetMaxSize(maxSize) {
646
716
  // Not configured means no limit
647
717
  if (maxSize == null) {
@@ -659,21 +729,18 @@ class FileUpload extends Base {
659
729
  }
660
730
  }
661
731
 
662
- set error(text) {
663
- const { cls } = this;
664
-
732
+ afterSetError(text) {
665
733
  if (text) {
666
734
  this.vdom.cn[4].cn = [{
667
735
  vtype : 'text',
668
736
  html : text
669
737
  }];
670
- NeoArray.add(cls, 'neo-invalid');
671
738
  }
672
739
  else {
673
- NeoArray.remove(cls, 'neo-invalid');
740
+ this.vdom.cn[4].cn = [];
674
741
  }
675
742
 
676
- this.cls = cls;
743
+ this.validate();
677
744
  this.update();
678
745
  }
679
746
 
@@ -687,6 +754,29 @@ class FileUpload extends Base {
687
754
  }
688
755
  return 'n/a';
689
756
  }
757
+
758
+ /**
759
+ * @returns {Boolean}
760
+ */
761
+ validate() {
762
+ const
763
+ { cls } = this,
764
+ isValid = this.isValid();
765
+
766
+ NeoArray.toggle(cls, 'neo-invalid', !isValid);
767
+ this.cls = cls;
768
+
769
+ return isValid;
770
+ }
771
+
772
+ isValid() {
773
+ const me = this;
774
+
775
+ return !me.error && !(me.state === 'ready' && me.required) ||
776
+ ( (me.state === 'downloadable') ||
777
+ (me.state === 'not-downloadable')
778
+ );
779
+ }
690
780
  }
691
781
 
692
782
  Neo.applyClassConfig(FileUpload);
@@ -0,0 +1,293 @@
1
+ import TreeModel from './TreeModel.mjs';
2
+ import NeoArray from "../util/Array.mjs";
3
+
4
+ /**
5
+ * @class Neo.selection.TreeAccordionModel
6
+ * @extends Neo.selection.TreeModel
7
+ */
8
+ class TreeAccordionModel extends TreeModel {
9
+ static config = {
10
+ /**
11
+ * @member {String} className='Neo.selection.TreeAccordionModel'
12
+ * @protected
13
+ */
14
+ className: 'Neo.selection.TreeAccordionModel',
15
+ /**
16
+ * @member {String} ntype='selection-treeaccordionmodel'
17
+ * @protected
18
+ */
19
+ ntype: 'selection-treeaccordionmodel'
20
+ }
21
+
22
+ /**
23
+ * Tries to find a child and returns it
24
+ * @param {Object} record
25
+ * @returns {Object|null}
26
+ */
27
+ checkForChild(record) {
28
+ const view = this.view,
29
+ recordId = record[view.getKeyProperty()];
30
+ let childRecord = null;
31
+
32
+ for (const item of view.store.items) {
33
+ if (item.parentId === recordId) {
34
+ childRecord = item;
35
+ break;
36
+ }
37
+ }
38
+
39
+ return childRecord;
40
+ }
41
+
42
+ /**
43
+ * Return the parent record if any
44
+ * @param {Object} record
45
+ * @returns {Object|null}
46
+ */
47
+ checkForParent(record) {
48
+ if (record.parentId) {
49
+ const view = this.view;
50
+
51
+ return view.store.get(record.parentId);
52
+ } else {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Depending on {-1|1} step return
59
+ * -1: previous record OR parent record
60
+ * 1: next record or null
61
+ *
62
+ * @param {Object} record
63
+ * @param {Number} step
64
+ * @returns {Object|null}
65
+ */
66
+ checkForSibling(record, step) {
67
+ const view = this.view,
68
+ store = view.store,
69
+ parentRecordId = record.parentId,
70
+ recordId = record[view.getKeyProperty()];
71
+ let hasFoundNext = false,
72
+ nextItemRecord = null,
73
+ previousItemRecord;
74
+
75
+ for (let item of store.items) {
76
+ if (hasFoundNext && item.parentId === parentRecordId) {
77
+ nextItemRecord = item;
78
+ break;
79
+ }
80
+
81
+ if (!hasFoundNext && item.parentId === parentRecordId) {
82
+ if (!hasFoundNext && item[view.getKeyProperty()] === recordId) {
83
+ if (step === -1) break;
84
+ hasFoundNext = true;
85
+ } else {
86
+ previousItemRecord = item;
87
+ }
88
+ }
89
+ }
90
+
91
+ return step === 1 ? nextItemRecord : (previousItemRecord || store.get(parentRecordId));
92
+ }
93
+
94
+ /**
95
+ * Find the next sibling of a parent item
96
+ * @param {Object} record
97
+ * @returns {Object|null}
98
+ */
99
+ checkNextParentSibling(record) {
100
+ const parent = this.view.store.get(record.parentId);
101
+ let parentSibling = this.checkForSibling(parent, 1);
102
+
103
+ if (!parentSibling && parent.parentId) this.checkNextParentSibling(parent);
104
+
105
+ return parentSibling;
106
+ }
107
+
108
+ /**
109
+ * Called by keys (List.mjs:register)
110
+ * Toggle collapse or if isLeaf select next item
111
+ * @param {Object} data
112
+ */
113
+ onKeyDownEnter(data) {
114
+ let me = this,
115
+ view = me.view,
116
+ itemId = me.getSelection()[0],
117
+ record = view.store.get(view.getItemRecordId(itemId));
118
+
119
+ if (record.isLeaf || record.collapsed) {
120
+ me.onKeyDownRight(data);
121
+ } else {
122
+ me.onKeyDownLeft(data);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Called by keys (List.mjs:register)
128
+ * Deselect all and fire event selectPostLastItem
129
+ * @param {Object} data
130
+ */
131
+ onKeyDownEscape(data) {
132
+ let me = this;
133
+
134
+ me.deselectAll();
135
+ }
136
+
137
+ /**
138
+ * Collapse folder or select previous
139
+ * @param {Object} data
140
+ */
141
+ onKeyDownLeft(data) {
142
+ const me = this,
143
+ view = me.view,
144
+ itemId = me.getSelection()[0];
145
+
146
+ if (!itemId) {
147
+ me.selectRoot();
148
+ return;
149
+ }
150
+
151
+ const record = view.store.get(view.getItemRecordId(itemId));
152
+
153
+ if (record.isLeaf || record.collapsed || !view.rootParentsAreCollapsible) {
154
+ me.onNavKey(data, -1);
155
+ } else {
156
+ me.toggleCollapsed(record, itemId, true);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Open folder or select next
162
+ * @param {Object} data
163
+ */
164
+ onKeyDownRight(data) {
165
+ const me = this,
166
+ view = me.view,
167
+ itemId = me.getSelection()[0];
168
+
169
+ if (!itemId) {
170
+ me.selectRoot();
171
+ return;
172
+ }
173
+
174
+ const record = view.store.get(view.getItemRecordId(itemId));
175
+
176
+ if (record.isLeaf || !record.collapsed) {
177
+ me.onNavKey(data, 1);
178
+ } else {
179
+ me.toggleCollapsed(record, itemId, false);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handles 'up' and 'down' keys
185
+ * @param {Object} data
186
+ * @param {Number} step
187
+ */
188
+ onNavKey(data, step) {
189
+ const me = this,
190
+ view = me.view,
191
+ item = me.getSelection()[0];
192
+ let newRecord;
193
+
194
+ if (item) {
195
+ const recordId = view.getItemRecordId(item);
196
+ let record = view.store.get(recordId);
197
+
198
+ if (step === 1) {
199
+ if (!record.isLeaf && !record.collapsed) {
200
+ // find first child
201
+ newRecord = this.checkForChild(record);
202
+ } else {
203
+ // find next sibling
204
+ newRecord = this.checkForSibling(record, step);
205
+ // no ==> loop through parent next siblings until no parent
206
+ if (!newRecord) {
207
+ newRecord = this.checkNextParentSibling(record);
208
+ }
209
+ }
210
+ // current item was the last item
211
+ if (!newRecord) {
212
+ me.deselectAll();
213
+ view.fire('selectPostLastItem');
214
+ }
215
+ } else if (step === -1) {
216
+ // check previous sibling
217
+ newRecord = this.checkForSibling(record, step);
218
+ // no ==> get parent
219
+ if (!newRecord) {
220
+ newRecord = this.checkForParent(record);
221
+ }
222
+ // current item was the first item
223
+ if (!newRecord) {
224
+ me.deselectAll();
225
+ view.fire('selectPreFirstItem');
226
+ }
227
+ }
228
+ } else {
229
+ me.selectRoot();
230
+ }
231
+
232
+ if (newRecord) {
233
+ const itemId = view.getItemId(newRecord[me.view.getKeyProperty()]);
234
+
235
+ me.selectAndScrollIntoView(itemId);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Select an item and scroll the tree to show the item in the center
241
+ * @param {String} itemId
242
+ */
243
+ selectAndScrollIntoView(itemId) {
244
+ const me = this;
245
+
246
+ me.select(itemId);
247
+
248
+ Neo.main.DomAccess.scrollIntoView({
249
+ id : itemId,
250
+ block: 'center'
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Select the root item of the tree
256
+ */
257
+ selectRoot() {
258
+ const me = this,
259
+ view = me.view,
260
+ store = view.store;
261
+ let rootItemId;
262
+
263
+ for (let record of store.items) {
264
+ if (!record.parentId) {
265
+ rootItemId = view.getItemId(record[me.view.getKeyProperty()]);
266
+ break;
267
+ }
268
+ }
269
+
270
+ me.selectAndScrollIntoView(rootItemId);
271
+ }
272
+
273
+ /**
274
+ * Return the parent record if any
275
+ * @param {Object} record
276
+ * @param {String} itemId
277
+ * @param {Boolean} collapse
278
+ */
279
+ toggleCollapsed(record, itemId, collapse) {
280
+ const me = this,
281
+ item = me.view.getVdomChild(itemId),
282
+ clsFn = collapse ? 'remove' : 'add';
283
+
284
+ NeoArray[clsFn](item.cls, 'neo-folder-open');
285
+ me.view.update();
286
+
287
+ record.collapsed = collapse;
288
+ }
289
+ }
290
+
291
+ Neo.applyClassConfig(TreeAccordionModel);
292
+
293
+ export default TreeAccordionModel;
@@ -22,7 +22,7 @@ class TreeModel extends ListModel {
22
22
  * @param {Object} data
23
23
  */
24
24
  onKeyDownEnter(data) {
25
- console.log('onKeyDownEnter', data)
25
+ Neo.log('onKeyDownEnter', data)
26
26
  }
27
27
 
28
28
  /**
@@ -30,10 +30,10 @@ class TreeModel extends ListModel {
30
30
  * @param {Number} step
31
31
  */
32
32
  onNavKey(data, step) {
33
- console.log('onNavKey', data, step)
33
+ Neo.log('onNavKey', data, step)
34
34
  }
35
35
  }
36
36
 
37
37
  Neo.applyClassConfig(TreeModel);
38
38
 
39
- export default TreeModel;
39
+ export default TreeModel;