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.
- package/apps/ServiceWorker.mjs +2 -2
- package/apps/portal/view/learn/ContentTreeList.mjs +24 -12
- package/apps/portal/view/learn/LivePreview.mjs +28 -11
- package/buildScripts/createAppMinimal.mjs +391 -0
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/button/base/neo-config.json +2 -1
- package/examples/list/chip/neo-config.json +1 -2
- package/package.json +72 -70
- package/resources/data/deck/learnneo/data/theBeatles.json +22 -0
- package/resources/data/deck/learnneo/p/2023-10-14T19-25-08-153Z.md +29 -20
- package/resources/data/deck/learnneo/p/ComponentModels.md +116 -1
- package/resources/data/deck/learnneo/p/Config.md +157 -0
- package/resources/data/deck/learnneo/p/DescribingTheUI.md +67 -1
- package/resources/data/deck/learnneo/p/Earthquakes.md +214 -0
- package/resources/data/deck/learnneo/p/Events.md +142 -1
- package/resources/data/deck/learnneo/p/Extending.md +116 -1
- package/resources/data/deck/learnneo/p/References.md +126 -0
- package/resources/data/deck/learnneo/p/TestLivePreview.md +28 -6
- package/resources/data/deck/learnneo/t.json +5 -6
- package/resources/data/deck/training/p/2022-12-27T21-55-30-948Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-27T22-23-55-083Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-29T16-00-13-223Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-29T18-34-25-826Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-29T18-36-56-893Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-31T18-43-56-338Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-31T18-51-50-682Z.md +1 -1
- package/resources/data/deck/training/p/2022-12-31T18-54-04-176Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-01T17-49-18-429Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-01T21-23-17-716Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-06T23-21-31-685Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-06T23-34-13-897Z.md +2 -2
- package/resources/data/deck/training/p/2023-01-06T23-46-36-687Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-08T01-24-21-088Z.md +1 -1
- package/resources/data/deck/training/p/2023-01-08T02-11-26-333Z.md +2 -2
- package/resources/data/deck/training/p/2023-01-14T00-40-27-784Z.md +2 -2
- package/resources/data/deck/training/p/2023-07-31T00-37-21-927Z.md +2 -2
- package/resources/data/deck/training/p/2023-10-14T19-25-08-153Z.md +3 -3
- package/resources/scss/src/apps/newwebsite/Viewport.scss +32 -0
- package/resources/scss/src/apps/portal/learn/ContentView.scss +20 -4
- package/resources/scss/src/apps/portal/learn/LivePreview.scss +8 -0
- package/resources/scss/src/component/Base.scss +13 -4
- package/resources/scss/src/form/field/Select.scss +2 -5
- package/resources/scss/src/form/field/Text.scss +0 -1
- package/resources/scss/src/list/Base.scss +47 -2
- package/resources/scss/src/list/Chip.scss +10 -4
- package/resources/scss/theme-dark/list/Base.scss +11 -10
- package/resources/scss/theme-light/list/Base.scss +11 -10
- package/resources/scss/theme-neo-light/design-tokens/Components.scss +3 -0
- package/resources/scss/theme-neo-light/list/Base.scss +1 -0
- package/src/DefaultConfig.mjs +3 -3
- package/src/component/Base.mjs +7 -0
- package/src/container/Base.mjs +6 -12
- package/src/core/Base.mjs +5 -2
- package/src/data/Model.mjs +7 -0
- package/src/data/RecordFactory.mjs +5 -4
- package/src/form/field/Base.mjs +11 -0
- package/src/form/field/Picker.mjs +0 -1
- package/src/form/field/Select.mjs +208 -257
- package/src/form/field/Text.mjs +3 -3
- package/src/form/field/trigger/Base.mjs +5 -6
- package/src/layout/Flexbox.mjs +23 -31
- package/src/layout/HBox.mjs +1 -1
- package/src/layout/VBox.mjs +1 -1
- package/src/list/Base.mjs +64 -31
- package/src/main/DomAccess.mjs +55 -28
- package/src/main/DomEvents.mjs +2 -1
- package/src/main/DomUtils.mjs +66 -0
- package/src/main/addon/Navigator.mjs +332 -0
- package/src/manager/DomEvent.mjs +2 -1
- package/src/selection/ListModel.mjs +46 -82
- package/src/selection/Model.mjs +56 -33
- package/src/util/Array.mjs +5 -2
- package/src/util/Function.mjs +31 -0
- package/src/util/String.mjs +9 -0
- package/src/vdom/Helper.mjs +1 -2
- package/test/components/app.mjs +4 -3
- package/test/components/files/component/ChipList.mjs +125 -0
- package/test/components/files/form/field/Select.mjs +177 -2
- package/test/components/siesta.js +34 -1
@@ -0,0 +1,332 @@
|
|
1
|
+
import Base from '../../core/Base.mjs';
|
2
|
+
import DomAccess from '../DomAccess.mjs';
|
3
|
+
import DomUtils from '../DomUtils.mjs';
|
4
|
+
import DomEvents from '../DomEvents.mjs';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Addon for Navigator
|
8
|
+
* @class Neo.main.addon.Navigator
|
9
|
+
* @extends Neo.core.Base
|
10
|
+
* @singleton
|
11
|
+
*/
|
12
|
+
class Navigator extends Base {
|
13
|
+
static config = {
|
14
|
+
/**
|
15
|
+
* @member {String} className='Neo.main.addon.Navigator'
|
16
|
+
* @protected
|
17
|
+
*/
|
18
|
+
className: 'Neo.main.addon.Navigator',
|
19
|
+
/**
|
20
|
+
* Remote method access for other workers
|
21
|
+
* @member {Object} remote={app: [//...]}
|
22
|
+
* @protected
|
23
|
+
*/
|
24
|
+
remote: {
|
25
|
+
app: [
|
26
|
+
'subscribe',
|
27
|
+
'unsubscribe',
|
28
|
+
'navigateTo'
|
29
|
+
]
|
30
|
+
},
|
31
|
+
/**
|
32
|
+
* @member {Boolean} singleton=true
|
33
|
+
* @protected
|
34
|
+
*/
|
35
|
+
singleton: true
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Sets up keyboard based navigation within the passed element id.
|
40
|
+
*
|
41
|
+
* When navigation occurs from one navigable element to another, the `navigate` event
|
42
|
+
* will be fired.
|
43
|
+
* @param {*} data
|
44
|
+
* @param {String} data.id The element id to navigate in.
|
45
|
+
* @param {String} [data.eventSource] Optional - the element id to read keystrokes from.
|
46
|
+
* defaults to the main element id.
|
47
|
+
* @param {String} data.selector A CSS selector which identifies the navigable elements.
|
48
|
+
* @param {String} data.activeCls A CSS class to add to the currently active navigable element.
|
49
|
+
* @param {Boolean} wrap Pass as `true` to have navigation wrap from first to last and vice versa.
|
50
|
+
*/
|
51
|
+
subscribe(data) {
|
52
|
+
const
|
53
|
+
me = this,
|
54
|
+
subject = data.subject = DomAccess.getElement(data.id),
|
55
|
+
eventSource = data.eventSource = data.eventSource ? DomAccess.getElement(data.eventSource) : subject;
|
56
|
+
|
57
|
+
subject.$navigator = data;
|
58
|
+
|
59
|
+
if (!data.activeCls) {
|
60
|
+
data.activeCls = 'neo-navigator-active-item'
|
61
|
+
}
|
62
|
+
|
63
|
+
// Finds a focusable item starting from a descendant el within one of our selector items
|
64
|
+
data.findFocusable = el => DomUtils.closest(el, el =>
|
65
|
+
// We're looking for an element that is focusable
|
66
|
+
DomUtils.isFocusable(el) &&
|
67
|
+
// And within our subject element
|
68
|
+
(subject.compcompareDocumentPosition(el) & Node.DOCUMENT_POSITION_CONTAINED_BY) &&
|
69
|
+
// And within an element that matches our selector
|
70
|
+
el.closest(data.selector)
|
71
|
+
);
|
72
|
+
|
73
|
+
// TreeWalker so that we can easily move between navigable elements within the target.
|
74
|
+
data.treeWalker = document.createTreeWalker(subject, NodeFilter.SHOW_ELEMENT, node => me.navigateNodeFilter(node, data));
|
75
|
+
|
76
|
+
// We have to know when the DOM mutates in case the active item is removed.
|
77
|
+
(data.targetMutationMonitor = new MutationObserver(e => me.navigateTargetChildListChange(e, data))).observe(subject, {
|
78
|
+
childList : true,
|
79
|
+
subtree : true
|
80
|
+
});
|
81
|
+
|
82
|
+
eventSource.addEventListener('keydown', data.l1 = e => me.navigateKeyDownHandler(e, data));
|
83
|
+
subject.addEventListener('mousedown', data.l2 = e => me.navigateMouseDownHandler(e, data));
|
84
|
+
subject.addEventListener('click', data.l3 = e => me.navigateClickHandler(e, data));
|
85
|
+
subject.addEventListener('focusin', data.l4 = e => me.navigateFocusInHandler(e, data));
|
86
|
+
}
|
87
|
+
|
88
|
+
unsubscribe(data) {
|
89
|
+
const target = DomAccess.getElement(data.id);
|
90
|
+
|
91
|
+
data = target?.$navigator;
|
92
|
+
if (data) {
|
93
|
+
delete target.$navigator;
|
94
|
+
data.targetMutationMonitor.disconnect(target);
|
95
|
+
data.eventSource.removeEventListener('keydown', data.l1);
|
96
|
+
target.removeEventListener('mousedown', data.l2);
|
97
|
+
target.removeEventListener('click', data.l3);
|
98
|
+
target.removeEventListener('focusin', data.l4);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
navigateTargetChildListChange(mutations, data) {
|
103
|
+
// Active item gone.
|
104
|
+
// Try to activate the item at the same index;
|
105
|
+
if (data.activeItem && !data.subject.contains(data.activeItem)) {
|
106
|
+
const allItems = data.subject.querySelectorAll(data.selector);
|
107
|
+
|
108
|
+
allItems.length && this.navigateTo(allItems[Math.max(Math.min(data.activeIndex, allItems.length - 1), 0)], data);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
navigateFocusInHandler(e, data) {
|
113
|
+
const target = e.target.closest(data.selector);
|
114
|
+
|
115
|
+
// If our targets are focusable and recieve focus, that is a navigation.
|
116
|
+
if (target) {
|
117
|
+
this.setActiveItem(target, data);
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
navigateClickHandler(e, data) {
|
122
|
+
const target = e.target.closest(data.selector);
|
123
|
+
|
124
|
+
// If there was a focusable under the mouse, mousedown will have focused it and and we
|
125
|
+
// will have respond to that in navigateFocusInHandler.
|
126
|
+
// If not, we navigate programatically.
|
127
|
+
if (target && !data.findFocusable(target)) {
|
128
|
+
this.navigateTo(target, data);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
navigateMouseDownHandler(e, data) {
|
133
|
+
const target = e.target.closest(data.selector);
|
134
|
+
|
135
|
+
// If there is a focusable undet the mouse, it will take focus, and we respond to that in navigateFocusInHandler.
|
136
|
+
// If not, we have to programatically activate on click, but we must not draw focus away from
|
137
|
+
// where it is, so preventDefault
|
138
|
+
if (target && !data.findFocusable(target)) {
|
139
|
+
e.preventDefault();
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
navigateKeyDownHandler(keyEvent, data) {
|
144
|
+
const
|
145
|
+
me = this,
|
146
|
+
{
|
147
|
+
subject,
|
148
|
+
wrap
|
149
|
+
} = data,
|
150
|
+
firstItem = subject.querySelector(data.selector);
|
151
|
+
|
152
|
+
if (!data.nextKey && firstItem) {
|
153
|
+
const
|
154
|
+
containerStyle = getComputedStyle(subject),
|
155
|
+
itemStyle = getComputedStyle(firstItem);
|
156
|
+
|
157
|
+
// Detect what the next and prev keys should be.
|
158
|
+
// Child elements layed out horizontally.
|
159
|
+
if (containerStyle.display === 'flex' && containerStyle.flexDirection === 'row'
|
160
|
+
|| itemStyle.display === 'inline' || itemStyle.display === 'inline-block') {
|
161
|
+
data.previousKey = 'ArrowLeft';
|
162
|
+
data.nextKey = 'ArrowRight';
|
163
|
+
}
|
164
|
+
// Child elements layed out vertically.
|
165
|
+
else {
|
166
|
+
data.previousKey = 'ArrowUp';
|
167
|
+
data.nextKey = 'ArrowDown';
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
let { key } = keyEvent,
|
172
|
+
newActiveElement;
|
173
|
+
|
174
|
+
switch(key) {
|
175
|
+
case data.previousKey:
|
176
|
+
newActiveElement = me.navigateGetAdjacent(-1, data);
|
177
|
+
if (!newActiveElement && wrap) {
|
178
|
+
newActiveElement = subject.querySelector(`${data.selector}:last-of-type`);
|
179
|
+
}
|
180
|
+
break;
|
181
|
+
case data.nextKey:
|
182
|
+
newActiveElement = me.navigateGetAdjacent(1, data);
|
183
|
+
if (!newActiveElement && wrap) {
|
184
|
+
newActiveElement = subject.querySelector(data.selector);
|
185
|
+
}
|
186
|
+
break;
|
187
|
+
case 'Home':
|
188
|
+
newActiveElement = subject.querySelector(data.selector);
|
189
|
+
break;
|
190
|
+
case 'End':
|
191
|
+
newActiveElement = subject.querySelector(`${data.selector}:last-of-type`);
|
192
|
+
break;
|
193
|
+
case 'Enter':
|
194
|
+
if (data.activeItem) {
|
195
|
+
const
|
196
|
+
rect = data.activeItem.getBoundingClientRect(),
|
197
|
+
clientX = rect.x + (rect.width / 2),
|
198
|
+
clientY = rect.y + (rect.height / 2);
|
199
|
+
|
200
|
+
data.activeItem.dispatchEvent(new MouseEvent('click', {
|
201
|
+
bubbles : true,
|
202
|
+
altKey : Neo.altKeyDown,
|
203
|
+
ctrlKey : Neo.controlKeyDown,
|
204
|
+
metaKey : Neo.metaKeyDown,
|
205
|
+
shiftKey : Neo.shiftKeyDown,
|
206
|
+
clientX,
|
207
|
+
clientY
|
208
|
+
}))
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
if (newActiveElement) {
|
213
|
+
keyEvent.preventDefault();
|
214
|
+
me.navigateTo(newActiveElement, data);
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
/**
|
219
|
+
* Navigates to the passed
|
220
|
+
* @param {String|Number} newActiveElement The id of the new active element in the subject
|
221
|
+
* element, or the index of the item.
|
222
|
+
* @param {Object} data The data block as passed to {@link #subscribe}
|
223
|
+
* @returns
|
224
|
+
*/
|
225
|
+
navigateTo(newActiveElement, data) {
|
226
|
+
if (!data.subject) {
|
227
|
+
// If subject has been unmounted, we cannot navigate
|
228
|
+
if (!(data = DomAccess.getElement(data.id)?.$navigator)) {
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
// Can navigate by index. This is useful if the active item is deleted.
|
234
|
+
// We can navigate to the same index and preserve UI stability.
|
235
|
+
if (typeof newActiveElement === 'number') {
|
236
|
+
newActiveElement = data.subject.querySelectorAll(data.selector)[newActiveElement];
|
237
|
+
}
|
238
|
+
else if (typeof newActiveElement === 'string') {
|
239
|
+
newActiveElement = DomAccess.getElement(newActiveElement);
|
240
|
+
}
|
241
|
+
|
242
|
+
// Find a focusable element which may be the item, or inside the item to draw focus to.
|
243
|
+
// For example a Chip list in which .neo-list-items contain focusable Chips.
|
244
|
+
const focusTarget = [newActiveElement, ...newActiveElement.querySelectorAll('*')].find(DomUtils.isFocusable);
|
245
|
+
|
246
|
+
// If the item contains a focusable, we focus it and then react in navigateFocusInHandler
|
247
|
+
if (focusTarget) {
|
248
|
+
focusTarget.focus();
|
249
|
+
}
|
250
|
+
// If not, we programatically navigate there
|
251
|
+
else {
|
252
|
+
this.setActiveItem(newActiveElement, data);
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
setActiveItem(newActiveElement, data) {
|
257
|
+
const allItems = Array.from(data.subject.querySelectorAll(data.selector));
|
258
|
+
|
259
|
+
// Can navigate by index. This is useful if the active item is deleted.
|
260
|
+
// We can navigate to the same index and preserve UI stability.
|
261
|
+
if (typeof newActiveElement === 'number') {
|
262
|
+
newActiveElement = allItems[Math.max(Math.min(newActiveElement, allItems.length - 1), 0)];
|
263
|
+
}
|
264
|
+
|
265
|
+
data.previousActiveIndex = data.activeIndex;
|
266
|
+
(data.previousActiveItem = data.activeItem)?.classList.remove(data.activeCls);
|
267
|
+
(data.activeItem = newActiveElement)?.classList.add(data.activeCls);
|
268
|
+
data.activeIndex = newActiveElement ? allItems.indexOf(newActiveElement) : -1;
|
269
|
+
|
270
|
+
newActiveElement.scrollIntoView({
|
271
|
+
block : 'nearest',
|
272
|
+
inline : 'nearest',
|
273
|
+
nehavior : 'smooth'
|
274
|
+
})
|
275
|
+
|
276
|
+
DomEvents.sendMessageToApp({
|
277
|
+
type : 'neonavigate',
|
278
|
+
target : data.id,
|
279
|
+
path : [{
|
280
|
+
id : data.id
|
281
|
+
}],
|
282
|
+
activeItem : data.activeItem.id,
|
283
|
+
previousActiveItem : data.previousActiveItem?.id,
|
284
|
+
activeIndex : data.activeIndex,
|
285
|
+
previousActiveIndex : data.previousActiveIndex,
|
286
|
+
altKey : Neo.altKeyDown,
|
287
|
+
ctrlKey : Neo.controlKeyDown,
|
288
|
+
metaKey : Neo.metaKeyDown,
|
289
|
+
shiftKey : Neo.shiftKeyDown
|
290
|
+
})
|
291
|
+
}
|
292
|
+
|
293
|
+
navigateGetAdjacent(direction = 1, data) {
|
294
|
+
const { treeWalker } = data;
|
295
|
+
|
296
|
+
// Walk forwards or backwards to the next or previous node which matches our selector
|
297
|
+
treeWalker.currentNode = this.navigatorGetActiveItem(data) || data.subject;
|
298
|
+
treeWalker[direction < 0 ? 'previousNode' : 'nextNode']();
|
299
|
+
|
300
|
+
// Found a target in the requested direction
|
301
|
+
if (treeWalker.currentNode) {
|
302
|
+
if (treeWalker.currentNode !== data.activeItem) {
|
303
|
+
return treeWalker.currentNode;
|
304
|
+
}
|
305
|
+
}
|
306
|
+
// Could not find target in requested direction, then wrap if configured to do so
|
307
|
+
else if (data.wrap !== false) {
|
308
|
+
const allItems = data.subject.querySelector(data.selector);
|
309
|
+
|
310
|
+
return allItems[direction === 1 ? 0 : allItems.length - 1];
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
navigatorGetActiveItem(data) {
|
315
|
+
let activeItem = data.activeItem && DomAccess.getElement(data.activeItem.id);
|
316
|
+
|
317
|
+
if (!activeItem && ('activeIndex' in data)) {
|
318
|
+
const allItems = data.subject.querySelectorAll(data.selector);
|
319
|
+
|
320
|
+
activeItem = allItems[Math.max(Math.min(data.activeIndex, allItems.length - 1), 0)];
|
321
|
+
}
|
322
|
+
return activeItem;
|
323
|
+
}
|
324
|
+
|
325
|
+
navigateNodeFilter(node, data) {
|
326
|
+
return node.offsetParent && node.matches?.(data.selector) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
let instance = Neo.applyClassConfig(Navigator);
|
331
|
+
|
332
|
+
export default instance;
|
package/src/manager/DomEvent.mjs
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import NeoArray from '../util/Array.mjs';
|
1
2
|
import Model from './Model.mjs';
|
2
3
|
|
3
4
|
/**
|
@@ -15,120 +16,64 @@ class ListModel extends Model {
|
|
15
16
|
* @member {String} ntype='selection-listmodel'
|
16
17
|
* @protected
|
17
18
|
*/
|
18
|
-
ntype: 'selection-listmodel'
|
19
|
-
/**
|
20
|
-
* @member {Boolean} stayInList=true
|
21
|
-
*/
|
22
|
-
stayInList: true
|
23
|
-
}
|
24
|
-
|
25
|
-
/**
|
26
|
-
* @param {Object} data
|
27
|
-
*/
|
28
|
-
onKeyDownDown(data) {
|
29
|
-
!this.view.disableSelection && this.onNavKey(data, 1);
|
19
|
+
ntype: 'selection-listmodel'
|
30
20
|
}
|
31
21
|
|
32
22
|
/**
|
23
|
+
* Placeholder method to get overridden by class extension list menu.ListModel
|
33
24
|
* @param {Object} data
|
34
25
|
*/
|
35
|
-
|
36
|
-
let view = this.view;
|
37
|
-
|
38
|
-
!view.disableSelection && view.onKeyDownEnter?.(this.getSelection()[0]);
|
39
|
-
}
|
26
|
+
onKeyDownEscape(data) {}
|
40
27
|
|
41
28
|
/**
|
42
29
|
* Placeholder method to get overridden by class extension list menu.ListModel
|
43
30
|
* @param {Object} data
|
44
31
|
*/
|
45
|
-
|
32
|
+
onKeyDownDown(data) {}
|
46
33
|
|
47
34
|
/**
|
35
|
+
* Placeholder method to get overridden by class extension list menu.ListModel
|
48
36
|
* @param {Object} data
|
49
37
|
*/
|
50
|
-
|
51
|
-
this.onKeyDownUp(data);
|
52
|
-
}
|
38
|
+
onKeyDownEnter(data) {}
|
53
39
|
|
54
40
|
/**
|
41
|
+
* Placeholder method to get overridden by class extension list menu.ListModel
|
55
42
|
* @param {Object} data
|
56
43
|
*/
|
57
|
-
|
58
|
-
this.onKeyDownDown(data);
|
59
|
-
}
|
44
|
+
onKeyDownLeft(data) {}
|
60
45
|
|
61
46
|
/**
|
47
|
+
* Placeholder method to get overridden by class extension list menu.ListModel
|
62
48
|
* @param {Object} data
|
63
49
|
*/
|
64
|
-
|
65
|
-
!this.view.disableSelection && this.onNavKey(data, -1);
|
66
|
-
}
|
50
|
+
onKeyDownRight(data) {}
|
67
51
|
|
68
52
|
/**
|
53
|
+
* Placeholder method to get overridden by class extension list menu.ListModel
|
69
54
|
* @param {Object} data
|
70
|
-
* @param {Number} step
|
71
55
|
*/
|
72
|
-
|
73
|
-
let me = this,
|
74
|
-
view = me.view,
|
75
|
-
store = view.store,
|
76
|
-
maxItems = store.getCount(),
|
77
|
-
preventSelection = false,
|
78
|
-
index, item, itemId, node, record, recordId;
|
79
|
-
|
80
|
-
for (node of data.path) {
|
81
|
-
if (node.cls.includes(view.itemCls)) {
|
82
|
-
item = node.id;
|
83
|
-
break;
|
84
|
-
}
|
85
|
-
}
|
56
|
+
onKeyDownUp(data) {}
|
86
57
|
|
87
|
-
|
58
|
+
onListClick({ currentTarget }) {
|
59
|
+
const { view } = this;
|
88
60
|
|
89
|
-
if (
|
90
|
-
|
91
|
-
index = store.indexOf(recordId) + step;
|
92
|
-
record = store.getAt(index);
|
61
|
+
if (!view.disableSelection) {
|
62
|
+
const record = view.store.get(view.getItemRecordId(currentTarget));
|
93
63
|
|
94
|
-
|
95
|
-
|
96
|
-
record = store.getAt(index)
|
64
|
+
if (record) {
|
65
|
+
this.select(record);
|
97
66
|
}
|
98
|
-
|
99
|
-
if (index < 0) {
|
100
|
-
if (me.stayInList) {
|
101
|
-
index = maxItems - 1;
|
102
|
-
} else {
|
103
|
-
preventSelection = true;
|
104
|
-
me.deselectAll();
|
105
|
-
view.fire('selectPreFirstItem')
|
106
|
-
}
|
107
|
-
} else if (index >= maxItems) {
|
108
|
-
if (me.stayInList) {
|
109
|
-
index = 0;
|
110
|
-
|
111
|
-
while (store.getAt(index)?.isHeader === true) {
|
112
|
-
index++;
|
113
|
-
}
|
114
|
-
} else {
|
115
|
-
preventSelection = true;
|
116
|
-
me.deselectAll();
|
117
|
-
view.fire('selectPostLastItem')
|
118
|
-
}
|
119
|
-
}
|
120
|
-
} else {
|
121
|
-
index = 0
|
122
67
|
}
|
68
|
+
}
|
123
69
|
|
124
|
-
|
125
|
-
|
126
|
-
|
70
|
+
onListNavigate(data) {
|
71
|
+
const
|
72
|
+
{ view } = this,
|
73
|
+
{ store } = view;
|
127
74
|
|
128
|
-
|
129
|
-
|
130
|
-
view.fire('itemNavigate', record)
|
131
|
-
}
|
75
|
+
data.record = store.getAt(Math.min(data.activeIndex, store.getCount()));
|
76
|
+
view.fire('itemNavigate', data);
|
132
77
|
}
|
133
78
|
|
134
79
|
/**
|
@@ -141,6 +86,26 @@ class ListModel extends Model {
|
|
141
86
|
id = me.id,
|
142
87
|
view = me.view;
|
143
88
|
|
89
|
+
view.addDomListeners([{
|
90
|
+
click : me.onListClick,
|
91
|
+
|
92
|
+
// Should be `.${view.itemCls}:not(.neo-disabled,.neo-list-header)`
|
93
|
+
// TODO parse delegate selectors
|
94
|
+
delegate : path => {
|
95
|
+
for (let i = 0, { length } = path; i < length; i++) {
|
96
|
+
const { cls } = path[i];
|
97
|
+
|
98
|
+
if (cls.includes(view.itemCls) && !cls.includes('neo-disabled') && !cls.includes('neo-list-header')) {
|
99
|
+
return i;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
},
|
103
|
+
scope : me
|
104
|
+
}, {
|
105
|
+
neonavigate : me.onListNavigate,
|
106
|
+
scope : me
|
107
|
+
}])
|
108
|
+
|
144
109
|
view.keys?._keys.push(
|
145
110
|
{fn: 'onKeyDownDown' ,key: 'Down' ,scope: id},
|
146
111
|
{fn: 'onKeyDownEnter' ,key: 'Enter' ,scope: id},
|
@@ -161,7 +126,6 @@ class ListModel extends Model {
|
|
161
126
|
|
162
127
|
if (itemId) {
|
163
128
|
this.select(itemId);
|
164
|
-
view.focus(itemId)
|
165
129
|
}
|
166
130
|
}
|
167
131
|
|
package/src/selection/Model.mjs
CHANGED
@@ -94,20 +94,32 @@ class Model extends Base {
|
|
94
94
|
* @param {String} [selectedCls]
|
95
95
|
*/
|
96
96
|
deselect(item, silent, itemCollection=this.items, selectedCls) {
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
// We hold vdom ids for now, so all incoming selections must be converted.
|
98
|
+
item = item.isRecord ? view.getItemId(item) : Neo.isObject(item) ? item.id : item;
|
99
|
+
|
100
|
+
if (itemCollection.includes(item)) {
|
101
|
+
let me = this,
|
102
|
+
view = me.view,
|
103
|
+
node = view.getVdomChild(item);
|
104
|
+
|
105
|
+
if (node) {
|
106
|
+
node.cls = NeoArray.remove(node.cls || [], selectedCls || me.selectedCls);
|
107
|
+
node['aria-selected'] = false;
|
108
|
+
}
|
101
109
|
|
102
|
-
|
103
|
-
cls = node.cls || [];
|
104
|
-
NeoArray.remove(cls, selectedCls || me.selectedCls);
|
105
|
-
node.cls = cls;
|
106
|
-
}
|
110
|
+
NeoArray.remove(itemCollection, item);
|
107
111
|
|
108
|
-
|
112
|
+
if (!silent) {
|
113
|
+
view.update();
|
109
114
|
|
110
|
-
|
115
|
+
me.fire('selectionChange', {
|
116
|
+
selection : itemCollection
|
117
|
+
});
|
118
|
+
}
|
119
|
+
}
|
120
|
+
else if (!silent) {
|
121
|
+
this.fire('noChange');
|
122
|
+
}
|
111
123
|
}
|
112
124
|
|
113
125
|
/**
|
@@ -118,12 +130,21 @@ class Model extends Base {
|
|
118
130
|
items = [...me.items],
|
119
131
|
view = me.view;
|
120
132
|
|
121
|
-
items.
|
122
|
-
|
123
|
-
|
133
|
+
if (items.length) {
|
134
|
+
items.forEach(item => {
|
135
|
+
me.deselect(item, true);
|
136
|
+
});
|
137
|
+
|
138
|
+
if (!silent && items.length > 0) {
|
139
|
+
view.update();
|
140
|
+
}
|
124
141
|
|
125
|
-
|
126
|
-
|
142
|
+
me.fire('selectionChange', {
|
143
|
+
selection : this.items
|
144
|
+
});
|
145
|
+
}
|
146
|
+
else if (!silent) {
|
147
|
+
me.fire('noChange');
|
127
148
|
}
|
128
149
|
}
|
129
150
|
|
@@ -196,27 +217,24 @@ class Model extends Base {
|
|
196
217
|
* @param {String} [selectedCls]
|
197
218
|
*/
|
198
219
|
select(items, itemCollection=this.items, selectedCls) {
|
199
|
-
items = Array.isArray(items) ? items : [items];
|
200
|
-
|
201
220
|
let me = this,
|
202
221
|
view = me.view,
|
203
|
-
vdom = view.vdom
|
204
|
-
|
222
|
+
vdom = view.vdom;
|
223
|
+
|
224
|
+
// We hold vdom ids for now, so all incoming selections must be converted.
|
225
|
+
items = (items = Array.isArray(items) ? items : [items]).map(item => item.isRecord ? view.getItemId(item) : Neo.isObject(item) ? item.id : item)
|
205
226
|
|
206
227
|
if (!Neo.isEqual(itemCollection, items)) {
|
207
228
|
if (me.singleSelect) {
|
208
229
|
me.deselectAll(true);
|
209
230
|
}
|
210
231
|
|
211
|
-
items.forEach(node => {
|
212
|
-
|
213
|
-
|
214
|
-
}
|
215
|
-
|
232
|
+
items.forEach((node, i) => {
|
233
|
+
node = view.getVdomChild(node);
|
234
|
+
|
216
235
|
if (node) {
|
217
|
-
cls = node.cls || [];
|
218
|
-
|
219
|
-
node.cls = cls;
|
236
|
+
node.cls = NeoArray.add(node.cls || [], selectedCls || me.selectedCls);
|
237
|
+
node['aria-selected'] = true;
|
220
238
|
}
|
221
239
|
});
|
222
240
|
|
@@ -225,6 +243,13 @@ class Model extends Base {
|
|
225
243
|
view[view.silentSelect ? '_vdom' : 'vdom'] = vdom;
|
226
244
|
|
227
245
|
view.onSelect?.(items);
|
246
|
+
|
247
|
+
me.fire('selectionChange', {
|
248
|
+
selection : itemCollection
|
249
|
+
});
|
250
|
+
}
|
251
|
+
else {
|
252
|
+
me.fire('noChange');
|
228
253
|
}
|
229
254
|
}
|
230
255
|
|
@@ -232,12 +257,10 @@ class Model extends Base {
|
|
232
257
|
* @param {Object} item
|
233
258
|
*/
|
234
259
|
toggleSelection(item) {
|
235
|
-
|
236
|
-
|
237
|
-
if (me.isSelected(item)) {
|
238
|
-
me.deselect(item);
|
260
|
+
if (this.isSelected(item)) {
|
261
|
+
this.deselect(item);
|
239
262
|
} else {
|
240
|
-
|
263
|
+
this.select(item);
|
241
264
|
}
|
242
265
|
}
|
243
266
|
|