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.
- package/apps/ServiceWorker.mjs +2 -2
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/form/field/fileupload/MainContainer.mjs +27 -2
- 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/examples/treeSelectionModel/MainContainer.scss +24 -0
- package/resources/scss/src/form/field/FileUpload.scss +16 -2
- package/resources/scss/src/tree/Accordion.scss +128 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/core/Base.mjs +6 -0
- package/src/form/field/FileUpload.mjs +139 -49
- 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
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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.
|
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 =
|
374
|
+
me.error = `${me.pleaseUseTheseTypes}: .${Object.keys(types).join(' .')}`;
|
350
375
|
}
|
351
376
|
else if (file.size > me.maxSize) {
|
352
|
-
me.error =
|
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
|
-
//
|
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
|
508
|
-
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(
|
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 =
|
557
|
+
me.error = `${me.documentDeleteError}: ${statusResponse.statusText}`;
|
529
558
|
}
|
530
559
|
}
|
531
560
|
|
532
561
|
async checkDocumentStatus() {
|
533
562
|
const
|
534
|
-
me
|
535
|
-
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(
|
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 =
|
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 =
|
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 =
|
638
|
+
status.innerHTML = `${me.uploadFailed}... (${Math.round(me.progress * 100)}%)`;
|
603
639
|
break;
|
604
640
|
case 'processing':
|
605
|
-
status.innerHTML =
|
641
|
+
status.innerHTML = `${me.scanning}... (${me.formatSize(me.uploadSize)})`;
|
642
|
+
vdom.inert = true;
|
606
643
|
break;
|
607
644
|
case 'scan-failed':
|
608
|
-
status.innerHTML =
|
609
|
-
me.error =
|
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 =
|
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
|
-
|
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
|
-
|
740
|
+
this.vdom.cn[4].cn = [];
|
674
741
|
}
|
675
742
|
|
676
|
-
this.
|
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
|
-
|
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
|
-
|
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;
|