neo.mjs 6.0.2 → 6.0.4

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.
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.0.2'
23
+ * @member {String} version='6.0.4'
24
24
  */
25
- version: '6.0.2'
25
+ version: '6.0.4'
26
26
  }
27
27
 
28
28
  /**
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.0.2'
23
+ * @member {String} version='6.0.4'
24
24
  */
25
- version: '6.0.2'
25
+ version: '6.0.4'
26
26
  }
27
27
 
28
28
  /**
@@ -1,9 +1,13 @@
1
1
  import ConfigurationViewport from '../ConfigurationViewport.mjs';
2
2
 
3
- import Store from '../../src/data/Store.mjs';
4
- import NumberField from '../../src/form/field/Number.mjs';
5
- import AccordionTree from '../../src/tree/Accordion.mjs';
6
- import CheckBox from "../../src/form/field/CheckBox.mjs";
3
+ import AccordionTree from '../../src/tree/Accordion.mjs';
4
+ import CheckBox from "../../src/form/field/CheckBox.mjs";
5
+ import NumberField from '../../src/form/field/Number.mjs';
6
+ import Panel from '../../src/container/Panel.mjs';
7
+ import Store from '../../src/data/Store.mjs';
8
+ // Do not remove the ViewController nor ViewModel
9
+ import ViewController from '../../src/controller/Component.mjs';
10
+ import ViewModel from '../../src/model/Component.mjs';
7
11
 
8
12
  /**
9
13
  * @class Neo.examples.treeSelectionModel.MainContainer
@@ -48,9 +52,9 @@ class MainContainer extends ConfigurationViewport {
48
52
  labelText: 'height',
49
53
  listeners: {change: me.onConfigChange.bind(me, 'height')},
50
54
  maxValue : 1200,
51
- minValue : 400,
52
- stepSize : 5,
53
- value : treeList.height,
55
+ minValue : 250,
56
+ stepSize : 50,
57
+ value : 650,
54
58
  style : {marginTop: '10px'}
55
59
  }, {
56
60
  module : NumberField,
@@ -59,9 +63,9 @@ class MainContainer extends ConfigurationViewport {
59
63
  listeners: {change: me.onConfigChange.bind(me, 'width')},
60
64
  maxValue : 1200,
61
65
  minValue : 200,
62
- stepSize : 5,
66
+ stepSize : 3,
63
67
  style : {marginTop: '10px'},
64
- value : treeList.width
68
+ value : 400
65
69
  }];
66
70
  }
67
71
 
@@ -69,69 +73,91 @@ class MainContainer extends ConfigurationViewport {
69
73
  * @returns {*}
70
74
  */
71
75
  createExampleComponent() {
72
- const store = Neo.create(Store, {
73
- keyProperty: 'id',
74
- model : {
75
- fields: [
76
- {name: 'collapsed', type: 'Boolean'},
77
- {name: 'content', type: 'String'},
78
- {name: 'iconCls', type: 'String'},
79
- {name: 'id', type: 'Integer'},
80
- {name: 'isLeaf', type: 'Boolean'},
81
- {name: 'name', type: 'String'},
82
- {name: 'parentId', type: 'Integer'}
83
- ]
84
- }
85
- });
76
+ const me = this,
77
+ store = Neo.create(Store, {
78
+ keyProperty: 'id',
79
+ model : {
80
+ fields: [
81
+ {name: 'collapsed', type: 'Boolean'},
82
+ {name: 'content', type: 'String'},
83
+ {name: 'iconCls', type: 'String'},
84
+ {name: 'id', type: 'Integer'},
85
+ {name: 'isLeaf', type: 'Boolean'},
86
+ {name: 'name', type: 'String'},
87
+ {name: 'parentId', type: 'Integer'}
88
+ ]
89
+ },
90
+
91
+ autoLoad: true,
92
+ url : '../../examples/treeSelectionModel/tree.json'
93
+ });
86
94
 
87
95
  return Neo.ntype({
88
- ntype : 'container',
96
+ ntype: 'container',
97
+
98
+ model: {
99
+ data: {
100
+ selection: [{name: 'Please select something'}]
101
+ }
102
+ },
103
+
89
104
  layout: {ntype: 'hbox', align: 'stretch'},
90
105
  items : [{
91
- // module: TextField,
92
- // label: 'Test',
93
- // plugin: {
94
- // Plugin: {
95
- // bold: true
96
- // }
97
- // }
98
- // },{
99
106
  module: AccordionTree,
100
- store : store,
101
- height: 800,
102
- width : 400,
103
-
104
- // ensure afterSetMounted runs only once
105
- storeLoaded: false,
106
- afterSetMounted() {
107
- if (!this.storeLoaded) {
108
- this.storeLoaded = true;
109
- } else {
110
- return;
111
- }
112
107
 
113
- let me = this;
108
+ bind: {selection: {twoWay: true, value: data => data.selection}},
114
109
 
115
- Neo.Xhr.promiseJson({
116
- url: '../../examples/treeSelectionModel/tree.json'
117
- }).then(data => {
118
- const items = data.json,
119
- colorArray = ['red', 'yellow', 'green'],
120
- iconArray = ['home', 'industry', 'user'];
110
+ store: store,
121
111
 
122
- // create random iconCls colors
123
- items.forEach((item) => {
124
- if (!item.iconCls) {
125
- const rand = Math.floor(Math.random() * 3);
126
-
127
- item.iconCls = 'fa fa-' + iconArray[rand] + ' color-' + colorArray[rand];
128
- }
129
- });
112
+ /**
113
+ * We are using data-binding.
114
+ * Here is an example for listener and controller
115
+ */
116
+ // controller: {
117
+ // module: ViewController,
118
+ //
119
+ // onAccordionItemClick(record) {
120
+ // let viewport = Neo.get('neo-configuration-viewport-1'),
121
+ // outputEl = viewport.getReference('output');
122
+ //
123
+ // outputEl.html = record.name;
124
+ // }
125
+ // },
126
+ //
127
+ // listeners: {
128
+ // leafItemClick: 'onAccordionItemClick'
129
+ // }
130
130
 
131
- me.store.data = data.json;
132
- me.createItems(null, me.getListItemsRoot(), 0);
133
- });
131
+ listeners: {
132
+ selectPreFirstItem: () => Neo.log('listener selectPreFirstItem fired'),
133
+ selectPostLastItem: () => Neo.log('listener selectPostLastItem fired')
134
134
  }
135
+ }, {
136
+ module: Panel,
137
+ height: 150,
138
+ width : '100%',
139
+
140
+ itemDefaults: {
141
+ style: {
142
+ padding: '10px'
143
+ }
144
+ },
145
+
146
+ headers: [{
147
+ dock : 'top',
148
+ style: {borderRightColor: 'transparent'},
149
+
150
+ items: [{
151
+ ntype: 'label',
152
+ text : 'Accordion Selection'
153
+ }]
154
+ }],
155
+
156
+ items: [{
157
+ ntype : 'component',
158
+ reference: 'output',
159
+ bind : {html: data => data.selection[0].name}
160
+ }]
135
161
  }]
136
162
  });
137
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -98,17 +98,39 @@
98
98
  flex : 0 0 1.2rem;
99
99
  }
100
100
 
101
- // The file input and label is only visible in the ready state
102
- &:not(.neo-file-upload-state-ready) {
101
+ // When we have a file selected, the file input and label are hidden
102
+ &:not(.neo-field-empty) {
103
103
  input[type="file"],label {
104
104
  display : none;
105
105
  }
106
106
  }
107
107
 
108
- &.neo-invalid {
108
+ &.neo-field-empty {
109
+ // The input field and its label are visible when no file is selected.
110
+ // It takes up the whole component, and is the only interactive item
111
+ :not(input[type="file"],label) {
112
+ display : none;
113
+ }
114
+ input::file-selector-button {
115
+ position : absolute;
116
+ border : 0 none;
117
+ margin : 0;
118
+ top : 0;
119
+ left : 0;
120
+ right : 0;
121
+ bottom : 0;
122
+ background-color : var(--fileuploadfield-background-color);
123
+ color : var(--fileuploadfield-color);
124
+ }
125
+ }
126
+
127
+ &.neo-invalid:not(.neo-field-empty) {
109
128
  .neo-file-upload-error-message {
110
129
  display : initial;
111
130
  }
131
+ input[type="file"],label {
132
+ display : none;
133
+ }
112
134
  }
113
135
  }
114
136
 
@@ -230,25 +252,6 @@
230
252
  }
231
253
  }
