neo.mjs 6.10.9 → 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 (80) 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/collection/Base.mjs +4 -0
  52. package/src/component/Base.mjs +7 -0
  53. package/src/container/Base.mjs +6 -12
  54. package/src/core/Base.mjs +5 -2
  55. package/src/data/Model.mjs +7 -0
  56. package/src/data/RecordFactory.mjs +5 -4
  57. package/src/form/field/Base.mjs +11 -0
  58. package/src/form/field/Picker.mjs +0 -1
  59. package/src/form/field/Select.mjs +208 -257
  60. package/src/form/field/Text.mjs +3 -3
  61. package/src/form/field/trigger/Base.mjs +5 -6
  62. package/src/layout/Flexbox.mjs +23 -31
  63. package/src/layout/HBox.mjs +1 -1
  64. package/src/layout/VBox.mjs +1 -1
  65. package/src/list/Base.mjs +64 -31
  66. package/src/main/DomAccess.mjs +55 -28
  67. package/src/main/DomEvents.mjs +2 -1
  68. package/src/main/DomUtils.mjs +66 -0
  69. package/src/main/addon/Navigator.mjs +332 -0
  70. package/src/manager/DomEvent.mjs +2 -1
  71. package/src/selection/ListModel.mjs +46 -82
  72. package/src/selection/Model.mjs +56 -33
  73. package/src/util/Array.mjs +5 -2
  74. package/src/util/Function.mjs +31 -0
  75. package/src/util/String.mjs +9 -0
  76. package/src/vdom/Helper.mjs +1 -2
  77. package/test/components/app.mjs +4 -3
  78. package/test/components/files/component/ChipList.mjs +125 -0
  79. package/test/components/files/form/field/Select.mjs +177 -2
  80. package/test/components/siesta.js +34 -1
