neo.mjs 6.10.10 → 6.10.11

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.
Files changed (79) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/apps/portal/view/learn/ContentTreeList.mjs +24 -12
  3. package/apps/portal/view/learn/LivePreview.mjs +28 -11
  4. package/buildScripts/createAppMinimal.mjs +391 -0
  5. package/examples/ServiceWorker.mjs +2 -2
  6. package/examples/button/base/neo-config.json +2 -1
  7. package/examples/list/chip/neo-config.json +1 -2
  8. package/package.json +72 -70
  9. package/resources/data/deck/learnneo/data/theBeatles.json +22 -0
  10. package/resources/data/deck/learnneo/p/2023-10-14T19-25-08-153Z.md +29 -20
  11. package/resources/data/deck/learnneo/p/ComponentModels.md +116 -1
  12. package/resources/data/deck/learnneo/p/Config.md +157 -0
  13. package/resources/data/deck/learnneo/p/DescribingTheUI.md +67 -1
  14. package/resources/data/deck/learnneo/p/Earthquakes.md +214 -0
  15. package/resources/data/deck/learnneo/p/Events.md +142 -1
  16. package/resources/data/deck/learnneo/p/Extending.md +116 -1
  17. package/resources/data/deck/learnneo/p/References.md +126 -0
  18. package/resources/data/deck/learnneo/p/TestLivePreview.md +28 -6
  19. package/resources/data/deck/learnneo/t.json +5 -6
  20. package/resources/data/deck/training/p/2022-12-27T21-55-30-948Z.md +1 -1
  21. package/resources/data/deck/training/p/2022-12-27T22-23-55-083Z.md +1 -1
  22. package/resources/data/deck/training/p/2022-12-29T16-00-13-223Z.md +1 -1
  23. package/resources/data/deck/training/p/2022-12-29T18-34-25-826Z.md +1 -1
  24. package/resources/data/deck/training/p/2022-12-29T18-36-56-893Z.md +1 -1
  25. package/resources/data/deck/training/p/2022-12-31T18-43-56-338Z.md +1 -1
  26. package/resources/data/deck/training/p/2022-12-31T18-51-50-682Z.md +1 -1
  27. package/resources/data/deck/training/p/2022-12-31T18-54-04-176Z.md +1 -1
  28. package/resources/data/deck/training/p/2023-01-01T17-49-18-429Z.md +1 -1
  29. package/resources/data/deck/training/p/2023-01-01T21-23-17-716Z.md +1 -1
  30. package/resources/data/deck/training/p/2023-01-06T23-21-31-685Z.md +1 -1
  31. package/resources/data/deck/training/p/2023-01-06T23-34-13-897Z.md +2 -2
  32. package/resources/data/deck/training/p/2023-01-06T23-46-36-687Z.md +1 -1
  33. package/resources/data/deck/training/p/2023-01-08T01-24-21-088Z.md +1 -1
  34. package/resources/data/deck/training/p/2023-01-08T02-11-26-333Z.md +2 -2
  35. package/resources/data/deck/training/p/2023-01-14T00-40-27-784Z.md +2 -2
  36. package/resources/data/deck/training/p/2023-07-31T00-37-21-927Z.md +2 -2
  37. package/resources/data/deck/training/p/2023-10-14T19-25-08-153Z.md +3 -3
  38. package/resources/scss/src/apps/newwebsite/Viewport.scss +32 -0
  39. package/resources/scss/src/apps/portal/learn/ContentView.scss +20 -4
  40. package/resources/scss/src/apps/portal/learn/LivePreview.scss +8 -0
  41. package/resources/scss/src/component/Base.scss +13 -4
  42. package/resources/scss/src/form/field/Select.scss +2 -5
  43. package/resources/scss/src/form/field/Text.scss +0 -1
  44. package/resources/scss/src/list/Base.scss +47 -2
  45. package/resources/scss/src/list/Chip.scss +10 -4
  46. package/resources/scss/theme-dark/list/Base.scss +11 -10
  47. package/resources/scss/theme-light/list/Base.scss +11 -10
  48. package/resources/scss/theme-neo-light/design-tokens/Components.scss +3 -0
  49. package/resources/scss/theme-neo-light/list/Base.scss +1 -0
  50. package/src/DefaultConfig.mjs +3 -3
  51. package/src/component/Base.mjs +7 -0
  52. package/src/container/Base.mjs +6 -12
  53. package/src/core/Base.mjs +5 -2
  54. package/src/data/Model.mjs +7 -0
  55. package/src/data/RecordFactory.mjs +5 -4
  56. package/src/form/field/Base.mjs +11 -0
  57. package/src/form/field/Picker.mjs +0 -1
  58. package/src/form/field/Select.mjs +208 -257
  59. package/src/form/field/Text.mjs +3 -3
  60. package/src/form/field/trigger/Base.mjs +5 -6
  61. package/src/layout/Flexbox.mjs +23 -31
  62. package/src/layout/HBox.mjs +1 -1
  63. package/src/layout/VBox.mjs +1 -1
  64. package/src/list/Base.mjs +64 -31
  65. package/src/main/DomAccess.mjs +55 -28
  66. package/src/main/DomEvents.mjs +2 -1
  67. package/src/main/DomUtils.mjs +66 -0
  68. package/src/main/addon/Navigator.mjs +332 -0
  69. package/src/manager/DomEvent.mjs +2 -1
  70. package/src/selection/ListModel.mjs +46 -82
  71. package/src/selection/Model.mjs +56 -33
  72. package/src/util/Array.mjs +5 -2
  73. package/src/util/Function.mjs +31 -0
  74. package/src/util/String.mjs +9 -0
  75. package/src/vdom/Helper.mjs +1 -2
  76. package/test/components/app.mjs +4 -3
  77. package/test/components/files/component/ChipList.mjs +125 -0
  78. package/test/components/files/form/field/Select.mjs +177 -2
  79. package/test/components/siesta.js +34 -1
