ts-spatial-navigation 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +362 -0
- package/README.md +284 -0
- package/dist/constants.d.ts +101 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +98 -0
- package/dist/constants.js.map +1 -0
- package/dist/core.d.ts +40 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +245 -0
- package/dist/core.js.map +1 -0
- package/dist/factory.d.ts +87 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +44 -0
- package/dist/factory.js.map +1 -0
- package/dist/geometry.d.ts +43 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +161 -0
- package/dist/geometry.js.map +1 -0
- package/dist/spatial-navigation.d.ts +84 -0
- package/dist/spatial-navigation.d.ts.map +1 -0
- package/dist/spatial-navigation.js +683 -0
- package/dist/spatial-navigation.js.map +1 -0
- package/dist/state.d.ts +82 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +179 -0
- package/dist/state.js.map +1 -0
- package/dist/strategies.d.ts +70 -0
- package/dist/strategies.d.ts.map +1 -0
- package/dist/strategies.js +147 -0
- package/dist/strategies.js.map +1 -0
- package/dist/types.d.ts +298 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
export { KeyCode, Grid, Defaults, EventName, RestrictMode, EnterTo } from './constants';
|
|
2
|
+
export { getRect, partition, distanceBuilder } from './geometry';
|
|
3
|
+
export { StateManager, createDefaultGlobalConfig, createInitialState } from './state';
|
|
4
|
+
// Strategy pattern exports
|
|
5
|
+
export { navigationStrategies, getStrategy, buildPrioritiesForDirection, leftStrategy, rightStrategy, upStrategy, downStrategy, } from './strategies';
|
|
6
|
+
import { dispatch, exclude, extend, getCurrentFocusedElement, matchSelector, navigate, parseSelector, setEventPrefix } from './core';
|
|
7
|
+
/************************/
|
|
8
|
+
/* Global Configuration */
|
|
9
|
+
/************************/
|
|
10
|
+
var globalConfig = {
|
|
11
|
+
selector: '',
|
|
12
|
+
straightOnly: false,
|
|
13
|
+
straightOverlapThreshold: 0.5,
|
|
14
|
+
rememberSource: false,
|
|
15
|
+
disabled: false,
|
|
16
|
+
defaultElement: '',
|
|
17
|
+
enterTo: '',
|
|
18
|
+
leaveFor: null,
|
|
19
|
+
// up: <extSelector>, down: <extSelector>}
|
|
20
|
+
restrict: 'self-first',
|
|
21
|
+
tabIndexIgnoreList: 'a, input, select, textarea, button, iframe, [contentEditable=true]',
|
|
22
|
+
navigableFilter: null,
|
|
23
|
+
sectionPrefix: 'section-',
|
|
24
|
+
eventPrefix: 'sn:'
|
|
25
|
+
};
|
|
26
|
+
/*********************/
|
|
27
|
+
/* Constant Variable */
|
|
28
|
+
/*********************/
|
|
29
|
+
const KEYMAPPING = {
|
|
30
|
+
37: 'left',
|
|
31
|
+
38: 'up',
|
|
32
|
+
39: 'right',
|
|
33
|
+
40: 'down'
|
|
34
|
+
};
|
|
35
|
+
const REVERSE = {
|
|
36
|
+
left: 'right',
|
|
37
|
+
up: 'down',
|
|
38
|
+
right: 'left',
|
|
39
|
+
down: 'up'
|
|
40
|
+
};
|
|
41
|
+
let ID_POOL_PREFIX = 'section-';
|
|
42
|
+
/********************/
|
|
43
|
+
/* Private Variable */
|
|
44
|
+
/********************/
|
|
45
|
+
let _idPool = 0;
|
|
46
|
+
let _ready = false;
|
|
47
|
+
let _pause = false;
|
|
48
|
+
let _sections = {};
|
|
49
|
+
let _sectionCount = 0;
|
|
50
|
+
let _defaultSectionId = '';
|
|
51
|
+
let _lastSectionId = '';
|
|
52
|
+
let _duringFocusChange = false;
|
|
53
|
+
/********************/
|
|
54
|
+
/* Private Function */
|
|
55
|
+
/********************/
|
|
56
|
+
const generateId = () => {
|
|
57
|
+
let id;
|
|
58
|
+
while (true) {
|
|
59
|
+
id = `${ID_POOL_PREFIX}${++_idPool}`;
|
|
60
|
+
if (!_sections[id]) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return id;
|
|
65
|
+
};
|
|
66
|
+
function isNavigable(elem, sectionId, verifySectionSelector) {
|
|
67
|
+
if (!elem || !sectionId || !_sections[sectionId] || _sections[sectionId].disabled) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if ((elem.offsetWidth <= 0 && elem.offsetHeight <= 0) || elem.hasAttribute('disabled')) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (verifySectionSelector && !matchSelector(elem, _sections[sectionId].selector)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (typeof _sections[sectionId].navigableFilter === 'function') {
|
|
77
|
+
if (_sections[sectionId].navigableFilter(elem, sectionId) === false) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (typeof globalConfig.navigableFilter === 'function') {
|
|
82
|
+
if (globalConfig.navigableFilter(elem, sectionId) === false) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function getSectionId(elem) {
|
|
89
|
+
for (const id in _sections) {
|
|
90
|
+
if (!_sections[id].disabled && matchSelector(elem, _sections[id].selector)) {
|
|
91
|
+
return id;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function getSectionNavigableElements(sectionId) {
|
|
96
|
+
return parseSelector(_sections[sectionId].selector).filter(function (elem) {
|
|
97
|
+
return isNavigable(elem, sectionId);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function getSectionDefaultElement(sectionId) {
|
|
101
|
+
const defaultElement = parseSelector(_sections[sectionId].defaultElement).find(function (elem) {
|
|
102
|
+
return isNavigable(elem, sectionId, true);
|
|
103
|
+
});
|
|
104
|
+
if (!defaultElement) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return defaultElement;
|
|
108
|
+
}
|
|
109
|
+
function getSectionLastFocusedElement(sectionId) {
|
|
110
|
+
var lastFocusedElement = _sections[sectionId].lastFocusedElement;
|
|
111
|
+
if (!isNavigable(lastFocusedElement, sectionId, true)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return lastFocusedElement;
|
|
115
|
+
}
|
|
116
|
+
function focusElement(elem, sectionId, direction) {
|
|
117
|
+
if (!elem) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
var currentFocusedElement = getCurrentFocusedElement();
|
|
121
|
+
var silentFocus = function () {
|
|
122
|
+
if (currentFocusedElement) {
|
|
123
|
+
currentFocusedElement.blur();
|
|
124
|
+
}
|
|
125
|
+
elem.focus();
|
|
126
|
+
focusChanged(elem, sectionId);
|
|
127
|
+
};
|
|
128
|
+
if (_duringFocusChange) {
|
|
129
|
+
silentFocus();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
_duringFocusChange = true;
|
|
133
|
+
if (_pause) {
|
|
134
|
+
silentFocus();
|
|
135
|
+
_duringFocusChange = false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (currentFocusedElement) {
|
|
139
|
+
const willUnfocusDetail = {
|
|
140
|
+
nextElement: elem,
|
|
141
|
+
nextId: sectionId,
|
|
142
|
+
direction: direction,
|
|
143
|
+
native: false
|
|
144
|
+
};
|
|
145
|
+
if (!dispatch(currentFocusedElement, 'willunfocus', willUnfocusDetail)) {
|
|
146
|
+
_duringFocusChange = false;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
currentFocusedElement.blur();
|
|
150
|
+
dispatch(currentFocusedElement, 'unfocused', willUnfocusDetail, false);
|
|
151
|
+
}
|
|
152
|
+
const focusDetail = {
|
|
153
|
+
previousElement: currentFocusedElement,
|
|
154
|
+
id: sectionId,
|
|
155
|
+
direction: direction,
|
|
156
|
+
native: false
|
|
157
|
+
};
|
|
158
|
+
if (!dispatch(elem, 'willfocus', focusDetail)) {
|
|
159
|
+
_duringFocusChange = false;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
elem.focus();
|
|
163
|
+
dispatch(elem, 'focused', focusDetail, false);
|
|
164
|
+
_duringFocusChange = false;
|
|
165
|
+
focusChanged(elem, sectionId);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
function focusChanged(elem, sectionId) {
|
|
169
|
+
if (!sectionId) {
|
|
170
|
+
sectionId = getSectionId(elem);
|
|
171
|
+
}
|
|
172
|
+
if (sectionId) {
|
|
173
|
+
_sections[sectionId].lastFocusedElement = elem;
|
|
174
|
+
_lastSectionId = sectionId;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const focusExtendedSelector = (selector, direction) => {
|
|
178
|
+
if (selector.charAt(0) == '@') {
|
|
179
|
+
if (selector.length == 1) {
|
|
180
|
+
return focusSection();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return focusSection(selector.substr(1));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
var next = parseSelector(selector)[0];
|
|
188
|
+
if (next) {
|
|
189
|
+
var nextSectionId = getSectionId(next);
|
|
190
|
+
if (isNavigable(next, nextSectionId)) {
|
|
191
|
+
return focusElement(next, nextSectionId, direction);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
function focusSection(sectionId) {
|
|
198
|
+
const range = [];
|
|
199
|
+
const addRange = function (id) {
|
|
200
|
+
if (id && range.indexOf(id) < 0 && _sections[id] && !_sections[id].disabled) {
|
|
201
|
+
range.push(id);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
if (sectionId) {
|
|
205
|
+
addRange(sectionId);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
addRange(_defaultSectionId);
|
|
209
|
+
addRange(_lastSectionId);
|
|
210
|
+
Object.keys(_sections).map(addRange);
|
|
211
|
+
}
|
|
212
|
+
range.forEach((id) => {
|
|
213
|
+
let next;
|
|
214
|
+
if (_sections[id].enterTo == 'last-focused') {
|
|
215
|
+
next =
|
|
216
|
+
getSectionLastFocusedElement(id) ||
|
|
217
|
+
getSectionDefaultElement(id) ||
|
|
218
|
+
getSectionNavigableElements(id)[0];
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
next =
|
|
222
|
+
getSectionDefaultElement(id) ||
|
|
223
|
+
getSectionLastFocusedElement(id) ||
|
|
224
|
+
getSectionNavigableElements(id)[0];
|
|
225
|
+
}
|
|
226
|
+
if (next) {
|
|
227
|
+
return focusElement(next, id);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function fireNavigateFailed(elem, direction) {
|
|
233
|
+
dispatch(elem, 'navigatefailed', { direction }, false);
|
|
234
|
+
}
|
|
235
|
+
function gotoLeaveFor(sectionId, direction) {
|
|
236
|
+
const leaveFor = _sections[sectionId]?.leaveFor?.[direction];
|
|
237
|
+
if (leaveFor) {
|
|
238
|
+
if (typeof leaveFor === 'string') {
|
|
239
|
+
if (leaveFor === '') {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return focusExtendedSelector(leaveFor, direction);
|
|
243
|
+
}
|
|
244
|
+
const nextSectionId = getSectionId(leaveFor);
|
|
245
|
+
if (isNavigable(leaveFor, nextSectionId)) {
|
|
246
|
+
return focusElement(leaveFor, nextSectionId, direction);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function focusNext(direction, currentFocusedElement, currentSectionId) {
|
|
252
|
+
const extSelector = currentFocusedElement.getAttribute('data-sn-' + direction);
|
|
253
|
+
if (typeof extSelector === 'string') {
|
|
254
|
+
if (extSelector === '' || !focusExtendedSelector(extSelector, direction)) {
|
|
255
|
+
fireNavigateFailed(currentFocusedElement, direction);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const sectionNavigableElements = {};
|
|
261
|
+
let allNavigableElements = [];
|
|
262
|
+
for (const id in _sections) {
|
|
263
|
+
sectionNavigableElements[id] = getSectionNavigableElements(id);
|
|
264
|
+
allNavigableElements = allNavigableElements.concat(sectionNavigableElements[id]);
|
|
265
|
+
}
|
|
266
|
+
const config = extend({}, globalConfig, _sections[currentSectionId]);
|
|
267
|
+
let next;
|
|
268
|
+
if (config.restrict == 'self-only' || config.restrict == 'self-first') {
|
|
269
|
+
const currentSectionNavigableElements = sectionNavigableElements[currentSectionId];
|
|
270
|
+
next = navigate(currentFocusedElement, direction, exclude(currentSectionNavigableElements, currentFocusedElement), config);
|
|
271
|
+
if (!next && config.restrict == 'self-first') {
|
|
272
|
+
next = navigate(currentFocusedElement, direction, exclude(allNavigableElements, currentSectionNavigableElements), config);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
next = navigate(currentFocusedElement, direction, exclude(allNavigableElements, currentFocusedElement), config);
|
|
277
|
+
}
|
|
278
|
+
if (next) {
|
|
279
|
+
_sections[currentSectionId].previous = {
|
|
280
|
+
target: currentFocusedElement,
|
|
281
|
+
destination: next,
|
|
282
|
+
reverse: REVERSE[direction]
|
|
283
|
+
};
|
|
284
|
+
const nextSectionId = getSectionId(next);
|
|
285
|
+
if (currentSectionId != nextSectionId) {
|
|
286
|
+
var result = gotoLeaveFor(currentSectionId, direction);
|
|
287
|
+
if (result) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
else if (result === null) {
|
|
291
|
+
fireNavigateFailed(currentFocusedElement, direction);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
var enterToElement;
|
|
295
|
+
switch (_sections[nextSectionId].enterTo) {
|
|
296
|
+
case 'last-focused':
|
|
297
|
+
enterToElement =
|
|
298
|
+
getSectionLastFocusedElement(nextSectionId) || getSectionDefaultElement(nextSectionId);
|
|
299
|
+
break;
|
|
300
|
+
case 'default-element':
|
|
301
|
+
enterToElement = getSectionDefaultElement(nextSectionId);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (enterToElement) {
|
|
305
|
+
next = enterToElement;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return focusElement(next, nextSectionId, direction);
|
|
309
|
+
}
|
|
310
|
+
else if (gotoLeaveFor(currentSectionId, direction)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
fireNavigateFailed(currentFocusedElement, direction);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
function onKeyDown(evt) {
|
|
317
|
+
if (!_sectionCount || _pause || evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const preventDefault = function () {
|
|
321
|
+
evt.preventDefault();
|
|
322
|
+
evt.stopPropagation();
|
|
323
|
+
return false;
|
|
324
|
+
};
|
|
325
|
+
let currentFocusedElement;
|
|
326
|
+
const direction = KEYMAPPING[evt.keyCode];
|
|
327
|
+
if (!direction) {
|
|
328
|
+
if (evt.keyCode == 13) {
|
|
329
|
+
currentFocusedElement = getCurrentFocusedElement();
|
|
330
|
+
if (currentFocusedElement && getSectionId(currentFocusedElement)) {
|
|
331
|
+
if (!dispatch(currentFocusedElement, 'enter-down')) {
|
|
332
|
+
return preventDefault();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
currentFocusedElement = getCurrentFocusedElement();
|
|
339
|
+
if (!currentFocusedElement) {
|
|
340
|
+
if (_lastSectionId) {
|
|
341
|
+
currentFocusedElement = getSectionLastFocusedElement(_lastSectionId);
|
|
342
|
+
}
|
|
343
|
+
if (!currentFocusedElement) {
|
|
344
|
+
focusSection();
|
|
345
|
+
return preventDefault();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const currentSectionId = getSectionId(currentFocusedElement);
|
|
349
|
+
if (!currentSectionId) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const willMoveDetail = {
|
|
353
|
+
direction: direction,
|
|
354
|
+
id: currentSectionId,
|
|
355
|
+
cause: 'keydown'
|
|
356
|
+
};
|
|
357
|
+
if (dispatch(currentFocusedElement, 'willmove', willMoveDetail)) {
|
|
358
|
+
focusNext(direction, currentFocusedElement, currentSectionId);
|
|
359
|
+
}
|
|
360
|
+
return preventDefault();
|
|
361
|
+
}
|
|
362
|
+
function onKeyUp(evt) {
|
|
363
|
+
if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!_pause && _sectionCount && evt.keyCode == 13) {
|
|
367
|
+
const currentFocusedElement = getCurrentFocusedElement();
|
|
368
|
+
if (currentFocusedElement && getSectionId(currentFocusedElement)) {
|
|
369
|
+
if (!dispatch(currentFocusedElement, 'enter-up')) {
|
|
370
|
+
evt.preventDefault();
|
|
371
|
+
evt.stopPropagation();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function onFocus(evt) {
|
|
377
|
+
const target = evt.target;
|
|
378
|
+
if (target !== window &&
|
|
379
|
+
target !== document &&
|
|
380
|
+
_sectionCount &&
|
|
381
|
+
!_duringFocusChange) {
|
|
382
|
+
const sectionId = getSectionId(target);
|
|
383
|
+
if (sectionId) {
|
|
384
|
+
if (_pause) {
|
|
385
|
+
focusChanged(target, sectionId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const focusProperties = {
|
|
389
|
+
sectionId: sectionId,
|
|
390
|
+
native: true
|
|
391
|
+
};
|
|
392
|
+
if (!dispatch(target, 'willfocus', focusProperties)) {
|
|
393
|
+
_duringFocusChange = true;
|
|
394
|
+
target.blur();
|
|
395
|
+
_duringFocusChange = false;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
dispatch(target, 'focused', focusProperties, false);
|
|
399
|
+
focusChanged(target, sectionId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function onBlur(evt) {
|
|
405
|
+
const target = evt.target;
|
|
406
|
+
/**
|
|
407
|
+
* Filter out blur events from window/document objects.
|
|
408
|
+
* Although window and document are not focusable elements, blur events
|
|
409
|
+
* can bubble up to them. We only want to handle blur from actual HTML elements.
|
|
410
|
+
*/
|
|
411
|
+
if (target !== window &&
|
|
412
|
+
target !== document &&
|
|
413
|
+
!_pause &&
|
|
414
|
+
_sectionCount &&
|
|
415
|
+
!_duringFocusChange &&
|
|
416
|
+
getSectionId(target)) {
|
|
417
|
+
const unfocusProperties = {
|
|
418
|
+
native: true
|
|
419
|
+
};
|
|
420
|
+
if (!dispatch(target, 'willunfocus', unfocusProperties)) {
|
|
421
|
+
_duringFocusChange = true;
|
|
422
|
+
setTimeout(function () {
|
|
423
|
+
target.focus();
|
|
424
|
+
_duringFocusChange = false;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
dispatch(target, 'unfocused', unfocusProperties, false);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/*******************/
|
|
433
|
+
/* Public Function */
|
|
434
|
+
/*******************/
|
|
435
|
+
const SpatialNavigation = {
|
|
436
|
+
/**
|
|
437
|
+
* Initializes SpatialNavigation and binds event listeners to the global object. It is a synchronous function, so you don't need to await ready state. Calling init() more than once is possible since SpatialNavigation internally prevents it from reiterating the initialization.
|
|
438
|
+
*
|
|
439
|
+
* Note: It should be called before using any other methods of SpatialNavigation!
|
|
440
|
+
*/
|
|
441
|
+
init: () => {
|
|
442
|
+
if (!_ready) {
|
|
443
|
+
window.addEventListener('keydown', onKeyDown);
|
|
444
|
+
window.addEventListener('keyup', onKeyUp);
|
|
445
|
+
window.addEventListener('focus', onFocus, true);
|
|
446
|
+
window.addEventListener('blur', onBlur, true);
|
|
447
|
+
_ready = true;
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
/**
|
|
451
|
+
* Uninitializes SpatialNavigation, resets the variable state and unbinds the event listeners.
|
|
452
|
+
*/
|
|
453
|
+
uninit: () => {
|
|
454
|
+
window.removeEventListener('blur', onBlur, true);
|
|
455
|
+
window.removeEventListener('focus', onFocus, true);
|
|
456
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
457
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
458
|
+
SpatialNavigation.clear();
|
|
459
|
+
_idPool = 0;
|
|
460
|
+
_ready = false;
|
|
461
|
+
},
|
|
462
|
+
/**
|
|
463
|
+
* Resets the variable state without unbinding the event listeners.
|
|
464
|
+
*/
|
|
465
|
+
clear: () => {
|
|
466
|
+
_sections = {};
|
|
467
|
+
_sectionCount = 0;
|
|
468
|
+
_defaultSectionId = '';
|
|
469
|
+
_lastSectionId = '';
|
|
470
|
+
_duringFocusChange = false;
|
|
471
|
+
},
|
|
472
|
+
/**
|
|
473
|
+
* Updates the config of the section with the specified `id`. If `id` is omitted, the global configuration will be updated.
|
|
474
|
+
*
|
|
475
|
+
* Omitted properties in config will not affect the original one, which was set by `add()`, so only properties that you want to update need to be listed. In other words, if you want to delete any previously added properties, you have to explicitly assign undefined to those properties in the config.
|
|
476
|
+
*/
|
|
477
|
+
set: (config) => {
|
|
478
|
+
// const { id } = config;
|
|
479
|
+
for (const [key, value] of Object.entries(config)) {
|
|
480
|
+
if (globalConfig[key] !== undefined) {
|
|
481
|
+
if (config?.id) {
|
|
482
|
+
_sections[config.id][key] = value;
|
|
483
|
+
}
|
|
484
|
+
else if (value !== undefined) {
|
|
485
|
+
globalConfig[key] = value;
|
|
486
|
+
// Apply global prefix changes
|
|
487
|
+
if (key === 'sectionPrefix' && typeof value === 'string') {
|
|
488
|
+
ID_POOL_PREFIX = value;
|
|
489
|
+
}
|
|
490
|
+
else if (key === 'eventPrefix' && typeof value === 'string') {
|
|
491
|
+
setEventPrefix(value);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (config?.id) {
|
|
497
|
+
// remove "undefined" items
|
|
498
|
+
_sections[config.id] = extend({}, _sections[config.id]);
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
/**
|
|
502
|
+
*
|
|
503
|
+
* Adds a section to SpatialNavigation with its own configuration. The config doesn't have to contain all the properties. Those omitted will inherit global ones automatically.
|
|
504
|
+
*
|
|
505
|
+
* A section is a conceptual scope to define a set of elements no matter where they are in DOM structure. You can group elements based on their functions or behaviors (e.g. main, menu, dialog, etc.) into a section.
|
|
506
|
+
*/
|
|
507
|
+
add: (config) => {
|
|
508
|
+
if (!config?.id)
|
|
509
|
+
config.id = generateId();
|
|
510
|
+
const { id } = config;
|
|
511
|
+
if (_sections[id]) {
|
|
512
|
+
throw new Error('Section "' + id + '" has already existed!');
|
|
513
|
+
}
|
|
514
|
+
_sections[id] = {};
|
|
515
|
+
_sectionCount++;
|
|
516
|
+
SpatialNavigation.set(config);
|
|
517
|
+
return id;
|
|
518
|
+
},
|
|
519
|
+
/**
|
|
520
|
+
* Removes the section with the specified `id` from SpatialNavigation.
|
|
521
|
+
*
|
|
522
|
+
* Elements defined in this section will not be navigated anymore.
|
|
523
|
+
*/
|
|
524
|
+
remove: (id) => {
|
|
525
|
+
if (!id || typeof id !== 'string') {
|
|
526
|
+
throw new Error('Please assign the "id"!');
|
|
527
|
+
}
|
|
528
|
+
if (_sections[id]) {
|
|
529
|
+
_sections[id] = undefined;
|
|
530
|
+
_sections = extend({}, _sections);
|
|
531
|
+
_sectionCount--;
|
|
532
|
+
if (_lastSectionId === id) {
|
|
533
|
+
_lastSectionId = '';
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
},
|
|
539
|
+
/**
|
|
540
|
+
*
|
|
541
|
+
* Disables the section with the specified `id` temporarily. Elements defined in this section will become unnavigable until enable() is called.
|
|
542
|
+
*/
|
|
543
|
+
disable: function (id) {
|
|
544
|
+
if (_sections[id]) {
|
|
545
|
+
_sections[id].disabled = true;
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
},
|
|
550
|
+
/**
|
|
551
|
+
* Enables the section with the specified `id`.
|
|
552
|
+
*
|
|
553
|
+
* Elements defined in this section, on which if `disable()` was called earlier, will become navigable again.
|
|
554
|
+
*/
|
|
555
|
+
enable: function (id) {
|
|
556
|
+
if (_sections[id]) {
|
|
557
|
+
_sections[id].disabled = false;
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
},
|
|
562
|
+
pause: function () {
|
|
563
|
+
_pause = true;
|
|
564
|
+
},
|
|
565
|
+
resume: function () {
|
|
566
|
+
_pause = false;
|
|
567
|
+
},
|
|
568
|
+
/**
|
|
569
|
+
* Focuses the section with the specified `id` or the first element that matches selector.
|
|
570
|
+
*
|
|
571
|
+
* If the first argument matches any of the existing `id`, it will be regarded as a `id`. Otherwise, it will be treated as selector instead. If omitted, the default section, which is set by `setDefaultSection()`, will be the substitution.
|
|
572
|
+
*/
|
|
573
|
+
focus: (elem, silent) => {
|
|
574
|
+
let result = false;
|
|
575
|
+
if (silent === undefined && typeof elem === 'boolean') {
|
|
576
|
+
silent = elem;
|
|
577
|
+
elem = undefined;
|
|
578
|
+
}
|
|
579
|
+
let autoPause = !_pause && silent;
|
|
580
|
+
if (autoPause) {
|
|
581
|
+
SpatialNavigation.pause();
|
|
582
|
+
}
|
|
583
|
+
if (!elem) {
|
|
584
|
+
result = focusSection();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
if (typeof elem === 'string') {
|
|
588
|
+
if (_sections[elem]) {
|
|
589
|
+
result = focusSection(elem);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
result = focusExtendedSelector(elem);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
const nextSectionId = getSectionId(elem);
|
|
597
|
+
if (isNavigable(elem, nextSectionId)) {
|
|
598
|
+
result = focusElement(elem, nextSectionId);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (autoPause) {
|
|
603
|
+
SpatialNavigation.resume();
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
},
|
|
607
|
+
/**
|
|
608
|
+
* Moves the focus to the given direction based on the rule of SpatialNavigation. The first element matching selector is regarded as the origin. If selector is omitted, SpatialNavigation will move the focus based on the currently focused element.
|
|
609
|
+
*/
|
|
610
|
+
move: (direction, selector) => {
|
|
611
|
+
if (!REVERSE[direction]) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
const elem = selector ? parseSelector(selector)[0] : getCurrentFocusedElement();
|
|
615
|
+
if (!elem) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const sectionId = getSectionId(elem);
|
|
619
|
+
if (!sectionId) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (!dispatch(elem, 'willmove', {
|
|
623
|
+
direction: direction,
|
|
624
|
+
id: sectionId,
|
|
625
|
+
cause: 'api'
|
|
626
|
+
})) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
return focusNext(direction, elem, sectionId);
|
|
630
|
+
},
|
|
631
|
+
/**
|
|
632
|
+
* A helper to add `tabindex="-1"` to elements defined in the specified section to make them focusable. If `id` is omitted, it applies to all sections.
|
|
633
|
+
*
|
|
634
|
+
* **Note:** It won't affect elements which have been focusable already or have not been appended to DOM tree yet.
|
|
635
|
+
*/
|
|
636
|
+
makeFocusable: (id) => {
|
|
637
|
+
const doMakeFocusable = function (section) {
|
|
638
|
+
const tabIndexIgnoreList = section.tabIndexIgnoreList !== undefined
|
|
639
|
+
? section.tabIndexIgnoreList
|
|
640
|
+
: globalConfig.tabIndexIgnoreList;
|
|
641
|
+
if (section.selector) {
|
|
642
|
+
parseSelector(section.selector).forEach(function (elem) {
|
|
643
|
+
if (!matchSelector(elem, tabIndexIgnoreList)) {
|
|
644
|
+
if (!elem.getAttribute('tabindex')) {
|
|
645
|
+
elem.setAttribute('tabindex', '-1');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
if (id) {
|
|
652
|
+
if (_sections[id]) {
|
|
653
|
+
doMakeFocusable(_sections[id]);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
throw new Error('Section "' + id + '" doesn\'t exist!');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
for (const _id in _sections) {
|
|
661
|
+
doMakeFocusable(_sections[_id]);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
/**
|
|
666
|
+
* Assigns the specified section to be the default section. It will be used as a substitution in certain methods, of which if sectionId is omitted.
|
|
667
|
+
*
|
|
668
|
+
* Calling this method without the argument can reset the default section to undefined.
|
|
669
|
+
*/
|
|
670
|
+
setDefaultSection: (id) => {
|
|
671
|
+
if (!id) {
|
|
672
|
+
_defaultSectionId = '';
|
|
673
|
+
}
|
|
674
|
+
else if (!_sections[id]) {
|
|
675
|
+
throw new Error('Section "' + id + '" doesn\'t exist!');
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
_defaultSectionId = id;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
export default SpatialNavigation;
|
|
683
|
+
//# sourceMappingURL=spatial-navigation.js.map
|