@@ -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),
@@ -37,7 +37,7 @@ class Base extends Component {
37
37
  /**
38
38
  * @member {Neo.form.field.Base|null} field=null
39
39
  */
40
- field: null,
40
+ field_: null,
41
41
  /**
42
42
  * @member {String|null} iconCls_=null
43
43
  */
@@ -62,11 +62,6 @@ class Base extends Component {
62
62
  * @protected
63
63
  */
64
64
  type: 'base',
65
- /**
66
- * @member {Object} _vdom={tabIndex: -1}
67
- */
68
- _vdom:
69
- {tabIndex: -1},
70
65
  /**
71
66
  * @member {Number} weight_=10
72
67
  */
@@ -110,6 +105,10 @@ class Base extends Component {
110
105
  this.cls = cls;
111
106
  }
112
107
 
108
+ afterSetField(field) {
109
+ (this.vdom.data || (this.vdom.data = {})).focus = field && field.getInputElId();
110
+ }
111
+
113
112
  /**
114
113
  * Triggered after the hidden config got changed
115
114
  * @param {Boolean} value
@@ -85,7 +85,7 @@ class Flexbox extends Base {
85
85
  * @protected
86
86
  */
87
87
  afterSetAlign(value, oldValue) {
88
- oldValue && this.updateInputValue(value, oldValue, 'align');
88
+ oldValue && this.updateInputValue(value, oldValue, 'align')
89
89
  }
90
90
 
91
91
  /**
@@ -95,7 +95,7 @@ class Flexbox extends Base {
95
95
  * @protected
96
96
  */
97
97
  afterSetDirection(value, oldValue) {
98
- oldValue && this.updateInputValue(value, oldValue, 'direction');
98
+ oldValue && this.updateInputValue(value, oldValue, 'direction')
99
99
  }
100
100
 
101
101
  /**
@@ -111,7 +111,7 @@ class Flexbox extends Base {
111
111
  style = item.wrapperStyle;
112
112
 
113
113
  style.gap = value;
114
- item.wrapperStyle = style;
114
+ item.wrapperStyle = style
115
115
  }
116
116
 
117
117
  /**
@@ -121,7 +121,7 @@ class Flexbox extends Base {
121
121
  * @protected
122
122
  */
123
123
  afterSetPack(value, oldValue) {
124
- oldValue && this.updateInputValue(value, oldValue, 'pack');
124
+ oldValue && this.updateInputValue(value, oldValue, 'pack')
125
125
  }
126
126
 
127
127
  /**
@@ -131,7 +131,7 @@ class Flexbox extends Base {
131
131
  * @protected
132
132
  */
133
133
  afterSetWrap(value, oldValue) {
134
- oldValue && this.updateInputValue(value, oldValue, 'wrap');
134
+ oldValue && this.updateInputValue(value, oldValue, 'wrap')
135
135
  }
136
136
 
137
137
  /**
@@ -143,11 +143,11 @@ class Flexbox extends Base {
143
143
  flex = style.flex || item.flex || (this.align === 'stretch' ? 1 : '0 1 auto');
144
144
 
145
145
  if (flex === 1) {
146
- flex = '1 1 auto';
146
+ flex = '1 1 auto'
147
147
  }
148
148
 
149
149
  style.flex = flex;
150
- item.wrapperStyle = style;
150
+ item.wrapperStyle = style
151
151
  }
152
152
 
153
153
  /**
@@ -160,7 +160,7 @@ class Flexbox extends Base {
160
160
  wrapperCls = container?.wrapperCls || [];
161
161
 
162
162
  if (!container) {
163
- Neo.logError('layout.Flexbox: applyRenderAttributes -> container not yet created', me.containerId);
163
+ Neo.logError('layout.Flexbox: applyRenderAttributes -> container not yet created', me.containerId)
164
164
  }
165
165
 
166
166
  NeoArray.add(wrapperCls, prefix + 'container');
@@ -170,7 +170,7 @@ class Flexbox extends Base {
170
170
  me.pack && NeoArray.add(wrapperCls, prefix + 'pack-' + me.pack);
171
171
  me.wrap && NeoArray.add(wrapperCls, prefix + 'wrap-' + me.wrap);
172
172
 
173
- container.wrapperCls = wrapperCls;
173
+ container.wrapperCls = wrapperCls
174
174
  }
175
175
 
176
176
  /**
@@ -181,7 +181,7 @@ class Flexbox extends Base {
181
181
  * @returns {String|null} value
182
182
  */
183
183
  beforeSetAlign(value, oldValue) {
184
- return this.testInputValue(value, oldValue, 'alignValues', 'align');
184
+ return this.testInputValue(value, oldValue, 'alignValues', 'align')
185
185
  }
186
186
 
187
187
  /**
@@ -203,7 +203,7 @@ class Flexbox extends Base {
203
203
  * @returns {String|null} value
204
204
  */
205
205
  beforeSetPack(value, oldValue) {
206
- return this.testInputValue(value, oldValue, 'packValues', 'pack');
206
+ return this.testInputValue(value, oldValue, 'packValues', 'pack')
207
207
  }
208
208
 
209
209
  /**
@@ -214,7 +214,7 @@ class Flexbox extends Base {
214
214
  * @returns {String} value
215
215
  */
216
216
  beforeSetWrap(value, oldValue) {
217
- return this.testInputValue(value, oldValue, 'wrapValues', 'wrap');
217
+ return this.testInputValue(value, oldValue, 'wrapValues', 'wrap')
218
218
  }
219
219
 
220
220
  /**
@@ -227,7 +227,7 @@ class Flexbox extends Base {
227
227
  let style = item.wrapperStyle || {};
228
228
 
229
229
  style.flex = item.flex || null;
230
- item.wrapperStyle = style;
230
+ item.wrapperStyle = style
231
231
  }
232
232
 
233
233
  /**
@@ -242,25 +242,17 @@ class Flexbox extends Base {
242
242
  wrapperCls = container?.wrapperCls || [];
243
243
 
244
244
  if (!container) {
245
- Neo.logError('layout.Flexbox: removeRenderAttributes -> container not yet created', me.containerId);
245
+ Neo.logError('layout.Flexbox: removeRenderAttributes -> container not yet created', me.containerId)
246
246
  }
247
247
 
248
248
  NeoArray.remove(wrapperCls, prefix + 'container');
249
249
 
250
- if (me.align) {
251
- NeoArray.remove(wrapperCls, prefix + 'align-' + me.align);
252
- }
253
- if (me.direction) {
254
- NeoArray.remove(wrapperCls, prefix + 'direction-' + me.direction);
255
- }
256
- if (me.pack) {
257
- NeoArray.remove(wrapperCls, prefix + 'pack-' + me.pack);
258
- }
259
- if (me.wrap) {
260
- NeoArray.remove(wrapperCls, prefix + 'wrap-' + me.wrap);
261
- }
250
+ me.align && NeoArray.remove(wrapperCls, prefix + 'align-' + me.align);
251
+ me.direction && NeoArray.remove(wrapperCls, prefix + 'direction-' + me.direction);
252
+ me.pack && NeoArray.remove(wrapperCls, prefix + 'pack-' + me.pack);
253
+ me.wrap && NeoArray.remove(wrapperCls, prefix + 'wrap-' + me.wrap);
262
254
 
263
- container.wrapperCls = wrapperCls;
255
+ container.wrapperCls = wrapperCls
264
256
  }
265
257
 
266
258
  /**
@@ -277,10 +269,10 @@ class Flexbox extends Base {
277
269
 
278
270
  if (!NeoArray.hasItem(validValues, value)) {
279
271
  Neo.logError(this.containerId, '-> layout: supported values for "' + propertyName + '" are' , validValues);
280
- return oldValue;
272
+ return oldValue
281
273
  }
282
274
 
283
- return value;
275
+ return value
284
276
  }
285
277
 
286
278
  /**
@@ -300,10 +292,10 @@ class Flexbox extends Base {
300
292
  NeoArray.remove(wrapperCls, prefix + propertyName + '-' + oldValue);
301
293
 
302
294
  if (value !== null) {
303
- NeoArray.add(wrapperCls, prefix + propertyName + '-' + value);
295
+ NeoArray.add(wrapperCls, prefix + propertyName + '-' + value)
304
296
  }
305
297
 
306
- container.wrapperCls = wrapperCls;
298
+ container.wrapperCls = wrapperCls
307
299
  }
308
300
  }
309
301
  }
@@ -29,7 +29,7 @@ class HBox extends Flexbox {
29
29
  */
30
30
  applyChildAttributes(item) {
31
31
  // Do not apply flex if fixed width
32
- !item.width && super.applyChildAttributes(item);
32
+ !item.width && super.applyChildAttributes(item)
33
33
  }
34
34
  }
35
35
 
@@ -29,7 +29,7 @@ class VBox extends Flexbox {
29
29
  */
30
30
  applyChildAttributes(item) {
31
31
  // Do not apply flex if fixed height
32
- !item.height && super.applyChildAttributes(item);
32
+ !item.height && super.applyChildAttributes(item)
33
33
  }
34
34
  }
35
35
 
package/src/list/Base.mjs CHANGED
@@ -135,7 +135,13 @@ class Base extends Component {
135
135
  * @member {Object} _vdom
136
136
  */
137
137
  _vdom:
138
- {tag: 'ul', cn: []}
138
+ {tag: 'ul', cn: []},
139
+
140
+ /**
141
+ * An object to help configure the navigation. Used to pass to {@link Neo.main.addon.Navigator#subscribe}.
142
+ * @member {Object} navigator={}
143
+ */
144
+ navigator : {}
139
145
  }
140
146
 
141
147
  /**
@@ -172,16 +178,15 @@ class Base extends Component {
172
178
  * @param {Number|null} oldValue
173
179
  * @protected
174
180
  */
175
- afterSetActiveIndex(value, oldValue) {
176
- let me = this,
177
- selectionModel = me.selectionModel;
181
+ afterSetActiveIndex(value) {
182
+ let me = this;
178
183
 
179
184
  if (Neo.isNumber(value)) {
180
- selectionModel?.selectAt(value);
181
185
  me.headerlessActiveIndex = me.getHeaderlessIndex(value)
182
- } else if (Neo.isNumber(oldValue)) {
183
- selectionModel.deselectAll();
184
- me.headerlessActiveIndex = null
186
+ Neo.main.addon.Navigator.navigateTo([value, this.navigator])
187
+ }
188
+ else if (value) {
189
+ Neo.main.addon.Navigator.navigateTo([me.getItemId(value[me.getKeyProperty()]), this.navigator])
185
190
  }
186
191
  }
187
192
 
@@ -257,6 +262,33 @@ class Base extends Component {
257
262
  }
258
263
  }
259
264
 
265
+ afterSetMounted(value) {
266
+ const me = this;
267
+
268
+ // Tear down navigation before we lose the element
269
+ if (!value && me.hasNavigator) {
270
+ Neo.main.addon.Navigator.unsubscribe(me.navigator);
271
+ me.hasNavigator = false;
272
+ me.activeIndex = null
273
+ }
274
+
275
+ super.afterSetMounted(...arguments);
276
+
277
+ if (value) {
278
+ // Set up item navigation in the list
279
+ if (!me.hasNavigator) {
280
+ me.navigator = {
281
+ appName : me.appName,
282
+ id : me.id,
283
+ selector : `.${me.itemCls}:not(.neo-disabled,.neo-list-header)`,
284
+ ...me.navigator
285
+ }
286
+ me.hasNavigator = true;
287
+ }
288
+ Neo.main.addon.Navigator.subscribe(me.navigator)
289
+ }
290
+ }
291
+
260
292
  /**
261
293
  * Triggered after the selectionModel config got changed
262
294
  * @param {Neo.selection.Model} value
@@ -392,12 +424,16 @@ class Base extends Component {
392
424
  }
393
425
 
394
426
  item = {
395
- tag : isHeader ? 'dt' : me.itemTagName,
396
- cls,
397
- id : itemId,
398
- tabIndex: -1
427
+ id : itemId,
428
+ tag : isHeader ? 'dt' : me.itemTagName,
429
+ 'aria-selected' : false,
430
+ cls
399
431
  };
400
432
 
433
+ if (me.itemsFocusable) {
434
+ item.tabIndex = -1;
435
+ }
436
+
401
437
  if (record.hidden) {
402
438
  item.removeDom = true
403
439
  }
@@ -513,15 +549,7 @@ class Base extends Component {
513
549
  * @param {String} [id]
514
550
  */
515
551
  focus(id) {
516
- super.focus(id);
517
-
518
- let me = this;
519
-
520
- id && me.scrollIntoViewOnFocus && Neo.main.DomAccess.scrollIntoView({
521
- appName : me.appName,
522
- behavior: 'auto',
523
- id : id || me.id
524
- })
552
+ Neo.main.addon.Navigator.navigateTo(id, this.navigator)
525
553
  }
526
554
 
527
555
  /**
@@ -569,11 +597,11 @@ class Base extends Component {
569
597
  }
570
598
 
571
599
  /**
572
- * @param {Number|String} recordId
600
+ * @param {Number|String|object} recordOrId
573
601
  * @returns {String}
574
602
  */
575
- getItemId(recordId) {
576
- return `${this.id}__${recordId}`
603
+ getItemId(recordOrId) {
604
+ return `${this.id}__${recordOrId.isRecord ? recordOrId[this.getKeyProperty()] : recordOrId}`
577
605
  }
578
606
 
579
607
  /**
@@ -655,10 +683,6 @@ class Base extends Component {
655
683
  // pass the record to class extensions
656
684
  data.record = record;
657
685
 
658
- if (!me.disableSelection && (!me.useHeaders || !record.isHeader)) {
659
- me.selectionModel?.select(node.id)
660
- }
661
-
662
686
  /**
663
687
  * The itemClick event fires when a click occurs on a list item
664
688
  * @event itemClick
@@ -723,10 +747,19 @@ class Base extends Component {
723
747
 
724
748
  /**
725
749
  * Convenience shortcut
726
- * @param {Number} index
750
+ * @param {Number|String} item
727
751
  */
728
- selectItem(index) {
729
- !this.disableSelection && this.selectionModel?.selectAt(index)
752
+ selectItem(item) {
753
+ if (!this.disableSelection) {
754
+ // Selecting index
755
+ if (Neo.isNumber(item)) {
756
+ this.selectionModel?.selectAt(item)
757
+ }
758
+ // Selecting record
759
+ else if (item) {
760
+ this.selectionModel?.selectAt(this.store.indexOf(item));
761
+ }
762
+ }
730
763
  }
731
764
  }
732
765
 
@@ -2,23 +2,14 @@ import Base from '../core/Base.mjs';
2
2
  import DeltaUpdates from './mixin/DeltaUpdates.mjs';
3
3
  import Observable from '../core/Observable.mjs';
4
4
  import Rectangle from '../util/Rectangle.mjs';
5
+ import String from '../util/String.mjs';
6
+ import DomUtils from './DomUtils.mjs';
5
7
 
6
8
  const
7
9
  doPreventDefault = e => e.preventDefault(),
8
- filterTabbable = e => !e.classList.contains('neo-focus-trap') && isTabbable(e) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
10
+ filterTabbable = e => !e.classList.contains('neo-focus-trap') && DomUtils.isTabbable(e) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
9
11
  lengthRE = /^\d+\w+$/,
10
12
 
11
- focusableTags = {
12
- BODY : 1,
13
- BUTTON : 1,
14
- EMBED : 1,
15
- IFRAME : 1,
16
- INPUT : 1,
17
- OBJECT : 1,
18
- SELECT : 1,
19
- TEXTAREA : 1
20
- },
21
-
22
13
  fontSizeProps = [
23
14
  'font-family',
24
15
  'font-kerning',
@@ -34,21 +25,15 @@ const
34
25
  'word-break'
35
26
  ],
36
27
 
37
- isTabbable = e => {
38
- const
39
- { nodeName } = e,
40
- style = getComputedStyle(e),
41
- tabIndex = e.getAttribute('tabIndex');
42
-
43
- // Hidden elements not tabbable
44
- if (style.getPropertyValue('display') === 'none' || style.getPropertyValue('visibility') === 'hidden') {
45
- return false
46
- }
47
-
48
- return focusableTags[nodeName] ||
49
- ((nodeName === 'A' || nodeName === 'LINK') && !!e.href) ||
50
- (tabIndex != null && Number(tabIndex) >= 0) ||
51
- e.contentEditable === 'true'
28
+ capturePassive = {
29
+ capture : true,
30
+ passive : true
31
+ },
32
+ modifierKeys = {
33
+ Shift : 1,
34
+ Alt : 1,
35
+ Meta : 1,
36
+ Control : 1
52
37
  };
53
38
 
54
39
  /**
@@ -103,6 +88,8 @@ class DomAccess extends Base {
103
88
  'getScrollingDimensions',
104
89
  'measure',
105
90
  'monitorAutoGrow',
91
+ 'navigate',
92
+ 'navigateTo',
106
93
  'scrollBy',
107
94
  'scrollIntoView',
108
95
  'scrollTo',
@@ -168,11 +155,51 @@ class DomAccess extends Base {
168
155
  }, 250)
169
156
  }
170
157
 
158
+ me.initGlobalListeners();
159
+
171
160
  // Set up our aligning callback which is called when things change which may
172
161
  // mean that alignments need to be updated.
173
162
  me.syncAligns = me.syncAligns.bind(me)
174
163
  }
175
164
 
165
+ initGlobalListeners() {
166
+ const me = this;
167
+
168
+ document.addEventListener('mousedown', me.onDocumentMouseDown.bind(me), { capture : true });
169
+ document.addEventListener('keydown', me.onDocumentKeyDown.bind(me), capturePassive);
170
+ document.addEventListener('keyup', me.onDocumentKeyUp.bind(me), capturePassive);
171
+ document.addEventListener('blur', me.onDocumentBlur.bind(me), capturePassive);
172
+ }
173
+
174
+ onDocumentMouseDown(e) {
175
+ const focusController = e.target?.closest('[data-focus]');
176
+
177
+ // data-focus on an element means reject mousedown gestures, and move focus
178
+ // to the referenced element.
179
+ if (focusController) {
180
+ e.preventDefault();
181
+ document.getElementById(focusController.dataset.focus)?.focus();
182
+ }
183
+ }
184
+
185
+ onDocumentKeyDown(keyEvent) {
186
+ if (modifierKeys[keyEvent.key]) {
187
+ // eg Neo.isShiftKeyDown = true or Neo.isControlKeyDown = true.
188
+ // Selection can consult this value
189
+ Neo[`${String.uncapitalize(keyEvent.key)}KeyDown`] = true;
190
+ }
191
+ }
192
+
193
+ onDocumentKeyUp(keyEvent) {
194
+ if (modifierKeys[keyEvent.key]) {
195
+ Neo[`${String.uncapitalize(keyEvent.key)}KeyDown`] = false;
196
+ }
197
+ }
198
+
199
+ onDocumentBlur() {
200
+ Neo.altKeyDown = Neo.controlKeyDown = Neo.metaKeyDown = Neo.shiftKeyDown = false;
201
+ }
202
+
176
203
  /**
177
204
  * @param {Object} alignSpec
178
205
  */
@@ -433,7 +460,7 @@ class DomAccess extends Base {
433
460
  { defaultView } = node.ownerDocument,
434
461
  rect = this.getBoundingClientRect(node);
435
462
 
436
- for (let parentElement = node.offsetParent; rect && parentElement !== document.documentElement; parentElement = parentElement.parentElement) {
463
+ for (let parentElement = node.offsetParent; parentElement && rect && parentElement !== document.documentElement; parentElement = parentElement.parentElement) {
437
464
  if (defaultView.getComputedStyle(parentElement).getPropertyValue('overflow') !== 'visible') {
438
465
  rect = rect.intersects(this.getBoundingClientRect(parentElement))
439
466
  }
@@ -267,7 +267,8 @@ class DomEvents extends Base {
267
267
  path : path.map(e => this.getTargetData(e)),
268
268
  target : this.getTargetData(event.target),
269
269
  timeStamp: event.timeStamp,
270
- type : event.type
270
+ type : event.type,
271
+ data : {...event.target.dataset}
271
272
  };
272
273
 
273
274
  if (event.relatedTarget) {
@@ -0,0 +1,66 @@
1
+ import Base from '../core/Base.mjs';
2
+
3
+ const focusableTags = {
4
+ BODY : 1,
5
+ BUTTON : 1,
6
+ EMBED : 1,
7
+ IFRAME : 1,
8
+ INPUT : 1,
9
+ OBJECT : 1,
10
+ SELECT : 1,
11
+ TEXTAREA : 1
12
+ };
13
+
14
+ /**
15
+ * @class Neo.main.DomUtils
16
+ * @extends Neo.core.Base
17
+ * @singleton
18
+ */
19
+ export default class DomUtils extends Base {
20
+ static config = {
21
+ /**
22
+ * @member {String} className='Neo.main.DomUtils'
23
+ * @protected
24
+ */
25
+ className: 'Neo.main.DomUtils'
26
+ }
27
+
28
+ /**
29
+ * Analogous to the `HTMLElement` `closest` method. Searches starting at the passed element for
30
+ * an element for which the passed `filterFn` returns `true`
31
+ * @param {HTMLElement} el The element to start from.
32
+ * @param {Function} filterFn A function which returns `true` when the desired element is reached.
33
+ * @param {HTMLElement} [limit] The element to stop at. This is *not* considered for matching.
34
+ * @returns
35
+ */
36
+ static closest(el, filterFn, limit = document.body) {
37
+ while (el?.nodeType === Node.ELEMENT_NODE && el !== limit) {
38
+ if (filterFn(el)) {
39
+ return el;
40
+ }
41
+ el = el.parentNode;
42
+ }
43
+ }
44
+
45
+ static isFocusable(e) {
46
+ // May be used as a scopeless callback, so use "DomUtils", not "this"
47
+ return DomUtils.isTabbable(e) || e.getAttribute('tabIndex') == -1;
48
+ }
49
+
50
+ static isTabbable(e) {
51
+ const
52
+ { nodeName } = e,
53
+ style = getComputedStyle(e),
54
+ tabIndex = e.getAttribute('tabIndex');
55
+
56
+ // Hidden elements not tabbable
57
+ if (!e.offsetParent || style.getPropertyValue('visibility') === 'hidden') {
58
+ return false
59
+ }
60
+
61
+ return focusableTags[nodeName] ||
62
+ ((nodeName === 'A' || nodeName === 'LINK') && !!e.href) ||
63
+ (tabIndex != null && Number(tabIndex) >= 0) ||
64
+ e.contentEditable === 'true'
65
+ }
66
+ }