232
254
 
233
- .neo-file-upload-state-ready {
234
- // Only the input field and its label is visible when in ready state
235
- // It takes up the whole component, and is the only interactive item
236
- :not(input[type="file"],label) {
237
- display : none;
238
- }
239
- input::file-selector-button {
240
- position : absolute;
241
- border : 0 none;
242
- margin : 0;
243
- top : 0;
244
- left : 0;
245
- right : 0;
246
- bottom : 0;
247
- background-color : var(--fileuploadfield-background-color);
248
- color : var(--fileuploadfield-color);
249
- }
250
- }
251
-
252
255
  .neo-file-upload-body {
253
256
  flex : 1 1 0%;
254
257
  display : flex;
@@ -236,12 +236,12 @@ const DefaultConfig = {
236
236
  useVdomWorker: true,
237
237
  /**
238
238
  * buildScripts/injectPackageVersion.mjs will update this value
239
- * @default '6.0.2'
239
+ * @default '6.0.4'
240
240
  * @memberOf! module:Neo
241
241
  * @name config.version
242
242
  * @type String
243
243
  */
244
- version: '6.0.2'
244
+ version: '6.0.4'
245
245
  };
246
246
 
247
247
  Object.assign(DefaultConfig, {
package/src/Neo.mjs CHANGED
@@ -59,11 +59,11 @@ Neo = globalThis.Neo = Object.assign({
59
59
 
60
60
  if (Object.hasOwn(ctor, 'classConfigApplied')) {
61
61
  baseCfg = Neo.clone(ctor.config, true);
62
- break;
62
+ break
63
63
  }
64
64
 
65
65
  protos.unshift(proto);
66
- proto = proto.__proto__;
66
+ proto = proto.__proto__
67
67
  }
68
68
 
69
69
  config = baseCfg || {};
@@ -76,7 +76,7 @@ Neo = globalThis.Neo = Object.assign({
76
76
  cfg = ctor.config || {};
77
77
 
78
78
  if (Neo.overwrites) {
79
- ctor.applyOverwrites(cfg);
79
+ ctor.applyOverwrites(cfg)
80
80
  }
81
81
 
82
82
  Object.entries(cfg).forEach(([key, value]) => {
@@ -84,7 +84,7 @@ Neo = globalThis.Neo = Object.assign({
84
84
  delete cfg[key];
85
85
  key = key.slice(0, -1);
86
86
  cfg[key] = value;
87
- autoGenerateGetSet(element, key);
87
+ autoGenerateGetSet(element, key)
88
88
  }
89
89
 
90
90
  // only apply properties which have no setters inside the prototype chain
@@ -94,7 +94,7 @@ Neo = globalThis.Neo = Object.assign({
94
94
  enumerable: true,
95
95
  value,
96
96
  writable : true
97
- });
97
+ })
98
98
  }
99
99
  });
100
100
 
@@ -104,7 +104,7 @@ Neo = globalThis.Neo = Object.assign({
104
104
  // Running the docs app inside a workspace can pull in the same classes from different roots,
105
105
  // so we want to check for different class names as well
106
106
  if (Object.hasOwn(ntypeMap, ntype) && cfg.className !== ntypeMap[ntype]) {
107
- throw new Error(`ntype conflict for '${ntype}' inside the classes:\n${ntypeMap[ntype]}\n${cfg.className}`);
107
+ throw new Error(`ntype conflict for '${ntype}' inside the classes:\n${ntypeMap[ntype]}\n${cfg.className}`)
108
108
  }
109
109
 
110
110
  ntypeMap[ntype] = cfg.className;
@@ -113,11 +113,11 @@ Neo = globalThis.Neo = Object.assign({
113
113
  mixins = Object.hasOwn(config, 'mixins') && config.mixins || [];
114
114
 
115
115
  if (ctor.observable) {
116
- mixins.push('Neo.core.Observable');
116
+ mixins.push('Neo.core.Observable')
117
117
  }
118
118
 
119
119
  if (Object.hasOwn(cfg, 'mixins') && Array.isArray(cfg.mixins) && cfg.mixins.length > 0) {
120
- mixins.push(...cfg.mixins);
120
+ mixins.push(...cfg.mixins)
121
121
  }
122
122
 
123
123
  if (mixins.length > 0) {
@@ -139,17 +139,17 @@ Neo = globalThis.Neo = Object.assign({
139
139
  isClass : true
140
140
  });
141
141
 
142
- !config.singleton && this.applyToGlobalNs(cls);
142
+ !config.singleton && this.applyToGlobalNs(cls)
143
143
  });
144
144
 
145
145
  proto = cls.prototype || cls;
146
146
 
147
147
  if (proto.singleton) {
148
148
  cls = Neo.create(cls);
149
- Neo.applyToGlobalNs(cls);
149
+ Neo.applyToGlobalNs(cls)
150
150
  }
151
151
 
152
- return cls;
152
+ return cls
153
153
  },
154
154
 
155
155
  /**
@@ -177,10 +177,10 @@ Neo = globalThis.Neo = Object.assign({
177
177
  Object.entries(config).forEach(([key, value]) => {
178
178
  fnName = namespace[value];
179
179
  target[key] = bind ? fnName.bind(namespace) : fnName;
180
- });
180
+ })
181
181
  }
182
182
 
183
- return target;
183
+ return target
184
184
  },
185
185
 
186
186
  /**
@@ -199,7 +199,7 @@ Neo = globalThis.Neo = Object.assign({
199
199
  nsArray = className.split('.');
200
200
  key = nsArray.pop();
201
201
  ns = Neo.ns(nsArray, true);
202
- ns[key] = cls;
202
+ ns[key] = cls
203
203
  }
204
204
  },
205
205
 
@@ -214,12 +214,12 @@ Neo = globalThis.Neo = Object.assign({
214
214
  if (target && Neo.typeOf(defaults) === 'Object') {
215
215
  Object.entries(defaults).forEach(([key, value]) => {
216
216
  if (!Object.hasOwn(target, key)) {
217
- target[key] = value;
217
+ target[key] = value
218
218
  }
219
- });
219
+ })
220
220
  }
221
221
 
222
- return target;
222
+ return target
223
223
  },
224
224
 
225
225
  /**
@@ -243,7 +243,7 @@ Neo = globalThis.Neo = Object.assign({
243
243
  out = {};
244
244
 
245
245
  Object.entries(obj).forEach(([key, value]) => {
246
- out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances);
246
+ out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
247
247
  });
248
248
 
249
249
  return out
@@ -263,7 +263,7 @@ Neo = globalThis.Neo = Object.assign({
263
263
  delete config._id;
264
264
  delete config.id;
265
265
 
266
- return Neo.create(instance.className, config);
266
+ return Neo.create(instance.className, config)
267
267
  },
268
268
 
269
269
  /**
@@ -305,7 +305,7 @@ Neo = globalThis.Neo = Object.assign({
305
305
  cls, instance;
306
306
 
307
307
  if (type === 'NeoClass') {
308
- cls = className;
308
+ cls = className
309
309
  } else {
310
310
  if (type === 'Object') {
311
311
  config = className;
@@ -313,17 +313,17 @@ Neo = globalThis.Neo = Object.assign({
313
313
  if (!config.className && !config.module) {
314
314
  // using console.error instead of throw to show the config object
315
315
  console.error('Class created with object configuration missing className or module property', config);
316
- return null;
316
+ return null
317
317
  }
318
318
 
319
319
  className = config.className || config.module.prototype.className;
320
320
  }
321
321
 
322
322
  if (!exists(className)) {
323
- throw new Error('Class ' + className + ' does not exist');
323
+ throw new Error('Class ' + className + ' does not exist')
324
324
  }
325
325
 
326
- cls = Neo.ns(className);
326
+ cls = Neo.ns(className)
327
327
  }
328
328
 
329
329
  instance = new cls();
@@ -333,7 +333,7 @@ Neo = globalThis.Neo = Object.assign({
333
333
  instance.onAfterConstructed();
334
334
  instance.init();
335
335
 
336
- return instance;
336
+ return instance
337
337
  },
338
338
 
339
339
  /**
@@ -435,7 +435,7 @@ Neo = globalThis.Neo = Object.assign({
435
435
  return createArrayNs(true, current, prev);
436
436
  }
437
437
 
438
- prev[current] = {};
438
+ prev[current] = {}
439
439
  }
440
440
 
441
441
  if (prev) {
@@ -443,9 +443,9 @@ Neo = globalThis.Neo = Object.assign({
443
443
  return createArrayNs(false, current, prev);
444
444
  }
445
445
 
446
- return prev[current];
446
+ return prev[current]
447
447
  }
448
- }, scope || globalThis);
448
+ }, scope || globalThis)
449
449
  },
450
450
 
451
451
  /**
@@ -474,16 +474,17 @@ Neo = globalThis.Neo = Object.assign({
474
474
  if (!config.ntype) {
475
475
  throw new Error('Class defined with object configuration missing ntype property. ' + config.ntype);
476
476
  }
477
- ntype = config.ntype;
477
+
478
+ ntype = config.ntype
478
479
  }
479
480
 
480
481
  let className = Neo.ntypeMap[ntype];
481
482
 
482
483
  if (!className) {
483
- throw new Error('ntype ' + ntype + ' does not exist');
484
+ throw new Error('ntype ' + ntype + ' does not exist')
484
485
  }
485
486
 
486
- return Neo.create(className, config);
487
+ return Neo.create(className, config)
487
488
  },
488
489
 
489
490
  /**
@@ -492,7 +493,7 @@ Neo = globalThis.Neo = Object.assign({
492
493
  */
493
494
  typeOf(item) {
494
495
  if (item === null || item === undefined) {
495
- return null;
496
+ return null
496
497
  }
497
498
 
498
499
  return {
@@ -626,7 +627,7 @@ function autoGenerateGetSet(proto, key) {
626
627
  delete me[configSymbol][key];
627
628
 
628
629
  if (key !== 'items') {
629
- value = Neo.clone(value, true, true);
630
+ value = Neo.clone(value, true, true)
630
631
  }
631
632
 
632
633
  // we do want to store the value before the beforeSet modification as well,
@@ -639,7 +640,7 @@ function autoGenerateGetSet(proto, key) {
639
640
  // If they don't return a value, that means no change
640
641
  if (value === undefined) {
641
642
  me[_key] = oldValue;
642
- return;
643
+ return
643
644
  }
644
645
 
645
646
  me[_key] = value;
@@ -650,7 +651,7 @@ function autoGenerateGetSet(proto, key) {
650
651
  !Neo.isEqual(value, oldValue)
651
652
  ) {
652
653
  me[afterSet]?.(value, oldValue);
653
- me.afterSetConfig?.(key, value, oldValue);
654
+ me.afterSetConfig?.(key, value, oldValue)
654
655
  }
655
656
  }
656
657
  };
@@ -672,9 +673,9 @@ function createArrayNs(create, current, prev) {
672
673
  arrItem, arrRoot;
673
674
 
674
675
  if (create) {
675
- prev[arrDetails[0]] = arrRoot = prev[arrDetails[0]] || [];
676
+ prev[arrDetails[0]] = arrRoot = prev[arrDetails[0]] || []
676
677
  } else {
677
- arrRoot = prev[arrDetails[0]];
678
+ arrRoot = prev[arrDetails[0]]
678
679
  }
679
680
 
680
681
  if (!arrRoot) {
@@ -688,10 +689,10 @@ function createArrayNs(create, current, prev) {
688
689
  arrRoot[arrItem] = arrRoot[arrItem] || {};
689
690
  }
690
691
 
691
- arrRoot = arrRoot[arrItem];
692
+ arrRoot = arrRoot[arrItem]
692
693
  }
693
694
 
694
- return arrRoot;
695
+ return arrRoot
695
696
  }
696
697
 
697
698
  /**
@@ -704,9 +705,9 @@ function exists(className) {
704
705
  try {
705
706
  return !!className.split('.').reduce((prev, current) => {
706
707
  return prev[current];
707
- }, globalThis);
708
+ }, globalThis)
708
709
  } catch(e) {
709
- return false;
710
+ return false
710
711
  }
711
712
  }
712
713
 
@@ -725,8 +726,9 @@ function mixinProperty(proto, mixinProto) {
725
726
  if (proto[key]?._from) {
726
727
  if (mixinProto.className === proto[key]._from) {
727
728
  console.warn('Mixin set multiple times or already defined on a Base Class', proto.className, mixinProto.className, key);
728
- return;
729
+ return
729
730
  }
731
+
730
732
  throw new Error(
731
733
  `${proto.className}: Multiple mixins defining same property (${mixinProto.className}, ${proto[key]._from}) => ${key}`
732
734
  );
@@ -737,7 +739,7 @@ function mixinProperty(proto, mixinProto) {
737
739
  Object.getOwnPropertyDescriptor(proto, key)._from = mixinProto.className;
738
740
 
739
741
  if (typeof proto[key] === 'function') {
740
- proto[key]._name = key;
742
+ proto[key]._name = key
741
743
  }
742
744
  };
743
745
  }
@@ -749,8 +751,8 @@ function mixinProperty(proto, mixinProto) {
749
751
  */
750
752
  function mixReduce(mixinCls) {
751
753
  return (prev, current, idx, arr) => {
752
- return prev[current] = idx !== arr.length -1 ? prev[current] || {} : mixinCls;
753
- };
754
+ return prev[current] = idx !== arr.length -1 ? prev[current] || {} : mixinCls
755
+ }
754
756
  }
755
757
 
756
758
  /**
@@ -761,7 +763,7 @@ function mixReduce(mixinCls) {
761
763
  function parseArrayFromString(str) {
762
764
  return (extractArraysRegex.exec(str) || [null]).slice(1).reduce(
763
765
  (fun, args) => [fun].concat(args.match(charsRegex))
764
- );
766
+ )
765
767
  }
766
768
 
767
769
  Neo.config = Neo.config || {};
package/src/core/Util.mjs CHANGED
@@ -42,7 +42,7 @@ class Util extends Base {
42
42
  * @returns {Boolean|String} Returns false for non string inputs
43
43
  */
44
44
  static capitalize(value) {
45
- return Util.isString(value) && value[0].toUpperCase() + value.slice(1);
45
+ return value[0].toUpperCase() + value.slice(1)
46
46
  }
47
47
 
48
48
  /**
@@ -51,10 +51,6 @@ class Util extends Base {
51
51
  * @returns {Object} The camelcase styles object
52
52
  */
53
53
  static createStyleObject(string) {
54
- if (!string) {
55
- return null;
56
- }
57
-
58
54
  let parts;
59
55
 
60
56
  // split(';') does fetch semicolons inside brackets
@@ -74,10 +70,11 @@ class Util extends Base {
74
70
  parts[0] = parts[0].replace(/-([a-z])/g, (str, letter) => {
75
71
  return letter.toUpperCase();
76
72
  });
77
- obj[parts[0]] = parts[1];
73
+
74
+ obj[parts[0]] = parts[1]
78
75
  }
79
76
  return obj;
80
- }, {});
77
+ }, {})
81
78
  }
82
79
 
83
80
  /**
@@ -90,11 +87,11 @@ class Util extends Base {
90
87
 
91
88
  Object.entries(styles).forEach(([key, value]) => {
92
89
  if (value !== undefined && value !== null) {
93
- style += Util.decamel(key) + ':' + value + ';';
90
+ style += Util.decamel(key) + ':' + value + ';'
94
91
  }
95
92
  });
96
93
 
97
- return style;
94
+ return style
98
95
  }
99
96
 
100
97
  /**
@@ -104,7 +101,7 @@ class Util extends Base {
104
101
  * @returns {String} The lowercase output
105
102
  */
106
103
  static decamel(value) {
107
- return value.replace(Util.decamelRegEx, '$1-$2').toLowerCase();
104
+ return value.replace(Util.decamelRegEx, '$1-$2').toLowerCase()
108
105
  }
109
106
 
110
107
  /**
@@ -122,7 +119,7 @@ class Util extends Base {
122
119
  * @returns {Boolean}
123
120
  */
124
121
  static isBoolean(value) {
125
- return typeof value === 'boolean';
122
+ return typeof value === 'boolean'
126
123
  }
127
124
 
128
125
  /**
@@ -131,7 +128,7 @@ class Util extends Base {
131
128
  * @returns {Boolean}
132
129
  */
133
130
  static isDefined(value) {
134
- return typeof value !== 'undefined';
131
+ return typeof value !== 'undefined'
135
132
  }
136
133
 
137
134
  /**
@@ -141,26 +138,26 @@ class Util extends Base {
141
138
  */
142
139
  static isEmpty(value) {
143
140
  if (value === null || value === undefined) {
144
- return true;
141
+ return true
145
142
  }
146
143
 
147
144
  if (Array.isArray(value)) {
148
- return value.length === 0;
145
+ return value.length === 0
149
146
  }
150
147
 
151
148
  if (value instanceof Date) {
152
- return false;
149
+ return false
153
150
  }
154
151
 
155
152
  if (Util.isObject(value)) {
156
- return Object.keys(value).length === 0;
153
+ return Object.keys(value).length === 0
157
154
  }
158
155
 
159
156
  if (Util.isString(value)) {
160
- return value === '';
157
+ return value === ''
161
158
  }
162
159
 
163
- return false;
160
+ return false
164
161
  }
165
162
 
166
163
  /**
@@ -169,7 +166,7 @@ class Util extends Base {
169
166
  * @returns {Boolean}
170
167
  */
171
168
  static isFunction(value) {
172
- return typeof value === 'function';
169
+ return typeof value === 'function'
173
170
  }
174
171
 
175
172
  /**
@@ -178,7 +175,7 @@ class Util extends Base {
178
175
  * @returns {Boolean}
179
176
  */
180
177
  static isNumber(value){
181
- return typeof value === 'number' && isFinite(value);
178
+ return typeof value === 'number' && isFinite(value)
182
179
  }
183
180
 
184
181
  /**
@@ -187,7 +184,7 @@ class Util extends Base {
187
184
  * @returns {Boolean}
188
185
  */
189
186
  static isObject(value) {
190
- return value !== null && typeof value === 'object' && !Array.isArray(value);
187
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
191
188
  }
192
189
 
193
190
  /**
@@ -196,7 +193,7 @@ class Util extends Base {
196
193
  * @returns {Boolean}
197
194
  */
198
195
  static isString(value) {
199
- return typeof value === 'string';
196
+ return typeof value === 'string'
200
197
  }
201
198
 
202
199
  /**
@@ -210,14 +207,14 @@ class Util extends Base {
210
207
  let len;
211
208
 
212
209
  if (!iterable || !(len = iterable.length)) {
213
- return [];
210
+ return []
214
211
  }
215
212
 
216
213
  if (typeof iterable === 'string') {
217
- return iterable.split('');
214
+ return iterable.split('')
218
215
  }
219
216
 
220
- return Array.prototype.slice.call(iterable, start || 0, end || len);
217
+ return Array.prototype.slice.call(iterable, start || 0, end || len)
221
218
  }
222
219
  }
223
220
 
@@ -481,6 +481,8 @@ class CheckBox extends Base {
481
481
  * @returns {Boolean}
482
482
  */
483
483
  isValid() {
484
+ this.validate(true); // silent
485
+
484
486
  return this.error ? false : super.isValid()
485
487
  }
486
488
 
@@ -132,7 +132,7 @@ class FileUpload extends Base {
132
132
  ]
133
133
  },
134
134
 
135
- cls : [],
135
+ cls : ['neo-field-empty'],
136
136
 
137
137
  /**
138
138
  * An Object containing a default set of headers to be passed to the server on every HTTP request.
@@ -153,6 +153,7 @@ class FileUpload extends Base {
153
153
  UPLOADING : 'scanning',
154
154
 
155
155
  MALWARE_DETECTED : 'scan-failed',
156
+ UN_DOWNLOADABLE : 'not-downloadable',
156
157
  AVAILABLE : 'not-downloadable',
157
158
  DOWNLOADABLE : 'downloadable',
158
159
  DELETED : 'deleted'
@@ -317,6 +318,7 @@ class FileUpload extends Base {
317
318
  // UI strings which can be overridden for other languages
318
319
  chooseFile : 'Choose file',
319
320
  documentText : 'Document',
321
+ invalidFileFormat : 'invalid file format',
320
322
  pleaseUseTheseTypes : 'Please use these file types',
321
323
  fileSizeMoreThan : 'File size exceeds',
322
324
  documentDeleteError : 'Document delete service error',
@@ -346,16 +348,19 @@ class FileUpload extends Base {
346
348
  }
347
349
 
348
350
  afterSetId(value, oldValue) {
349
- const
350
- labelEl = this.vdom.cn[4],
351
- inputElId = `${this.id}-input`;
351
+ const inputElId = `${this.id}-input`;
352
352
 
353
- this.getInputEl().id = labelEl.for = inputElId;
354
- labelEl.html = this.chooseFile;
353
+ this.getInputEl().id = this.vdom.cn[4].for = inputElId;
355
354
 
356
355
  // silent vdom update, the super call will trigger the engine
357
356
  super.afterSetId?.(value, oldValue);
358
357
  }
358
+
359
+ onConstructed() {
360
+ super.onConstructed(...arguments);
361
+
362
+ this.vdom.cn[4].html = this.chooseFile;
363
+ }
359
364
 
360
365
  /**
361
366
  * @returns {Object}
@@ -365,9 +370,15 @@ class FileUpload extends Base {
365
370
  }
366
371
 
367
372
  async clear() {
368
- const me = this;
373
+ const
374
+ me = this,
375
+ { cls } = me;
376
+
377
+ NeoArray.add(cls, 'neo-field-empty');
378
+ me.cls = cls;
369
379
 
370
380
  me.vdom.cn[3] = {
381
+ id : `${me.id}-input`,
371
382
  cls : 'neo-file-upload-input',
372
383
  tag : 'input',
373
384
  type : 'file',
@@ -375,6 +386,7 @@ class FileUpload extends Base {
375
386
  };
376
387
  me.state = 'ready';
377
388
  me.error = '';
389
+ me.file = me.document = null;
378
390
 
379
391
  // We have to wait for the DOM to have changed, and the input field to be visible
380
392
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -388,18 +400,29 @@ class FileUpload extends Base {
388
400
  onInputValueChange({ files }) {
389
401
  const
390
402
  me = this,
391
- { types } = me;
403
+ {
404
+ types,
405
+ cls
406
+ } = me,
407
+ body = me.vdom.cn[1];
392
408
 
393
409
  if (files.length) {
410
+ NeoArray.remove(cls, 'neo-field-empty');
411
+ me.cls = cls;
412
+
394
413
  const
395
414
  file = files.item(0),
396
415
  pointPos = file.name.lastIndexOf('.'),
397
416
  type = pointPos > -1 ? file.name.slice(pointPos + 1) : '';
398
417
 
399
418
  if (me.types && !types[type]) {
419
+ body.cn[0].innerHTML = file.name;
420
+ body.cn[1].innerHTML = `${me.invalidFileFormat} (.${type}) ${me.formatSize(file.size)}`;
400
421
  me.error = `${me.pleaseUseTheseTypes}: .${Object.keys(types).join(' .')}`;
401
422
  }
402
423
  else if (file.size > me.maxSize) {
424
+ body.cn[0].innerHTML = file.name;
425
+ body.cn[1].innerHTML = me.formatSize(file.size);
403
426
  me.error = `${me.fileSizeMoreThan} ${String(me._maxSize).toUpperCase()}`;
404
427
  }
405
428
  // If it passes the type and maxSize check, upload it
@@ -424,6 +447,7 @@ class FileUpload extends Base {
424
447
  headers = { ...me.headers };
425
448
 
426
449
  // Show the action button
450
+ me.file = file;
427
451
  me.state = 'starting';
428
452
 
429
453
  // We have to wait for the DOM to have changed, and the action button to be visible
@@ -553,6 +577,9 @@ class FileUpload extends Base {
553
577
  me.clear();
554
578
  me.state = 'ready';
555
579
  break;
580
+ case 'ready':
581
+ me.clear();
582
+ break;
556
583
  }
557
584
  }
558
585
 
@@ -629,9 +656,12 @@ class FileUpload extends Base {
629
656
  afterSetDocument(document) {
630
657
  if (document) {
631
658
  const
632
- me = this;
659
+ me = this,
660
+ { cls } = me;
661
+
662
+ NeoArray.remove(cls, 'neo-field-empty');
663
+ me.cls = cls;
633
664
 
634
- me.preExistingDocument = true;
635
665
  me.documentId = document.id;
636
666
  me.fileSize = me.formatSize(document.size);
637
667
  me.vdom.cn[1].cn[0].innerHTML = document.fileName;
@@ -680,8 +710,7 @@ class FileUpload extends Base {
680
710
  status.innerHTML = me.fileSize;
681
711
  break;
682
712
  case 'not-downloadable':
683
- status.innerHTML = me.preExistingDocument ?
684
- me.fileSize : `${me.successfullyUploaded} \u2022 ${me.fileSize}`;
713
+ status.innerHTML = me.document ? me.fileSize : `${me.successfullyUploaded} \u2022 ${me.fileSize}`;
685
714
  break;
686
715
  case 'deleted':
687
716
  status.innerHTML = me.fileWasDeleted;
@@ -695,6 +724,7 @@ class FileUpload extends Base {
695
724
 
696
725
  NeoArray.remove(cls, 'neo-file-upload-state-' + oldValue);
697
726
  NeoArray.add(cls, 'neo-file-upload-state-' + value);
727
+ NeoArray[me.file || me.document ? 'remove' : 'add', 'neo-field-empty'];
698
728
  me.cls = cls;
699
729
  }
700
730
 
@@ -23,7 +23,7 @@ class Text extends Base {
23
23
  * @protected
24
24
  * @static
25
25
  */
26
- static labelPositions = ['bottom', 'inline', 'left', 'right', 'top']
26
+ static labelPositions = ['bottom', 'inline', 'left', 'right', 'top']
27
27
 
28
28
  static config = {
29
29
  /**
@@ -220,18 +220,14 @@ class Text extends Base {
220
220
  * @member {Object} _vdom
221
221
  */
222
222
  _vdom:
223
- {
224
- cn: [
225
- {tag: 'label', cls: [], style: {}},
226
- {tag: 'label', cls: []},
227
- {tag: 'input', cls: ['neo-textfield-input'], flag: 'neo-real-input', style: {}},
228
- {
229
- cls: ['neo-textfield-error-wrapper'], removeDom: true, cn: [
230
- {cls: ['neo-textfield-error']}
231
- ]
232
- }
233
- ]
234
- }
223
+ {cn: [
224
+ {tag: 'label', cls: [], style: {}},
225
+ {tag: 'label', cls: []},
226
+ {tag: 'input', cls: ['neo-textfield-input'], flag: 'neo-real-input', style: {}},
227
+ {cls: ['neo-textfield-error-wrapper'], removeDom: true, cn: [
228
+ {cls: ['neo-textfield-error']}
229
+ ]}
230
+ ]}
235
231
  }
236
232
 
237
233
  /**
@@ -250,10 +246,10 @@ class Text extends Base {
250
246
  let me = this;
251
247
 
252
248
  me.addDomListeners([
253
- {input: me.onInputValueChange, scope: me},
249
+ {input : me.onInputValueChange, scope: me},
254
250
  {mouseenter: me.onMouseEnter, scope: me},
255
251
  {mouseleave: me.onMouseLeave, scope: me}
256
- ]);
252
+ ])
257
253
  }
258
254
 
259
255
  /**
@@ -263,13 +259,11 @@ class Text extends Base {
263
259
  * @protected
264
260
  */
265
261
  afterSetAppName(value, oldValue) {
266
- let me = this;
267
-
268
262
  super.afterSetAppName(value, oldValue);
269
263
 
270
- value && me.triggers?.forEach(item => {
264
+ value && this.triggers?.forEach(item => {
271
265
  item.appName = value;
272
- });
266
+ })
273
267
  }
274
268
 
275
269
  /**
@@ -279,7 +273,7 @@ class Text extends Base {
279
273
  * @protected
280
274
  */
281
275
  afterSetAutoCapitalize(value, oldValue) {
282
- this.changeInputElKey('autocapitalize', value === 'off' || value === 'none' ? null : value);
276
+ this.changeInputElKey('autocapitalize', value === 'off' || value === 'none' ? null : value)
283
277
  }
284
278
 
285
279
  /**
@@ -290,7 +284,7 @@ class Text extends Base {
290
284
  * @protected
291
285
  */
292
286
  afterSetAutoComplete(value, oldValue) {
293
- this.changeInputElKey('autocomplete', value ? null : 'no');
287
+ this.changeInputElKey('autocomplete', value ? null : 'no')
294
288
  }
295
289
 
296
290
  /**
@@ -306,9 +300,9 @@ class Text extends Base {
306
300
  if (value) {
307
301
  triggers = me.triggers || [];
308
302
  triggers.unshift(ClearTrigger);
309
- me.triggers = triggers;
303
+ me.triggers = triggers
310
304
  } else {
311
- me.removeTrigger('clear');
305
+ me.removeTrigger('clear')
312
306
  }
313
307
  }
314
308
 
@@ -505,7 +499,7 @@ class Text extends Base {
505
499
 
506
500
  !isEmpty && setTimeout(() => {
507
501
  me.updateCenterBorderElWidth(false)
508
- }, 20);
502
+ }, 20)
509
503
  } else {
510
504
  // changes from e.g. left to top
511
505
  me.updateInputWidth()
@@ -980,7 +974,7 @@ class Text extends Base {
980
974
  })
981
975
  }
982
976
 
983
- super.destroy(...args);
977
+ super.destroy(...args)
984
978
  }
985
979
 
986
980
  /**
@@ -1419,7 +1413,7 @@ class Text extends Base {
1419
1413
  * @param {Boolean} silent=true
1420
1414
  * @returns {Boolean} Returns true in case there are no client-side errors
1421
1415
  */
1422
- validate(silent = true) {
1416
+ validate(silent=true) {
1423
1417
  let me = this,
1424
1418
  maxLength = me.maxLength,
1425
1419
  minLength = me.minLength,
@@ -37,6 +37,18 @@ class AccordionTree extends TreeList {
37
37
  * @member {Boolean} firstParentIsVisible=true
38
38
  */
39
39
  firstParentIsVisible_: true,
40
+ /**
41
+ * Currently selected item, which is bindable
42
+ * @member {Record[|null} selection=null
43
+ *
44
+ * @example
45
+ * module: AccordionTree,
46
+ * bind : {selection: {twoWay: true, value: data => data.selection}}
47
+ *
48
+ * ntype: 'component',
49
+ * bind : {html: data => data.selection[0].name}
50
+ */
51
+ selection_: null,
40
52
  /**
41
53
  * @member {Object} _vdom
42
54
  */
@@ -112,6 +124,25 @@ class AccordionTree extends TreeList {
112
124
  return ClassSystemUtil.beforeSetInstance(value, TreeAccordionModel);
113
125
  }
114
126
 
127
+ /**
128
+ * Remove all items from the accordion
129
+ * If you do not need to update the view after clearing, set `withUpdate = false`
130
+ *
131
+ * @param {Boolean} [withUpdate=true]
132
+ */
133
+ clear(withUpdate = true) {
134
+ delete this.getVdomRoot().cn[0].cn
135
+
136
+ if (withUpdate) this.update();
137
+ }
138
+
139
+ /**
140
+ * Remove all items from the selection
141
+ */
142
+ clearSelection() {
143
+ this.selectionModel.deselectAll();
144
+ }
145
+
115
146
  /**
116
147
  * @param {String} [parentId] The parent node
117
148
  * @param {Object} [vdomRoot] The vdom template root for the current sub tree
@@ -133,12 +164,9 @@ class AccordionTree extends TreeList {
133
164
 
134
165
  if (parentId !== null) {
135
166
  vdomRoot.cn.push({
136
- tag : 'ul',
137
- cls : ['neo-list'],
138
- cn : [],
139
- style: {
140
- paddingLeft: '15px'
141
- }
167
+ tag: 'ul',
168
+ cls: ['neo-list'],
169
+ cn : []
142
170
  });
143
171
 
144
172
  tmpRoot = vdomRoot.cn[vdomRoot.cn.length - 1];
@@ -188,7 +216,6 @@ class AccordionTree extends TreeList {
188
216
  }]
189
217
  }],
190
218
  style: {
191
- padding : '10px',
192
219
  position: item.isLeaf ? null : 'sticky',
193
220
  top : item.isLeaf ? null : (level * 38) + 'px',
194
221
  zIndex : item.isLeaf ? null : (20 / (level + 1)),
@@ -257,6 +284,7 @@ class AccordionTree extends TreeList {
257
284
 
258
285
  /**
259
286
  * Accordion gaining focus without selection => setSelection
287
+ *
260
288
  * @param {Object} data
261
289
  */
262
290
  onFocus(data) {
@@ -267,12 +295,85 @@ class AccordionTree extends TreeList {
267
295
  if (!selection) selModel.selectRoot();
268
296
  }
269
297
 
270
- // Todo Might be needed
271
- onStoreLoad() {
298
+ /**
299
+ * Called from SelectionModel select()
300
+ *
301
+ * @param {String[]} value
302
+ */
303
+ onSelect(value) {
304
+ const me = this;
305
+ let records = [];
306
+
307
+ value.forEach((selectItemId) => {
308
+ let id = me.getItemRecordId(selectItemId),
309
+ record = me.store.get(id);
310
+
311
+ records.push(record);
312
+ });
313
+
314
+ me.selection = records;
315
+ }
316
+
317
+ /**
318
+ * After the store loaded, create the items for the list
319
+ *
320
+ * @param {Record[]} records
321
+ */
322
+ onStoreLoad(records) {
323
+ let me = this,
324
+ listenerId;
325
+
326
+ me.clear(false);
327
+
328
+ if (!me.mounted && me.rendering) {
329
+ listenerId = me.on('mounted', () => {
330
+ me.un('mounted', listenerId);
331
+ me.createItems(null, me.getListItemsRoot(), 0);
332
+ me.timeout(0).then(() => {
333
+ me.update();
334
+ });
335
+ });
336
+ } else {
337
+ me.createItems(null, me.getListItemsRoot(), 0);
338
+ me.timeout(0).then(() => {
339
+ me.update();
340
+ });
341
+ }
272
342
  }
273
343
 
274
344
  onStoreRecordChange() {
275
345
  }
346
+
347
+ /**
348
+ * Set the selection either bei record id or record.
349
+ * You can pass a record or a recordId as value
350
+ *
351
+ * @param {Record|Record[]|Number|Number[]|String|String[]} value
352
+ */
353
+ setSelection(value) {
354
+ if (value === null) {
355
+ this.clearSelection();
356
+ return;
357
+ }
358
+
359
+ // In case you pass in an array use only the first item
360
+ if (Neo.isArray(value)) value = value[0];
361
+
362
+ const me = this;
363
+ let recordKeyProperty, elId;
364
+
365
+ if (Neo.isObject(value)) {
366
+ // Record
367
+ recordKeyProperty = value[me.getKeyProperty()];
368
+ } else {
369
+ // RecordId
370
+ recordKeyProperty = value;
371
+ }
372
+
373
+ elId = me.getItemId(recordKeyProperty);
374
+
375
+ me.selectionModel.selectAndScrollIntoView(elId);
376
+ }
276
377
  }
277
378
 
278
379
  Neo.applyClassConfig(AccordionTree);