@@ -4,9 +4,11 @@ import List from '../../list/Base.mjs';
4
4
  import Picker from './Picker.mjs';
5
5
  import Store from '../../data/Store.mjs';
6
6
  import VDomUtil from '../../util/VDom.mjs';
7
-
7
+ import { buffer } from '../../util/Function.mjs';
8
8
  /**
9
- * Provides a dropdown list to select one or multiple items
9
+ * Provides a dropdown list to select one or multiple items.
10
+ *
11
+ * Conforms to ARIA accessiblity standards outlines in https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
10
12
  * @class Neo.form.field.Select
11
13
  * @extends Neo.form.field.Picker
12
14
  */
@@ -49,19 +51,16 @@ class Select extends Picker {
49
51
  */
50
52
  forceSelection: false,
51
53
  /**
52
- * @member {String|Number|null} hintRecordId=null
54
+ * @member {String|Number|null} activeRecordId=null
53
55
  */
54
- hintRecordId: null,
56
+ activeRecordId: null,
55
57
  /**
56
58
  * Additional used keys for the selection model
57
59
  * @member {Object} keys
58
60
  */
59
61
  keys: {
60
62
  Down : 'onKeyDownDown',
61
- Enter : 'onKeyDownEnter',
62
- Escape: 'onKeyDownEscape',
63
- Right : 'onKeyDownRight',
64
- Up : 'onKeyDownUp'
63
+ Escape: 'onKeyDownEscape'
65
64
  },
66
65
  /**
67
66
  * @member {String|null} lastManualInput=null
@@ -88,9 +87,9 @@ class Select extends Picker {
88
87
  */
89
88
  record_: null,
90
89
  /**
91
- * @member {String|null} role='listbox'
90
+ * @member {String|null} role='combobox'
92
91
  */
93
- role: 'listbox',
92
+ role: 'combobox',
94
93
  /**
95
94
  * @member {Neo.data.Store|null} store_=null
96
95
  */
@@ -117,7 +116,13 @@ class Select extends Picker {
117
116
  * which you want to submit instead
118
117
  * @member {Number|String} valueField='id'
119
118
  */
120
- valueField: 'id'
119
+ valueField: 'id',
120
+ /**
121
+ * The millisecond time to delay between input field mutation and applying the input field's
122
+ * new value to the filter
123
+ * @member {Number} filterDelay=300
124
+ */
125
+ filterDelay : 300
121
126
  }
122
127
 
123
128
  /**
@@ -126,34 +131,10 @@ class Select extends Picker {
126
131
  construct(config) {
127
132
  super.construct(config);
128
133
 
129
- let me = this;
130
-
131
- me.list = Neo.create({
132
- module : List,
133
- appName : me.appName,
134
- displayField : me.displayField,
135
- itemRole : 'option',
136
- parentId : me.id,
137
- selectionModel: {stayInList: false},
138
- store : me.store,
139
- ...me.listConfig
140
- });
141
-
142
- me.list.keys._keys.push(
143
- {fn: 'onContainerKeyDownEnter', key: 'Enter', scope: me.id},
144
- {fn: 'onContainerKeyDownEscape', key: 'Escape', scope: me.id}
145
- );
146
-
147
- me.list.on({
148
- createItems : me.onListCreateItems,
149
- itemClick : me.onListItemClick,
150
- itemNavigate : me.onListItemNavigate,
151
- selectPostLastItem: me.onSelectPostLastItem,
152
- selectPreFirstItem: me.onSelectPreFirstItem,
153
- scope : me
154
- });
134
+ // Create buffered function to respond to input field mutation
135
+ this.filterOnInput = buffer(this.filterOnInput, this, this.filterDelay);
155
136
 
156
- me.typeAhead && me.updateTypeAhead()
137
+ this.typeAhead && this.updateTypeAhead()
157
138
  }
158
139
 
159
140
  /**
@@ -162,23 +143,17 @@ class Select extends Picker {
162
143
  * @param {Object} oldValue
163
144
  * @protected
164
145
  */
165
- afterSetRecord(value, oldValue) {
166
- let me = this,
167
- list = me.list,
168
- selectionModel = list?.selectionModel,
169
- valueField = me.valueField,
170
- nodeId;
171
-
172
- if (oldValue) {
173
- nodeId = list?.getItemId(oldValue[valueField]);
146
+ afterSetRecord(value) {
147
+ if (this._picker?.isVisible) {
148
+ let me = this,
149
+ selectionModel = me.list?.selectionModel;
174
150
 
175
- selectionModel?.deselect(nodeId);
176
- }
177
-
178
- if (value) {
179
- nodeId = list?.getItemId(value[valueField]);
180
-
181
- selectionModel?.select(nodeId);
151
+ if (value) {
152
+ selectionModel?.select(value);
153
+ }
154
+ else {
155
+ selectionModel.deselectAll();
156
+ }
182
157
  }
183
158
  }
184
159
 
@@ -224,19 +199,6 @@ class Select extends Picker {
224
199
  this.rendered && this.updateTypeAhead()
225
200
  }
226
201
 
227
- /**
228
- * Triggered after the value config got changed
229
- * @param {Number|String|null} value
230
- * @param {Number|String|null} oldValue
231
- * @param {Boolean} preventFilter=false
232
- * @protected
233
- */
234
- afterSetValue(value, oldValue, preventFilter=false) {
235
- !preventFilter && this.updateValue(true);
236
-
237
- super.afterSetValue(value, oldValue)
238
- }
239
-
240
202
  /**
241
203
  * Triggered before the listConfig config gets changed.
242
204
  * @param {Object} value
@@ -317,7 +279,37 @@ class Select extends Picker {
317
279
  * @returns {Neo.list.Base}
318
280
  */
319
281
  createPickerComponent() {
320
- return this.list
282
+ const me = this;
283
+
284
+ me.list = Neo.create({
285
+ module : List,
286
+ appName : me.appName,
287
+ displayField : me.displayField,
288
+ role : 'listbox',
289
+ itemRole : 'option',
290
+ parentId : me.id,
291
+ navigator : {
292
+ eventSource : me.getInputElId()
293
+ },
294
+ selectionModel : {stayInList: false},
295
+ store : me.store,
296
+ ...me.listConfig
297
+ });
298
+ me.getInputEl()['aria-controls'] = me.list.id;
299
+
300
+ me.list.addDomListeners({
301
+ neonavigate: {
302
+ fn : me.onListItemNavigate,
303
+ scope : me
304
+ }
305
+ });
306
+ me.list.selectionModel.on({
307
+ selectionChange : me.onListItemSelectionChange,
308
+ noChange : me.onListItemSelectionNoChange,
309
+ scope : me
310
+ })
311
+
312
+ return me.list;
321
313
  }
322
314
 
323
315
  /**
@@ -355,24 +347,6 @@ class Select extends Picker {
355
347
  }
356
348
  }
357
349
 
358
- /**
359
- * @param {Function} [callback]
360
- */
361
- focusInputEl(callback) {
362
- let me = this,
363
- lastManualInput = me.lastManualInput;
364
-
365
- me.updateTypeAheadValue(lastManualInput, true);
366
- me.value = lastManualInput;
367
-
368
- Neo.main.DomAccess.focus({
369
- appName: me.appName,
370
- id : me.getInputElId()
371
- }).then(() => {
372
- callback?.apply(me)
373
- })
374
- }
375
-
376
350
  /**
377
351
  * @returns {Object}
378
352
  */
@@ -407,35 +381,13 @@ class Select extends Picker {
407
381
  return me.record?.[me.valueField] || me.value
408
382
  }
409
383
 
410
- /**
411
- * @param {Object} data
412
- * @protected
413
- */
414
- handleKeyDownEnter(data) {
415
- let me = this;
416
-
417
- if (me.pickerIsMounted) {
418
- me.selectListItem();
419
- super.onKeyDownEnter(data)
420
- } else {
421
- super.onKeyDownEnter(data, me.selectListItem)
422
- }
423
- }
424
-
425
- /**
426
- * @param {Object} data
427
- * @protected
428
- */
429
- onContainerKeyDownEnter(data) {
430
- this.hidePicker()
431
- }
384
+ onConstructed() {
385
+ const inputEl = this.getInputEl();
432
386
 
433
- /**
434
- * @param {Object} data
435
- * @protected
436
- */
437
- onContainerKeyDownEscape(data) {
438
- this.focusInputEl(this.hidePicker)
387
+ inputEl['aria-expanded'] = false
388
+ inputEl['aria-haspopup'] = 'listbox'
389
+ inputEl['aria-activedescendant'] = ''
390
+ super.onConstructed(...arguments);
439
391
  }
440
392
 
441
393
  /**
@@ -446,20 +398,45 @@ class Select extends Picker {
446
398
  onFocusLeave(data) {
447
399
  let me = this;
448
400
 
449
- if (me.forceSelection && !me.record) {
450
- me.value = me.hintRecordId;
401
+ if (!me.record) {
402
+ if (me.forceSelection) {
403
+ me.value = me.forceSelection ? me.activeRecordId : null;
404
+ }
405
+ // If we exit without selecting a record, clear the filter input value.
406
+ else {
407
+ me.getInputEl().value = '';
408
+ }
409
+ }
410
+
411
+ // Clear any typeahead hint
412
+ me.updateTypeAheadValue('');
413
+
414
+ // The VDOM must not carry the empty string permanently. Only while clearing the value.
415
+ if (!me.record && !me.forceSelection) {
416
+ delete me.getInputEl().value;
451
417
  }
452
418
 
453
419
  super.onFocusLeave(data)
454
420
  }
455
421
 
422
+ filterOnInput(data) {
423
+ if (data.value) {
424
+ this.doFilter(data.value);
425
+ }
426
+ else if (this.picker) {
427
+ this.picker?.hide();
428
+ }
429
+ }
430
+
456
431
  /**
457
432
  * @param {Object} data
458
433
  * @protected
459
434
  */
460
435
  onInputValueChange(data) {
461
- super.onInputValueChange(data);
436
+ // We do not call super here. The value of the Select is *not* connected to the value
437
+ // typed into the input area. The input area is just a filter value to filter the list.
462
438
  this.lastManualInput = data.value
439
+ this.filterOnInput(data);
463
440
  }
464
441
 
465
442
  /**
@@ -467,70 +444,43 @@ class Select extends Picker {
467
444
  * @protected
468
445
  */
469
446
  onKeyDownDown(data) {
470
- this.handleKeyDownEnter(data)
471
- }
472
-
473
- /**
474
- * @param {Object} data
475
- * @protected
476
- */
477
- onKeyDownEnter(data) {
478
- this.handleKeyDownEnter(data);
479
- }
480
-
481
- /**
482
- * @param {Object} data
483
- * @protected
484
- */
485
- onKeyDownRight(data) {
486
- let me = this,
487
- oldValue, record;
488
-
489
- if (me.hintRecordId) {
490
- oldValue = me.value;
491
- record = me.store.get(me.hintRecordId);
492
-
493
- me.record = record;
494
- me._value = record[me.displayField];
495
-
496
- me.afterSetValue(me._value, oldValue)
447
+ if (!this.picker || this.picker?.hidden) {
448
+ this.onPickerTriggerClick();
497
449
  }
498
450
  }
499
451
 
500
- /**
501
- * @param {Object} data
502
- * @protected
503
- */
504
- onKeyDownUp(data) {
505
- let me = this;
452
+ onPickerHiddenChange({ value }) {
453
+ const inputEl = this.getInputEl();
506
454
 
507
- if (me.pickerIsMounted) {
508
- me.selectLastListItem();
509
- super.onKeyDownEnter(data)
510
- } else {
511
- super.onKeyDownEnter(data, me.selectLastListItem)
455
+ super.onPickerHiddenChange(...arguments);
456
+ if (value) {
457
+ inputEl['aria-activedescendant'] = '';
512
458
  }
459
+ inputEl['aria-expanded'] = !value;
460
+ this.update();
513
461
  }
514
462
 
463
+ // TODO:
464
+ // When we are using a `Collection` as our `valueCollection`, and that `Collection` is the
465
+ // `items` of the List's `selectionModel`, then this will be `onValueCollectionChange`,
466
+ // a `mutate` listener on our own `valueCollection` which backs our `value` field which
467
+ // will be implemented by a getter which accesses `valueCollection`.
468
+ // This will become important for implementing multiSelect
515
469
  /**
470
+ * @param {Object} selectionChangeEvent
471
+ * @param {Object[]} selectionChangeEvent.selection
516
472
  * @protected
517
473
  */
518
- onListCreateItems() {
519
- let me = this;
520
- me.typeAhead && me.picker?.mounted && me.updateTypeAheadValue()
521
- }
522
-
523
- /**
524
- * @param {Object} record
525
- * @protected
526
- */
527
- onListItemChange(record) {
528
- let me = this,
529
- displayField = me.displayField,
530
- oldValue = me.value,
531
- value = record[displayField];
474
+ onListItemSelectionChange({ selection }) {
475
+ if (selection?.length) {
476
+ const
477
+ me = this,
478
+ oldValue = me.value,
479
+ selected = selection[0],
480
+ record = typeof selected === 'string' ? me.store.get(me.list.getItemRecordId(selected)) : selected,
481
+ value = record[me.displayField];
532
482
 
533
- if (me.value !== value) {
483
+ me.hidePicker();
534
484
  me.hintRecordId = null;
535
485
  me.record = record;
536
486
  me._value = value;
@@ -540,42 +490,36 @@ class Select extends Picker {
540
490
 
541
491
  me.fire('select', {
542
492
  record,
543
- value: record[displayField]
493
+ value
544
494
  })
545
495
  }
546
496
  }
547
497
 
548
498
  /**
549
- * @param {Object} record
550
- * @protected
499
+ * Selection was attempted to be changed but resulted in no action.
500
+ * For example clicking on already selected list item.
551
501
  */
552
- onListItemClick(record) {
553
- this.onListItemChange(record);
554
- this.hidePicker()
502
+ onListItemSelectionNoChange() {
503
+ this.hidePicker();
555
504
  }
556
505
 
557
506
  /**
558
507
  * @param {Object} record
559
508
  * @protected
560
509
  */
561
- onListItemNavigate(record) {
562
- this.onListItemChange(record)
563
- }
510
+ onListItemNavigate({ activeItem, activeIndex }) {
511
+ if (activeIndex >= 0) {
512
+ const
513
+ me = this,
514
+ { store } = me;
564
515
 
565
- /**
566
- * @protected
567
- */
568
- onSelectPostLastItem() {
569
- this.record = null;
570
- this.focusInputEl()
571
- }
516
+ me.activeRecord = store.getAt(activeIndex)
517
+ me.activeRecordId = me.activeRecord[store.keyProperty || model.keyProperty]
518
+ me.getInputEl()['aria-activedescendant'] = activeItem;
572
519
 
573
- /**
574
- * @protected
575
- */
576
- onSelectPreFirstItem() {
577
- this.record = null;
578
- this.focusInputEl()
520
+ // Update typeahead hint (which updates DOM), or update DOM
521
+ me.typeAhead ? me.updateTypeAheadValue(me.lastManualInput) : me.update();
522
+ }
579
523
  }
580
524
 
581
525
  /**
@@ -614,33 +558,73 @@ class Select extends Picker {
614
558
  let me = this;
615
559
 
616
560
  if (!Neo.isNumber(index)) {
617
- if (me.hintRecordId) {
618
- index = me.store.indexOfKey(me.hintRecordId);
561
+ if (me.activeRecordId) {
562
+ index = me.store.indexOfKey(me.activeRecordId);
619
563
  } else {
620
564
  index = 0;
621
565
  }
622
566
  }
623
567
 
624
568
  me.list.selectItem(index);
625
- me.onListItemNavigate(me.store.getAt(index))
569
+ }
570
+
571
+ onPickerTriggerClick() {
572
+ let me = this;
573
+
574
+ if (me.picker?.isVisible) {
575
+ me.picker.hidden = true;
576
+ }
577
+ else if (!me.readOnly && !me.disabled) {
578
+ me.doFilter(null)
579
+ }
626
580
  }
627
581
 
628
582
  /**
583
+ * All routes which expect to open the picker route through here. This updates the
584
+ * filter and ensures that the picker is visible and reflecting the state of the filter.
629
585
  *
630
- */
631
- togglePicker() {
632
- let me = this,
633
- filter;
586
+ * Input event processing passes the current input field value in as the filter value.
587
+ *
588
+ * Invocation of the expand trigger passes `null` so as to clear filtering.
589
+ * @private
590
+ * @param {String}null} value The value to filter the picker by
591
+ */
592
+ doFilter(value) {
593
+ let me = this,
594
+ filter = me.store.getFilter(me.displayField),
595
+ {
596
+ record,
597
+ picker
598
+ } = me;
634
599
 
635
- if (me.triggerAction === 'all' && !me.pickerIsMounted) {
636
- filter = me.store.getFilter(me.displayField);
600
+ if (filter) {
601
+ filter.value = value;
602
+ }
637
603
 
638
- if (filter) {
639
- filter.value = null;
604
+ // Filter resulting in something to show
605
+ if (me.store.getCount()) {
606
+ me.getPicker().hidden = false;
607
+
608
+ // List might not exist until the picker is created
609
+ const
610
+ { list } = me,
611
+ { selectionModel } = list;
612
+
613
+ // On show, set the active item to be the current selected record or the first
614
+ if (record) {
615
+ // We do not want to hear back about our own selection
616
+ selectionModel.suspendEvents = true;
617
+ selectionModel.select(record);
618
+ selectionModel.suspendEvents = false;
640
619
  }
620
+ setTimeout(() => {
621
+ list.activeIndex = me.record || 0;
622
+ }, 100)
623
+ }
624
+ // Filtered down to nothing - hide picker if it has been created.
625
+ else if (picker) {
626
+ picker._hidden = true;
641
627
  }
642
-
643
- super.togglePicker()
644
628
  }
645
629
 
646
630
  /**
@@ -658,7 +642,7 @@ class Select extends Picker {
658
642
  cls: ['neo-input-field-wrapper'],
659
643
  cn : [{
660
644
  tag : 'input',
661
- autocomplete: 'no',
645
+ autocomplete: 'off',
662
646
  autocorrect : 'off',
663
647
  cls : ['neo-textfield-input', 'neo-typeahead-input'],
664
648
  disabled : true,
@@ -674,70 +658,37 @@ class Select extends Picker {
674
658
  }
675
659
 
676
660
  /**
677
- * @param {String|null} [value=this.value]
661
+ * @param {String|null} [value=this.lastManualInput]
678
662
  * @param {Boolean} [silent=false]
679
663
  * @protected
680
664
  */
681
- updateTypeAheadValue(value=this.value, silent=false) {
665
+ updateTypeAheadValue(value=this.lastManualInput, silent=false) {
682
666
  let me = this,
683
- hasMatch = false,
684
- store = me.store,
685
- i = 0,
686
- len = store.getCount(),
687
- inputHintEl = me.getInputHintEl(),
688
- storeValue;
689
-
690
- if (!me.record && value && value.length > 0) {
691
- for (; i < len; i++) {
692
- storeValue = store.items[i][me.displayField];
693
-
694
- if (!Neo.isString(storeValue)) {
695
- return;
696
- }
697
-
698
- if (storeValue.toLowerCase().indexOf(value.toLowerCase()) === 0) {
699
- hasMatch = true;
700
- break;
701
- }
667
+ match = false,
668
+ {
669
+ store,
670
+ displayField
702
671
  }
672
+ = me,
673
+ inputHintEl = me.getInputHintEl();
674
+
675
+ if (!me.record && value?.length > 0) {
676
+ const search = value.toLocaleLowerCase();
677
+ match = store.items.find(r => r[displayField]?.toLowerCase?.()?.startsWith(search));
703
678
 
704
- if (hasMatch && inputHintEl) {
705
- inputHintEl.value = value + storeValue.substr(value.length);
706
- me.hintRecordId = store.items[i][store.keyProperty || store.model.keyProperty]
679
+ if (match && inputHintEl) {
680
+ inputHintEl.value = value + match[displayField].substr(value.length);
681
+ me.activeRecord = match;
682
+ me.activeRecordId = match[store.keyProperty || store.model.keyProperty]
707
683
  }
708
684
  }
709
685
 
710
- if (!hasMatch && inputHintEl) {
711
- inputHintEl.value = null;
712
- me.hintRecordId = null;
686
+ if (!match && inputHintEl) {
687
+ inputHintEl.value = me.activeRecord = me.activeRecordId = null;
713
688
  }
714
689
 
715
690
  !silent && me.update()
716
691
  }
717
-
718
- /**
719
- * @param {Boolean} silent=false
720
- * @protected
721
- */
722
- updateValue(silent=false) {
723
- let me = this,
724
- displayField = me.displayField,
725
- store = me.store,
726
- value = me._value,
727
- filter;
728
-
729
- if (store && !Neo.isEmpty(store.filters)) {
730
- filter = store.getFilter(displayField);
731
-
732
- if (filter) {
733
- filter.value = me.record?.[displayField] || value;
734
- }
735
- }
736
-
737
- if (me.typeAhead && !me.picker?.containsFocus) {
738
- me.updateTypeAheadValue(value, silent)
739
- }
740
- }
741
692
  }
742
693
 
743
694
  /**
@@ -318,7 +318,7 @@ class Text extends Base {
318
318
  * @protected
319
319
  */
320
320
  afterSetAutoComplete(value, oldValue) {
321
- this.changeInputElKey('autocomplete', value ? null : 'no')
321
+ this.changeInputElKey('autocomplete', value ? null : 'off')
322
322
  }
323
323
 
324
324
  /**
@@ -1323,7 +1323,7 @@ class Text extends Base {
1323
1323
  onInputValueChange(data) {
1324
1324
  let me = this,
1325
1325
  oldValue = me.value,
1326
- value = data.value,
1326
+ value = data.value ? data.value.toString().trim() : me.emptyValue,
1327
1327
  vnode = VNodeUtil.findChildVnode(me.vnode, {nodeName: 'input'});
1328
1328
 
1329
1329
  if (vnode) {
@@ -1526,7 +1526,7 @@ class Text extends Base {
1526
1526
  minLength = me.minLength,
1527
1527
  required = me.required,
1528
1528
  returnValue = true,
1529
- value = me.value,
1529
+ value = me.value ? me.value.toString().trim() : me.emptyValue,
1530
1530
  valueLength = value?.toString().length,
1531
1531
  inputPattern = me.inputPattern,
1532
1532
  isEmpty = value !== 0 && (!value || valueLength < 1),