tizenvtv-mods 1.0.0
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/package.json +19 -0
- package/rollup.config.js +31 -0
- package/spatial-navigation-polyfill.js +1756 -0
- package/userScript.js +64 -0
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
//
|
|
2
|
+
// https://raw.githubusercontent.com/WICG/spatial-navigation/183f0146b6741007e46fa64ab0950447defdf8af/polyfill/spatial-navigation-polyfill.js
|
|
3
|
+
// License: MIT
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
/* Spatial Navigation Polyfill
|
|
7
|
+
*
|
|
8
|
+
* It follows W3C official specification
|
|
9
|
+
* https://drafts.csswg.org/css-nav-1/
|
|
10
|
+
*
|
|
11
|
+
* Copyright (c) 2018-2019 LG Electronics Inc.
|
|
12
|
+
* https://github.com/WICG/spatial-navigation/polyfill
|
|
13
|
+
*
|
|
14
|
+
* Licensed under the MIT license (MIT)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
(function () {
|
|
18
|
+
|
|
19
|
+
// The polyfill must not be executed, if it's already enabled via browser engine or browser extensions.
|
|
20
|
+
if ('navigate' in window) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ARROW_KEY_CODE = {37: 'left', 38: 'up', 39: 'right', 40: 'down'};
|
|
25
|
+
const TAB_KEY_CODE = 9;
|
|
26
|
+
let mapOfBoundRect = null;
|
|
27
|
+
let startingPoint = null; // Saves spatial navigation starting point
|
|
28
|
+
let savedSearchOrigin = {element: null, rect: null}; // Saves previous search origin
|
|
29
|
+
let searchOriginRect = null; // Rect of current search origin
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initiate the spatial navigation features of the polyfill.
|
|
33
|
+
* @function initiateSpatialNavigation
|
|
34
|
+
*/
|
|
35
|
+
function initiateSpatialNavigation() {
|
|
36
|
+
/*
|
|
37
|
+
* Bind the standards APIs to be exposed to the window object for authors
|
|
38
|
+
*/
|
|
39
|
+
window.navigate = navigate;
|
|
40
|
+
window.Element.prototype.spatialNavigationSearch = spatialNavigationSearch;
|
|
41
|
+
window.Element.prototype.focusableAreas = focusableAreas;
|
|
42
|
+
window.Element.prototype.getSpatialNavigationContainer = getSpatialNavigationContainer;
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* CSS.registerProperty() from the Properties and Values API
|
|
46
|
+
* Reference: https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function
|
|
47
|
+
*/
|
|
48
|
+
if (window.CSS && CSS.registerProperty) {
|
|
49
|
+
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-contain') === '') {
|
|
50
|
+
CSS.registerProperty({
|
|
51
|
+
name: '--spatial-navigation-contain',
|
|
52
|
+
syntax: 'auto | contain',
|
|
53
|
+
inherits: false,
|
|
54
|
+
initialValue: 'auto'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-action') === '') {
|
|
59
|
+
CSS.registerProperty({
|
|
60
|
+
name: '--spatial-navigation-action',
|
|
61
|
+
syntax: 'auto | focus | scroll',
|
|
62
|
+
inherits: false,
|
|
63
|
+
initialValue: 'auto'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-function') === '') {
|
|
68
|
+
CSS.registerProperty({
|
|
69
|
+
name: '--spatial-navigation-function',
|
|
70
|
+
syntax: 'normal | grid',
|
|
71
|
+
inherits: false,
|
|
72
|
+
initialValue: 'normal'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add event handlers for the spatial navigation behavior.
|
|
80
|
+
* This function defines which input methods trigger the spatial navigation behavior.
|
|
81
|
+
* @function spatialNavigationHandler
|
|
82
|
+
*/
|
|
83
|
+
function spatialNavigationHandler() {
|
|
84
|
+
/*
|
|
85
|
+
* keydown EventListener :
|
|
86
|
+
* If arrow key pressed, get the next focusing element and send it to focusing controller
|
|
87
|
+
*/
|
|
88
|
+
window.addEventListener('keydown', (e) => {
|
|
89
|
+
const currentKeyMode = (parent && parent.__spatialNavigation__.keyMode) || window.__spatialNavigation__.keyMode;
|
|
90
|
+
const eventTarget = document.activeElement;
|
|
91
|
+
const dir = ARROW_KEY_CODE[e.keyCode];
|
|
92
|
+
|
|
93
|
+
if (e.keyCode === TAB_KEY_CODE) {
|
|
94
|
+
startingPoint = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!currentKeyMode ||
|
|
98
|
+
(currentKeyMode === 'NONE') ||
|
|
99
|
+
((currentKeyMode === 'SHIFTARROW') && !e.shiftKey) ||
|
|
100
|
+
((currentKeyMode === 'ARROW') && e.shiftKey))
|
|
101
|
+
return;
|
|
102
|
+
|
|
103
|
+
if (!e.defaultPrevented) {
|
|
104
|
+
let focusNavigableArrowKey = {left: true, up: true, right: true, down: true};
|
|
105
|
+
|
|
106
|
+
// Edge case (text input, area) : Don't move focus, just navigate cursor in text area
|
|
107
|
+
if ((eventTarget.nodeName === 'INPUT') || eventTarget.nodeName === 'TEXTAREA') {
|
|
108
|
+
focusNavigableArrowKey = handlingEditableElement(e);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (focusNavigableArrowKey[dir]) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
mapOfBoundRect = new Map();
|
|
114
|
+
|
|
115
|
+
navigate(dir);
|
|
116
|
+
|
|
117
|
+
mapOfBoundRect = null;
|
|
118
|
+
startingPoint = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* mouseup EventListener :
|
|
125
|
+
* If the mouse click a point in the page, the point will be the starting point.
|
|
126
|
+
* NOTE: Let UA set the spatial navigation starting point based on click
|
|
127
|
+
*/
|
|
128
|
+
document.addEventListener('mouseup', (e) => {
|
|
129
|
+
startingPoint = {x: e.clientX, y: e.clientY};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* focusin EventListener :
|
|
134
|
+
* When the element get the focus, save it and its DOMRect for resetting the search origin
|
|
135
|
+
* if it disappears.
|
|
136
|
+
*/
|
|
137
|
+
window.addEventListener('focusin', (e) => {
|
|
138
|
+
if (e.target !== window) {
|
|
139
|
+
savedSearchOrigin.element = e.target;
|
|
140
|
+
savedSearchOrigin.rect = e.target.getBoundingClientRect();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Enable the author to trigger spatial navigation programmatically, as if the user had done so manually.
|
|
147
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-window-navigate}
|
|
148
|
+
* @function navigate
|
|
149
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
150
|
+
*/
|
|
151
|
+
function navigate(dir) {
|
|
152
|
+
// spatial navigation steps
|
|
153
|
+
|
|
154
|
+
// 1
|
|
155
|
+
const searchOrigin = findSearchOrigin();
|
|
156
|
+
let eventTarget = searchOrigin;
|
|
157
|
+
|
|
158
|
+
let elementFromPosition = null;
|
|
159
|
+
|
|
160
|
+
// 2 Optional step, UA defined starting point
|
|
161
|
+
if (startingPoint) {
|
|
162
|
+
// if there is a starting point, set eventTarget as the element from position for getting the spatnav container
|
|
163
|
+
elementFromPosition = document.elementFromPoint(startingPoint.x, startingPoint.y);
|
|
164
|
+
|
|
165
|
+
// Use starting point if the starting point isn't inside the focusable element (but not container)
|
|
166
|
+
// * Starting point is meaningfull when:
|
|
167
|
+
// 1) starting point is inside the spatnav container
|
|
168
|
+
// 2) starting point is inside the non-focusable element
|
|
169
|
+
if (elementFromPosition === null) {
|
|
170
|
+
elementFromPosition = document.body;
|
|
171
|
+
}
|
|
172
|
+
if (isFocusable(elementFromPosition) && !isContainer(elementFromPosition)) {
|
|
173
|
+
startingPoint = null;
|
|
174
|
+
} else if (isContainer(elementFromPosition)) {
|
|
175
|
+
eventTarget = elementFromPosition;
|
|
176
|
+
} else {
|
|
177
|
+
eventTarget = elementFromPosition.getSpatialNavigationContainer();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4
|
|
182
|
+
if (eventTarget === document || eventTarget === document.documentElement) {
|
|
183
|
+
eventTarget = document.body || document.documentElement;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5
|
|
187
|
+
// At this point, spatialNavigationSearch can be applied.
|
|
188
|
+
// If startingPoint is either a scroll container or the document,
|
|
189
|
+
// find the best candidate within startingPoint
|
|
190
|
+
let container = null;
|
|
191
|
+
if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) {
|
|
192
|
+
if (eventTarget.nodeName === 'IFRAME') {
|
|
193
|
+
eventTarget = eventTarget.contentDocument.documentElement;
|
|
194
|
+
}
|
|
195
|
+
container = eventTarget;
|
|
196
|
+
let bestInsideCandidate = null;
|
|
197
|
+
|
|
198
|
+
// 5-2
|
|
199
|
+
if ((document.activeElement === searchOrigin) ||
|
|
200
|
+
(document.activeElement === document.body) && (searchOrigin === document.documentElement)) {
|
|
201
|
+
if (getCSSSpatNavAction(eventTarget) === 'scroll') {
|
|
202
|
+
if (scrollingController(eventTarget, dir)) return;
|
|
203
|
+
} else if (getCSSSpatNavAction(eventTarget) === 'focus') {
|
|
204
|
+
bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, {container: eventTarget, candidates: getSpatialNavigationCandidates(eventTarget, {mode: 'all'})});
|
|
205
|
+
if (focusingController(bestInsideCandidate, dir)) return;
|
|
206
|
+
} else if (getCSSSpatNavAction(eventTarget) === 'auto') {
|
|
207
|
+
bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, {container: eventTarget});
|
|
208
|
+
if (focusingController(bestInsideCandidate, dir) || scrollingController(eventTarget, dir)) return;
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// when the previous search origin became offscreen
|
|
212
|
+
container = container.getSpatialNavigationContainer();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 6
|
|
217
|
+
// Let container be the nearest ancestor of eventTarget
|
|
218
|
+
container = eventTarget.getSpatialNavigationContainer();
|
|
219
|
+
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
|
|
220
|
+
|
|
221
|
+
// When the container is the viewport of a browsing context
|
|
222
|
+
if (!parentContainer && ( window.location !== window.parent.location)) {
|
|
223
|
+
parentContainer = window.parent.document.documentElement;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (getCSSSpatNavAction(container) === 'scroll') {
|
|
227
|
+
if (scrollingController(container, dir)) return;
|
|
228
|
+
} else if (getCSSSpatNavAction(container) === 'focus') {
|
|
229
|
+
navigateChain(eventTarget, container, parentContainer, dir, 'all');
|
|
230
|
+
} else if (getCSSSpatNavAction(container) === 'auto') {
|
|
231
|
+
navigateChain(eventTarget, container, parentContainer, dir, 'visible');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Move the focus to the best candidate or do nothing.
|
|
237
|
+
* @function focusingController
|
|
238
|
+
* @param bestCandidate {Node} - The best candidate of the spatial navigation
|
|
239
|
+
* @param dir {SpatialNavigationDirection}- The directional information for the spatial navigation (e.g. LRUD)
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
242
|
+
function focusingController(bestCandidate, dir) {
|
|
243
|
+
// 10 & 11
|
|
244
|
+
// When bestCandidate is found
|
|
245
|
+
if (bestCandidate) {
|
|
246
|
+
// When bestCandidate is a focusable element and not a container : move focus
|
|
247
|
+
/*
|
|
248
|
+
* [event] navbeforefocus : Fired before spatial or sequential navigation changes the focus.
|
|
249
|
+
*/
|
|
250
|
+
if (!createSpatNavEvents('beforefocus', bestCandidate, null, dir))
|
|
251
|
+
return true;
|
|
252
|
+
|
|
253
|
+
const container = bestCandidate.getSpatialNavigationContainer();
|
|
254
|
+
|
|
255
|
+
if ((container !== window) && (getCSSSpatNavAction(container) === 'focus')) {
|
|
256
|
+
bestCandidate.focus();
|
|
257
|
+
} else {
|
|
258
|
+
bestCandidate.focus({preventScroll: true});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
startingPoint = null;
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// When bestCandidate is not found within the scrollport of a container: Nothing
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Directionally scroll the scrollable spatial navigation container if it can be manually scrolled more.
|
|
271
|
+
* @function scrollingController
|
|
272
|
+
* @param container {Node} - The spatial navigation container which can scroll
|
|
273
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
274
|
+
* @returns {boolean}
|
|
275
|
+
*/
|
|
276
|
+
function scrollingController(container, dir) {
|
|
277
|
+
|
|
278
|
+
// If there is any scrollable area among parent elements and it can be manually scrolled, scroll the document
|
|
279
|
+
if (isScrollable(container, dir) && !isScrollBoundary(container, dir)) {
|
|
280
|
+
moveScroll(container, dir);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If the spatnav container is document and it can be scrolled, scroll the document
|
|
285
|
+
if (!container.parentElement && !isHTMLScrollBoundary(container, dir)) {
|
|
286
|
+
moveScroll(container.ownerDocument.documentElement, dir);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Find the candidates within a spatial navigation container include delegable container.
|
|
294
|
+
* This function does not search inside delegable container or focusable container.
|
|
295
|
+
* In other words, this return candidates set is not included focusable elements inside delegable container or focusable container.
|
|
296
|
+
*
|
|
297
|
+
* @function getSpatialNavigationCandidates
|
|
298
|
+
* @param container {Node} - The spatial navigation container
|
|
299
|
+
* @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements.
|
|
300
|
+
* Default value is 'visible'.
|
|
301
|
+
* @returns {sequence<Node>} candidate elements within the container
|
|
302
|
+
*/
|
|
303
|
+
function getSpatialNavigationCandidates (container, option = {mode: 'visible'}) {
|
|
304
|
+
let candidates = [];
|
|
305
|
+
|
|
306
|
+
if (container.childElementCount > 0) {
|
|
307
|
+
if (!container.parentElement) {
|
|
308
|
+
container = container.getElementsByTagName('body')[0] || document.body;
|
|
309
|
+
}
|
|
310
|
+
const children = container.children;
|
|
311
|
+
for (const elem of children) {
|
|
312
|
+
if (isDelegableContainer(elem)) {
|
|
313
|
+
candidates.push(elem);
|
|
314
|
+
} else if (isFocusable(elem)) {
|
|
315
|
+
candidates.push(elem);
|
|
316
|
+
|
|
317
|
+
if (!isContainer(elem) && elem.childElementCount) {
|
|
318
|
+
candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
|
|
319
|
+
}
|
|
320
|
+
} else if (elem.childElementCount) {
|
|
321
|
+
candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return (option.mode === 'all') ? candidates : candidates.filter(isVisible);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Find the candidates among focusable elements within a spatial navigation container from the search origin (currently focused element)
|
|
330
|
+
* depending on the directional information.
|
|
331
|
+
* @function getFilteredSpatialNavigationCandidates
|
|
332
|
+
* @param element {Node} - The currently focused element which is defined as 'search origin' in the spec
|
|
333
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
334
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation without the directional information
|
|
335
|
+
* @param container {Node} - The spatial navigation container
|
|
336
|
+
* @returns {Node} The candidates for spatial navigation considering the directional information
|
|
337
|
+
*/
|
|
338
|
+
function getFilteredSpatialNavigationCandidates (element, dir, candidates, container) {
|
|
339
|
+
const targetElement = element;
|
|
340
|
+
// Removed below line due to a bug. (iframe body rect is sometime weird.)
|
|
341
|
+
// const targetElement = (element.nodeName === 'IFRAME') ? element.contentDocument.body : element;
|
|
342
|
+
// If the container is unknown, get the closest container from the element
|
|
343
|
+
container = container || targetElement.getSpatialNavigationContainer();
|
|
344
|
+
|
|
345
|
+
// If the candidates is unknown, find candidates
|
|
346
|
+
// 5-1
|
|
347
|
+
candidates = (!candidates || candidates.length <= 0) ? getSpatialNavigationCandidates(container) : candidates;
|
|
348
|
+
return filteredCandidates(targetElement, candidates, dir, container);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Find the best candidate among the candidates within the container from the search origin (currently focused element)
|
|
353
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-spatialnavigationsearch}
|
|
354
|
+
* @function spatialNavigationSearch
|
|
355
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
356
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation
|
|
357
|
+
* @param container {Node} - The spatial navigation container
|
|
358
|
+
* @returns {Node} The best candidate which will gain the focus
|
|
359
|
+
*/
|
|
360
|
+
function spatialNavigationSearch (dir, args) {
|
|
361
|
+
const targetElement = this;
|
|
362
|
+
let internalCandidates = [];
|
|
363
|
+
let externalCandidates = [];
|
|
364
|
+
let insideOverlappedCandidates = getOverlappedCandidates(targetElement);
|
|
365
|
+
let bestTarget;
|
|
366
|
+
|
|
367
|
+
// Set default parameter value
|
|
368
|
+
if (!args)
|
|
369
|
+
args = {};
|
|
370
|
+
|
|
371
|
+
const defaultContainer = targetElement.getSpatialNavigationContainer();
|
|
372
|
+
let defaultCandidates = getSpatialNavigationCandidates(defaultContainer);
|
|
373
|
+
const container = args.container || defaultContainer;
|
|
374
|
+
if (args.container && (defaultContainer.contains(args.container))) {
|
|
375
|
+
defaultCandidates = defaultCandidates.concat(getSpatialNavigationCandidates(container));
|
|
376
|
+
}
|
|
377
|
+
const candidates = (args.candidates && args.candidates.length > 0) ?
|
|
378
|
+
args.candidates.filter((candidate) => container.contains(candidate)) :
|
|
379
|
+
defaultCandidates.filter((candidate) => container.contains(candidate) && (container !== candidate));
|
|
380
|
+
|
|
381
|
+
// Find the best candidate
|
|
382
|
+
// 5
|
|
383
|
+
// If startingPoint is either a scroll container or the document,
|
|
384
|
+
// find the best candidate within startingPoint
|
|
385
|
+
if (candidates && candidates.length > 0) {
|
|
386
|
+
|
|
387
|
+
// Divide internal or external candidates
|
|
388
|
+
candidates.forEach(candidate => {
|
|
389
|
+
if (candidate !== targetElement) {
|
|
390
|
+
(targetElement.contains(candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// include overlapped element to the internalCandidates
|
|
395
|
+
let fullyOverlapped = insideOverlappedCandidates.filter(candidate => !internalCandidates.includes(candidate));
|
|
396
|
+
let overlappedContainer = candidates.filter(candidate => (isContainer(candidate) && isEntirelyVisible(targetElement, candidate)));
|
|
397
|
+
let overlappedByParent = overlappedContainer.map((elm) => elm.focusableAreas()).flat().filter(candidate => candidate !== targetElement);
|
|
398
|
+
|
|
399
|
+
internalCandidates = internalCandidates.concat(fullyOverlapped).filter((candidate) => container.contains(candidate));
|
|
400
|
+
externalCandidates = externalCandidates.concat(overlappedByParent).filter((candidate) => container.contains(candidate));
|
|
401
|
+
|
|
402
|
+
// Filter external Candidates
|
|
403
|
+
if (externalCandidates.length > 0) {
|
|
404
|
+
externalCandidates = getFilteredSpatialNavigationCandidates(targetElement, dir, externalCandidates, container);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If there isn't search origin element but search orgin rect exist (search origin isn't in the layout case)
|
|
408
|
+
if (searchOriginRect) {
|
|
409
|
+
bestTarget = selectBestCandidate(targetElement, getFilteredSpatialNavigationCandidates(targetElement, dir, internalCandidates, container), dir);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if ((internalCandidates && internalCandidates.length > 0) && !(targetElement.nodeName === 'INPUT')) {
|
|
413
|
+
bestTarget = selectBestCandidateFromEdge(targetElement, internalCandidates, dir);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
bestTarget = bestTarget || selectBestCandidate(targetElement, externalCandidates, dir);
|
|
417
|
+
|
|
418
|
+
if (bestTarget && isDelegableContainer(bestTarget)) {
|
|
419
|
+
// if best target is delegable container, then find descendants candidate inside delegable container.
|
|
420
|
+
const innerTarget = getSpatialNavigationCandidates(bestTarget, {mode: 'all'});
|
|
421
|
+
const descendantsBest = innerTarget.length > 0 ? targetElement.spatialNavigationSearch(dir, {candidates: innerTarget, container: bestTarget}) : null;
|
|
422
|
+
if (descendantsBest) {
|
|
423
|
+
bestTarget = descendantsBest;
|
|
424
|
+
} else if (!isFocusable(bestTarget)) {
|
|
425
|
+
// if there is no target inside bestTarget and delegable container is not focusable,
|
|
426
|
+
// then try to find another best target without curren best target.
|
|
427
|
+
candidates.splice(candidates.indexOf(bestTarget), 1);
|
|
428
|
+
bestTarget = candidates.length ? targetElement.spatialNavigationSearch(dir, {candidates: candidates, container: container}) : null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return bestTarget;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get the filtered candidate among candidates.
|
|
439
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
|
|
440
|
+
* @function filteredCandidates
|
|
441
|
+
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
|
|
442
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation
|
|
443
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
444
|
+
* @param container {Node} - The spatial navigation container
|
|
445
|
+
* @returns {sequence<Node>} The filtered candidates which are not the search origin and not in the given spatial navigation direction from the search origin
|
|
446
|
+
*/
|
|
447
|
+
// TODO: Need to fix filtering the candidates with more clean code
|
|
448
|
+
function filteredCandidates(currentElm, candidates, dir, container) {
|
|
449
|
+
const originalContainer = currentElm.getSpatialNavigationContainer();
|
|
450
|
+
let eventTargetRect;
|
|
451
|
+
|
|
452
|
+
// If D(dir) is null, let candidates be the same as visibles
|
|
453
|
+
if (dir === undefined)
|
|
454
|
+
return candidates;
|
|
455
|
+
|
|
456
|
+
// Offscreen handling when originalContainer is not <HTML>
|
|
457
|
+
if (originalContainer.parentElement && container !== originalContainer && !isVisible(currentElm)) {
|
|
458
|
+
eventTargetRect = getBoundingClientRect(originalContainer);
|
|
459
|
+
} else {
|
|
460
|
+
eventTargetRect = searchOriginRect || getBoundingClientRect(currentElm);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* Else, let candidates be the subset of the elements in visibles
|
|
465
|
+
* whose principal box’s geometric center is within the closed half plane
|
|
466
|
+
* whose boundary goes through the geometric center of starting point and is perpendicular to D.
|
|
467
|
+
*/
|
|
468
|
+
if ((isContainer(currentElm) || currentElm.nodeName === 'BODY') && !(currentElm.nodeName === 'INPUT')) {
|
|
469
|
+
return candidates.filter(candidate => {
|
|
470
|
+
const candidateRect = getBoundingClientRect(candidate);
|
|
471
|
+
return container.contains(candidate) &&
|
|
472
|
+
((currentElm.contains(candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm) ||
|
|
473
|
+
isOutside(candidateRect, eventTargetRect, dir));
|
|
474
|
+
});
|
|
475
|
+
} else {
|
|
476
|
+
return candidates.filter(candidate => {
|
|
477
|
+
const candidateRect = getBoundingClientRect(candidate);
|
|
478
|
+
const candidateBody = (candidate.nodeName === 'IFRAME') ? candidate.contentDocument.body : null;
|
|
479
|
+
return container.contains(candidate) &&
|
|
480
|
+
candidate !== currentElm && candidateBody !== currentElm &&
|
|
481
|
+
isOutside(candidateRect, eventTargetRect, dir) &&
|
|
482
|
+
!isInside(eventTargetRect, candidateRect);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Select the best candidate among given candidates.
|
|
489
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
|
|
490
|
+
* @function selectBestCandidate
|
|
491
|
+
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
|
|
492
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation
|
|
493
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
494
|
+
* @returns {Node} The best candidate which will gain the focus
|
|
495
|
+
*/
|
|
496
|
+
function selectBestCandidate(currentElm, candidates, dir) {
|
|
497
|
+
const container = currentElm.getSpatialNavigationContainer();
|
|
498
|
+
const spatialNavigationFunction = getComputedStyle(container).getPropertyValue('--spatial-navigation-function');
|
|
499
|
+
const currentTargetRect = searchOriginRect || getBoundingClientRect(currentElm);
|
|
500
|
+
let distanceFunction;
|
|
501
|
+
let alignedCandidates;
|
|
502
|
+
|
|
503
|
+
switch (spatialNavigationFunction) {
|
|
504
|
+
case 'grid':
|
|
505
|
+
alignedCandidates = candidates.filter(elm => isAligned(currentTargetRect, getBoundingClientRect(elm), dir));
|
|
506
|
+
if (alignedCandidates.length > 0) {
|
|
507
|
+
candidates = alignedCandidates;
|
|
508
|
+
}
|
|
509
|
+
distanceFunction = getAbsoluteDistance;
|
|
510
|
+
break;
|
|
511
|
+
default:
|
|
512
|
+
distanceFunction = getDistance;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
return getClosestElement(currentElm, candidates, dir, distanceFunction);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Select the best candidate among candidates by finding the closet candidate from the edge of the currently focused element (search origin).
|
|
520
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate (Step 5)}
|
|
521
|
+
* @function selectBestCandidateFromEdge
|
|
522
|
+
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
|
|
523
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation
|
|
524
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
525
|
+
* @returns {Node} The best candidate which will gain the focus
|
|
526
|
+
*/
|
|
527
|
+
function selectBestCandidateFromEdge(currentElm, candidates, dir) {
|
|
528
|
+
if (startingPoint)
|
|
529
|
+
return getClosestElement(currentElm, candidates, dir, getDistanceFromPoint);
|
|
530
|
+
else
|
|
531
|
+
return getClosestElement(currentElm, candidates, dir, getInnerDistance);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Select the closest candidate from the currently focused element (search origin) among candidates by using the distance function.
|
|
536
|
+
* @function getClosestElement
|
|
537
|
+
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
|
|
538
|
+
* @param candidates {sequence<Node>} - The candidates for spatial navigation
|
|
539
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
540
|
+
* @param distanceFunction {function} - The distance function which measures the distance from the search origin to each candidate
|
|
541
|
+
* @returns {Node} The candidate which is the closest one from the search origin
|
|
542
|
+
*/
|
|
543
|
+
function getClosestElement(currentElm, candidates, dir, distanceFunction) {
|
|
544
|
+
let eventTargetRect = null;
|
|
545
|
+
if (( window.location !== window.parent.location ) && (currentElm.nodeName === 'BODY' || currentElm.nodeName === 'HTML')) {
|
|
546
|
+
// If the eventTarget is iframe, then get rect of it based on its containing document
|
|
547
|
+
// Set the iframe's position as (0,0) because the rects of elements inside the iframe don't know the real iframe's position.
|
|
548
|
+
eventTargetRect = window.frameElement.getBoundingClientRect();
|
|
549
|
+
eventTargetRect.x = 0;
|
|
550
|
+
eventTargetRect.y = 0;
|
|
551
|
+
} else {
|
|
552
|
+
eventTargetRect = searchOriginRect || currentElm.getBoundingClientRect();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let minDistance = Number.POSITIVE_INFINITY;
|
|
556
|
+
let minDistanceElements = [];
|
|
557
|
+
|
|
558
|
+
if (candidates) {
|
|
559
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
560
|
+
const distance = distanceFunction(eventTargetRect, getBoundingClientRect(candidates[i]), dir);
|
|
561
|
+
|
|
562
|
+
// If the same distance, the candidate will be selected in the DOM order
|
|
563
|
+
if (distance < minDistance) {
|
|
564
|
+
minDistance = distance;
|
|
565
|
+
minDistanceElements = [candidates[i]];
|
|
566
|
+
} else if (distance === minDistance) {
|
|
567
|
+
minDistanceElements.push(candidates[i]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (minDistanceElements.length === 0)
|
|
572
|
+
return null;
|
|
573
|
+
|
|
574
|
+
return (minDistanceElements.length > 1 && distanceFunction === getAbsoluteDistance) ?
|
|
575
|
+
getClosestElement(currentElm, minDistanceElements, dir, getEuclideanDistance) : minDistanceElements[0];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get container of an element.
|
|
580
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-getspatialnavigationcontainer}
|
|
581
|
+
* @module Element
|
|
582
|
+
* @function getSpatialNavigationContainer
|
|
583
|
+
* @returns {Node} The spatial navigation container
|
|
584
|
+
*/
|
|
585
|
+
function getSpatialNavigationContainer() {
|
|
586
|
+
let container = this;
|
|
587
|
+
|
|
588
|
+
do {
|
|
589
|
+
if (!container.parentElement) {
|
|
590
|
+
if (window.location !== window.parent.location) {
|
|
591
|
+
container = window.parent.document.documentElement;
|
|
592
|
+
} else {
|
|
593
|
+
container = window.document.documentElement;
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
} else {
|
|
597
|
+
container = container.parentElement;
|
|
598
|
+
}
|
|
599
|
+
} while (!isContainer(container));
|
|
600
|
+
return container;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get nearest scroll container of an element.
|
|
605
|
+
* @function getScrollContainer
|
|
606
|
+
* @param Element
|
|
607
|
+
* @returns {Node} The spatial navigation container
|
|
608
|
+
*/
|
|
609
|
+
function getScrollContainer(element) {
|
|
610
|
+
let scrollContainer = element;
|
|
611
|
+
|
|
612
|
+
do {
|
|
613
|
+
if (!scrollContainer.parentElement) {
|
|
614
|
+
if (window.location !== window.parent.location) {
|
|
615
|
+
scrollContainer = window.parent.document.documentElement;
|
|
616
|
+
} else {
|
|
617
|
+
scrollContainer = window.document.documentElement;
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
} else {
|
|
621
|
+
scrollContainer = scrollContainer.parentElement;
|
|
622
|
+
}
|
|
623
|
+
} while (!isScrollContainer(scrollContainer) || !isVisible(scrollContainer));
|
|
624
|
+
|
|
625
|
+
if (scrollContainer === document || scrollContainer === document.documentElement) {
|
|
626
|
+
scrollContainer = window;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return scrollContainer;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Find focusable elements within the spatial navigation container.
|
|
634
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-focusableareas}
|
|
635
|
+
* @function focusableAreas
|
|
636
|
+
* @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements.
|
|
637
|
+
* Default value is 'visible'.
|
|
638
|
+
* @returns {sequence<Node>} All focusable elements or only visible focusable elements within the container
|
|
639
|
+
*/
|
|
640
|
+
function focusableAreas(option = {mode: 'visible'}) {
|
|
641
|
+
const container = this.parentElement ? this : document.body;
|
|
642
|
+
const focusables = Array.prototype.filter.call(container.getElementsByTagName('*'), isFocusable);
|
|
643
|
+
return (option.mode === 'all') ? focusables : focusables.filter(isVisible);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Create the NavigationEvent: navbeforefocus, navnotarget
|
|
648
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#events-navigationevent}
|
|
649
|
+
* @function createSpatNavEvents
|
|
650
|
+
* @param option {string} - Type of the navigation event (beforefocus, notarget)
|
|
651
|
+
* @param element {Node} - The target element of the event
|
|
652
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
653
|
+
*/
|
|
654
|
+
function createSpatNavEvents(eventType, containerElement, currentElement, direction) {
|
|
655
|
+
if (['beforefocus', 'notarget'].includes(eventType)) {
|
|
656
|
+
const data = {
|
|
657
|
+
causedTarget: currentElement,
|
|
658
|
+
dir: direction
|
|
659
|
+
};
|
|
660
|
+
const triggeredEvent = new CustomEvent('nav' + eventType, {bubbles: true, cancelable: true, detail: data});
|
|
661
|
+
return containerElement.dispatchEvent(triggeredEvent);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get the value of the CSS custom property of the element
|
|
667
|
+
* @function readCssVar
|
|
668
|
+
* @param element {Node}
|
|
669
|
+
* @param varName {string} - The name of the css custom property without '--'
|
|
670
|
+
* @returns {string} The value of the css custom property
|
|
671
|
+
*/
|
|
672
|
+
function readCssVar(element, varName) {
|
|
673
|
+
// 20210606 fix getPropertyValue returning null ~inf
|
|
674
|
+
return (element.style.getPropertyValue(`--${varName}`) || '').trim();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Decide whether or not the 'contain' value is given to 'spatial-navigation-contain' css property of an element
|
|
679
|
+
* @function isCSSSpatNavContain
|
|
680
|
+
* @param element {Node}
|
|
681
|
+
* @returns {boolean}
|
|
682
|
+
*/
|
|
683
|
+
function isCSSSpatNavContain(element) {
|
|
684
|
+
return readCssVar(element, 'spatial-navigation-contain') === 'contain';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Return the value of 'spatial-navigation-action' css property of an element
|
|
689
|
+
* @function getCSSSpatNavAction
|
|
690
|
+
* @param element {Node} - would be the spatial navigation container
|
|
691
|
+
* @returns {string} auto | focus | scroll
|
|
692
|
+
*/
|
|
693
|
+
function getCSSSpatNavAction(element) {
|
|
694
|
+
return readCssVar(element, 'spatial-navigation-action') || 'auto';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Only move the focus with spatial navigation. Manually scrolling isn't available.
|
|
699
|
+
* @function navigateChain
|
|
700
|
+
* @param eventTarget {Node} - currently focused element
|
|
701
|
+
* @param container {SpatialNavigationContainer} - container
|
|
702
|
+
* @param parentContainer {SpatialNavigationContainer} - parent container
|
|
703
|
+
* @param option - visible || all
|
|
704
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
705
|
+
*/
|
|
706
|
+
function navigateChain(eventTarget, container, parentContainer, dir, option) {
|
|
707
|
+
let currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};
|
|
708
|
+
|
|
709
|
+
while (parentContainer) {
|
|
710
|
+
if (focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) {
|
|
711
|
+
return;
|
|
712
|
+
} else {
|
|
713
|
+
if ((option === 'visible') && scrollingController(container, dir)) return;
|
|
714
|
+
else {
|
|
715
|
+
if (!createSpatNavEvents('notarget', container, eventTarget, dir)) return;
|
|
716
|
+
|
|
717
|
+
// find the container
|
|
718
|
+
if (container === document || container === document.documentElement) {
|
|
719
|
+
if ( window.location !== window.parent.location ) {
|
|
720
|
+
// The page is in an iframe. eventTarget needs to be reset because the position of the element in the iframe
|
|
721
|
+
eventTarget = window.frameElement;
|
|
722
|
+
container = eventTarget.ownerDocument.documentElement;
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
container = parentContainer;
|
|
726
|
+
}
|
|
727
|
+
currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};
|
|
728
|
+
let nextContainer = container.getSpatialNavigationContainer();
|
|
729
|
+
|
|
730
|
+
if (nextContainer !== container) {
|
|
731
|
+
parentContainer = nextContainer;
|
|
732
|
+
} else {
|
|
733
|
+
parentContainer = null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};
|
|
740
|
+
|
|
741
|
+
// Behavior after 'navnotarget' - Getting out from the current spatnav container
|
|
742
|
+
if ((!parentContainer && container) && focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) return;
|
|
743
|
+
|
|
744
|
+
if (!createSpatNavEvents('notarget', currentOption.container, eventTarget, dir)) return;
|
|
745
|
+
|
|
746
|
+
if ((getCSSSpatNavAction(container) === 'auto') && (option === 'visible')) {
|
|
747
|
+
if (scrollingController(container, dir)) return;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Find search origin
|
|
753
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#nav}
|
|
754
|
+
* @function findSearchOrigin
|
|
755
|
+
* @returns {Node} The search origin for the spatial navigation
|
|
756
|
+
*/
|
|
757
|
+
function findSearchOrigin() {
|
|
758
|
+
let searchOrigin = document.activeElement;
|
|
759
|
+
|
|
760
|
+
if (!searchOrigin || (searchOrigin === document.body && !document.querySelector(':focus'))) {
|
|
761
|
+
// When the previous search origin lost its focus by blur: (1) disable attribute (2) visibility: hidden
|
|
762
|
+
if (savedSearchOrigin.element && (searchOrigin !== savedSearchOrigin.element)) {
|
|
763
|
+
const elementStyle = window.getComputedStyle(savedSearchOrigin.element, null);
|
|
764
|
+
const invisibleStyle = ['hidden', 'collapse'];
|
|
765
|
+
|
|
766
|
+
if (savedSearchOrigin.element.disabled || invisibleStyle.includes(elementStyle.getPropertyValue('visibility'))) {
|
|
767
|
+
searchOrigin = savedSearchOrigin.element;
|
|
768
|
+
return searchOrigin;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
searchOrigin = document.documentElement;
|
|
772
|
+
}
|
|
773
|
+
// When the previous search origin lost its focus by blur: (1) display:none () element size turned into zero
|
|
774
|
+
if (savedSearchOrigin.element &&
|
|
775
|
+
((getBoundingClientRect(savedSearchOrigin.element).height === 0) || (getBoundingClientRect(savedSearchOrigin.element).width === 0))) {
|
|
776
|
+
searchOriginRect = savedSearchOrigin.rect;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!isVisibleInScroller(searchOrigin)) {
|
|
780
|
+
const scroller = getScrollContainer(searchOrigin);
|
|
781
|
+
if (scroller && ((scroller === window) || (getCSSSpatNavAction(scroller) === 'auto')))
|
|
782
|
+
return scroller;
|
|
783
|
+
}
|
|
784
|
+
return searchOrigin;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Move the scroll of an element depending on the given spatial navigation directrion
|
|
789
|
+
* (Assume that User Agent defined distance is '40px')
|
|
790
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#directionally-scroll-an-element}
|
|
791
|
+
* @function moveScroll
|
|
792
|
+
* @param element {Node} - The scrollable element
|
|
793
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
794
|
+
* @param offset {Number} - The explicit amount of offset for scrolling. Default value is 0.
|
|
795
|
+
*/
|
|
796
|
+
function moveScroll(element, dir, offset = 0) {
|
|
797
|
+
if (element) {
|
|
798
|
+
switch (dir) {
|
|
799
|
+
case 'left': element.scrollLeft -= (40 + offset); break;
|
|
800
|
+
case 'right': element.scrollLeft += (40 + offset); break;
|
|
801
|
+
case 'up': element.scrollTop -= (40 + offset); break;
|
|
802
|
+
case 'down': element.scrollTop += (40 + offset); break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Decide whether an element is container or not.
|
|
809
|
+
* @function isContainer
|
|
810
|
+
* @param element {Node} element
|
|
811
|
+
* @returns {boolean}
|
|
812
|
+
*/
|
|
813
|
+
function isContainer(element) {
|
|
814
|
+
return (!element.parentElement) ||
|
|
815
|
+
(element.nodeName === 'IFRAME') ||
|
|
816
|
+
(isScrollContainer(element)) ||
|
|
817
|
+
(isCSSSpatNavContain(element));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Decide whether an element is delegable container or not.
|
|
822
|
+
* NOTE: THIS IS NON-NORMATIVE API.
|
|
823
|
+
* @function isDelegableContainer
|
|
824
|
+
* @param element {Node} element
|
|
825
|
+
* @returns {boolean}
|
|
826
|
+
*/
|
|
827
|
+
function isDelegableContainer(element) {
|
|
828
|
+
return readCssVar(element, 'spatial-navigation-contain') === 'delegable';
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Decide whether an element is a scrollable container or not.
|
|
833
|
+
* @see {@link https://drafts.csswg.org/css-overflow-3/#scroll-container}
|
|
834
|
+
* @function isScrollContainer
|
|
835
|
+
* @param element {Node}
|
|
836
|
+
* @returns {boolean}
|
|
837
|
+
*/
|
|
838
|
+
function isScrollContainer(element) {
|
|
839
|
+
const elementStyle = window.getComputedStyle(element, null);
|
|
840
|
+
const overflowX = elementStyle.getPropertyValue('overflow-x');
|
|
841
|
+
const overflowY = elementStyle.getPropertyValue('overflow-y');
|
|
842
|
+
|
|
843
|
+
return ((overflowX !== 'visible' && overflowX !== 'clip' && isOverflow(element, 'left')) ||
|
|
844
|
+
(overflowY !== 'visible' && overflowY !== 'clip' && isOverflow(element, 'down'))) ?
|
|
845
|
+
true : false;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Decide whether this element is scrollable or not.
|
|
850
|
+
* NOTE: If the value of 'overflow' is given to either 'visible', 'clip', or 'hidden', the element isn't scrollable.
|
|
851
|
+
* If the value is 'hidden', the element can be only programmically scrollable. (https://drafts.csswg.org/css-overflow-3/#valdef-overflow-hidden)
|
|
852
|
+
* @function isScrollable
|
|
853
|
+
* @param element {Node}
|
|
854
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
855
|
+
* @returns {boolean}
|
|
856
|
+
*/
|
|
857
|
+
function isScrollable(element, dir) { // element, dir
|
|
858
|
+
if (element && typeof element === 'object') {
|
|
859
|
+
if (dir && typeof dir === 'string') { // parameter: dir, element
|
|
860
|
+
if (isOverflow(element, dir)) {
|
|
861
|
+
// style property
|
|
862
|
+
const elementStyle = window.getComputedStyle(element, null);
|
|
863
|
+
const overflowX = elementStyle.getPropertyValue('overflow-x');
|
|
864
|
+
const overflowY = elementStyle.getPropertyValue('overflow-y');
|
|
865
|
+
|
|
866
|
+
switch (dir) {
|
|
867
|
+
case 'left':
|
|
868
|
+
/* falls through */
|
|
869
|
+
case 'right':
|
|
870
|
+
return (overflowX !== 'visible' && overflowX !== 'clip' && overflowX !== 'hidden');
|
|
871
|
+
case 'up':
|
|
872
|
+
/* falls through */
|
|
873
|
+
case 'down':
|
|
874
|
+
return (overflowY !== 'visible' && overflowY !== 'clip' && overflowY !== 'hidden');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return false;
|
|
878
|
+
} else { // parameter: element
|
|
879
|
+
return (element.nodeName === 'HTML' || element.nodeName === 'BODY') ||
|
|
880
|
+
(isScrollContainer(element) && isOverflow(element));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Decide whether an element is overflow or not.
|
|
887
|
+
* @function isOverflow
|
|
888
|
+
* @param element {Node}
|
|
889
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
890
|
+
* @returns {boolean}
|
|
891
|
+
*/
|
|
892
|
+
function isOverflow(element, dir) {
|
|
893
|
+
if (element && typeof element === 'object') {
|
|
894
|
+
if (dir && typeof dir === 'string') { // parameter: element, dir
|
|
895
|
+
switch (dir) {
|
|
896
|
+
case 'left':
|
|
897
|
+
/* falls through */
|
|
898
|
+
case 'right':
|
|
899
|
+
return (element.scrollWidth > element.clientWidth);
|
|
900
|
+
case 'up':
|
|
901
|
+
/* falls through */
|
|
902
|
+
case 'down':
|
|
903
|
+
return (element.scrollHeight > element.clientHeight);
|
|
904
|
+
}
|
|
905
|
+
} else { // parameter: element
|
|
906
|
+
return (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight);
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Decide whether the scrollbar of the browsing context reaches to the end or not.
|
|
914
|
+
* @function isHTMLScrollBoundary
|
|
915
|
+
* @param element {Node} - The top browsing context
|
|
916
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
917
|
+
* @returns {boolean}
|
|
918
|
+
*/
|
|
919
|
+
function isHTMLScrollBoundary(element, dir) {
|
|
920
|
+
let result = false;
|
|
921
|
+
switch (dir) {
|
|
922
|
+
case 'left':
|
|
923
|
+
result = element.scrollLeft === 0;
|
|
924
|
+
break;
|
|
925
|
+
case 'right':
|
|
926
|
+
result = (element.scrollWidth - element.scrollLeft - element.clientWidth) === 0;
|
|
927
|
+
break;
|
|
928
|
+
case 'up':
|
|
929
|
+
result = element.scrollTop === 0;
|
|
930
|
+
break;
|
|
931
|
+
case 'down':
|
|
932
|
+
result = (element.scrollHeight - element.scrollTop - element.clientHeight) === 0;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
return result;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Decide whether the scrollbar of an element reaches to the end or not.
|
|
940
|
+
* @function isScrollBoundary
|
|
941
|
+
* @param element {Node}
|
|
942
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
943
|
+
* @returns {boolean}
|
|
944
|
+
*/
|
|
945
|
+
function isScrollBoundary(element, dir) {
|
|
946
|
+
if (isScrollable(element, dir)) {
|
|
947
|
+
const winScrollY = element.scrollTop;
|
|
948
|
+
const winScrollX = element.scrollLeft;
|
|
949
|
+
|
|
950
|
+
const height = element.scrollHeight - element.clientHeight;
|
|
951
|
+
const width = element.scrollWidth - element.clientWidth;
|
|
952
|
+
|
|
953
|
+
switch (dir) {
|
|
954
|
+
case 'left': return (winScrollX === 0);
|
|
955
|
+
case 'right': return (Math.abs(winScrollX - width) <= 1);
|
|
956
|
+
case 'up': return (winScrollY === 0);
|
|
957
|
+
case 'down': return (Math.abs(winScrollY - height) <= 1);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Decide whether an element is inside the scorller viewport or not
|
|
965
|
+
*
|
|
966
|
+
* @function isVisibleInScroller
|
|
967
|
+
* @param element {Node}
|
|
968
|
+
* @returns {boolean}
|
|
969
|
+
*/
|
|
970
|
+
function isVisibleInScroller(element) {
|
|
971
|
+
const elementRect = element.getBoundingClientRect();
|
|
972
|
+
let nearestScroller = getScrollContainer(element);
|
|
973
|
+
|
|
974
|
+
let scrollerRect = null;
|
|
975
|
+
if (nearestScroller !== window) {
|
|
976
|
+
scrollerRect = getBoundingClientRect(nearestScroller);
|
|
977
|
+
} else {
|
|
978
|
+
scrollerRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (isInside(scrollerRect, elementRect, 'left') && isInside(scrollerRect, elementRect, 'down'))
|
|
982
|
+
return true;
|
|
983
|
+
else
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Decide whether an element is focusable for spatial navigation.
|
|
989
|
+
* 1. If element is the browsing context (document, iframe), then it's focusable,
|
|
990
|
+
* 2. If the element is scrollable container (regardless of scrollable axis), then it's focusable,
|
|
991
|
+
* 3. The value of tabIndex >= 0, then it's focusable,
|
|
992
|
+
* 4. If the element is disabled, it isn't focusable,
|
|
993
|
+
* 5. If the element is expressly inert, it isn't focusable,
|
|
994
|
+
* 6. Whether the element is being rendered or not.
|
|
995
|
+
*
|
|
996
|
+
* @function isFocusable
|
|
997
|
+
* @param element {Node}
|
|
998
|
+
* @returns {boolean}
|
|
999
|
+
*
|
|
1000
|
+
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#focusable-area}
|
|
1001
|
+
*/
|
|
1002
|
+
function isFocusable(element) {
|
|
1003
|
+
if ((element.tabIndex < 0) || isAtagWithoutHref(element) || isActuallyDisabled(element) || isExpresslyInert(element) || !isBeingRendered(element))
|
|
1004
|
+
return false;
|
|
1005
|
+
else if ((!element.parentElement) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0))
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Decide whether an element is a tag without href attribute or not.
|
|
1011
|
+
*
|
|
1012
|
+
* @function isAtagWithoutHref
|
|
1013
|
+
* @param element {Node}
|
|
1014
|
+
* @returns {boolean}
|
|
1015
|
+
*/
|
|
1016
|
+
function isAtagWithoutHref(element) {
|
|
1017
|
+
return (element.tagName === 'A' && element.getAttribute('href') === null && element.getAttribute('tabIndex') === null);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Decide whether an element is actually disabled or not.
|
|
1022
|
+
*
|
|
1023
|
+
* @function isActuallyDisabled
|
|
1024
|
+
* @param element {Node}
|
|
1025
|
+
* @returns {boolean}
|
|
1026
|
+
*
|
|
1027
|
+
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
|
|
1028
|
+
*/
|
|
1029
|
+
function isActuallyDisabled(element) {
|
|
1030
|
+
if (['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(element.tagName))
|
|
1031
|
+
return (element.disabled);
|
|
1032
|
+
else
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Decide whether the element is expressly inert or not.
|
|
1038
|
+
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
|
|
1039
|
+
* @function isExpresslyInert
|
|
1040
|
+
* @param element {Node}
|
|
1041
|
+
* @returns {boolean}
|
|
1042
|
+
*/
|
|
1043
|
+
function isExpresslyInert(element) {
|
|
1044
|
+
return ((element.inert) && (!element.ownerDocument.documentElement.inert));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Decide whether the element is being rendered or not.
|
|
1049
|
+
* 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
|
|
1050
|
+
* 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
|
|
1051
|
+
* 3. If width and height of an element are explicitly set to 0, it is not being rendered.
|
|
1052
|
+
* 4. If a parent element is hidden, an element itself is not being rendered.
|
|
1053
|
+
* (CSS visibility property and display property are inherited.)
|
|
1054
|
+
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
|
|
1055
|
+
* @function isBeingRendered
|
|
1056
|
+
* @param element {Node}
|
|
1057
|
+
* @returns {boolean}
|
|
1058
|
+
*/
|
|
1059
|
+
function isBeingRendered(element) {
|
|
1060
|
+
if (!isVisibleStyleProperty(element.parentElement))
|
|
1061
|
+
return false;
|
|
1062
|
+
if (!isVisibleStyleProperty(element) || (element.style.opacity === '0') ||
|
|
1063
|
+
(window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px'))
|
|
1064
|
+
return false;
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Decide whether this element is partially or completely visible to user agent.
|
|
1070
|
+
* @function isVisible
|
|
1071
|
+
* @param element {Node}
|
|
1072
|
+
* @returns {boolean}
|
|
1073
|
+
*/
|
|
1074
|
+
function isVisible(element) {
|
|
1075
|
+
return (!element.parentElement) || (isVisibleStyleProperty(element) && hitTest(element));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Decide whether this element is completely visible in this viewport for the arrow direction.
|
|
1080
|
+
* @function isEntirelyVisible
|
|
1081
|
+
* @param element {Node}
|
|
1082
|
+
* @returns {boolean}
|
|
1083
|
+
*/
|
|
1084
|
+
function isEntirelyVisible(element, container) {
|
|
1085
|
+
const rect = getBoundingClientRect(element);
|
|
1086
|
+
const containerElm = container || element.getSpatialNavigationContainer();
|
|
1087
|
+
const containerRect = getBoundingClientRect(containerElm);
|
|
1088
|
+
|
|
1089
|
+
// FIXME: when element is bigger than container?
|
|
1090
|
+
const entirelyVisible = !((rect.left < containerRect.left) ||
|
|
1091
|
+
(rect.right > containerRect.right) ||
|
|
1092
|
+
(rect.top < containerRect.top) ||
|
|
1093
|
+
(rect.bottom > containerRect.bottom));
|
|
1094
|
+
|
|
1095
|
+
return entirelyVisible;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Decide the style property of this element is specified whether it's visible or not.
|
|
1100
|
+
* @function isVisibleStyleProperty
|
|
1101
|
+
* @param element {CSSStyleDeclaration}
|
|
1102
|
+
* @returns {boolean}
|
|
1103
|
+
*/
|
|
1104
|
+
function isVisibleStyleProperty(element) {
|
|
1105
|
+
const elementStyle = window.getComputedStyle(element, null);
|
|
1106
|
+
const thisVisibility = elementStyle.getPropertyValue('visibility');
|
|
1107
|
+
const thisDisplay = elementStyle.getPropertyValue('display');
|
|
1108
|
+
const invisibleStyle = ['hidden', 'collapse'];
|
|
1109
|
+
|
|
1110
|
+
return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility));
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Decide whether this element is entirely or partially visible within the viewport.
|
|
1115
|
+
* @function hitTest
|
|
1116
|
+
* @param element {Node}
|
|
1117
|
+
* @returns {boolean}
|
|
1118
|
+
*/
|
|
1119
|
+
function hitTest(element) {
|
|
1120
|
+
const elementRect = getBoundingClientRect(element);
|
|
1121
|
+
if (element.nodeName !== 'IFRAME' && (elementRect.top < 0 || elementRect.left < 0 ||
|
|
1122
|
+
elementRect.top > element.ownerDocument.documentElement.clientHeight || elementRect.left >element.ownerDocument.documentElement.clientWidth))
|
|
1123
|
+
return false;
|
|
1124
|
+
|
|
1125
|
+
let offsetX = parseInt(element.offsetWidth) / 10;
|
|
1126
|
+
let offsetY = parseInt(element.offsetHeight) / 10;
|
|
1127
|
+
|
|
1128
|
+
offsetX = isNaN(offsetX) ? 1 : offsetX;
|
|
1129
|
+
offsetY = isNaN(offsetY) ? 1 : offsetY;
|
|
1130
|
+
|
|
1131
|
+
const hitTestPoint = {
|
|
1132
|
+
// For performance, just using the three point(middle, leftTop, rightBottom) of the element for hit testing
|
|
1133
|
+
middle: [(elementRect.left + elementRect.right) / 2, (elementRect.top + elementRect.bottom) / 2],
|
|
1134
|
+
leftTop: [elementRect.left + offsetX, elementRect.top + offsetY],
|
|
1135
|
+
rightBottom: [elementRect.right - offsetX, elementRect.bottom - offsetY]
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
for(const point in hitTestPoint) {
|
|
1139
|
+
const elemFromPoint = element.ownerDocument.elementFromPoint(...hitTestPoint[point]);
|
|
1140
|
+
if (element === elemFromPoint || element.contains(elemFromPoint)) {
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Decide whether a child element is entirely or partially Included within container visually.
|
|
1149
|
+
* @function isInside
|
|
1150
|
+
* @param containerRect {DOMRect}
|
|
1151
|
+
* @param childRect {DOMRect}
|
|
1152
|
+
* @returns {boolean}
|
|
1153
|
+
*/
|
|
1154
|
+
function isInside(containerRect, childRect) {
|
|
1155
|
+
const rightEdgeCheck = (containerRect.left <= childRect.right && containerRect.right >= childRect.right);
|
|
1156
|
+
const leftEdgeCheck = (containerRect.left <= childRect.left && containerRect.right >= childRect.left);
|
|
1157
|
+
const topEdgeCheck = (containerRect.top <= childRect.top && containerRect.bottom >= childRect.top);
|
|
1158
|
+
const bottomEdgeCheck = (containerRect.top <= childRect.bottom && containerRect.bottom >= childRect.bottom);
|
|
1159
|
+
return (rightEdgeCheck || leftEdgeCheck) && (topEdgeCheck || bottomEdgeCheck);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Decide whether this element is entirely or partially visible within the viewport.
|
|
1164
|
+
* Note: rect1 is outside of rect2 for the dir
|
|
1165
|
+
* @function isOutside
|
|
1166
|
+
* @param rect1 {DOMRect}
|
|
1167
|
+
* @param rect2 {DOMRect}
|
|
1168
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1169
|
+
* @returns {boolean}
|
|
1170
|
+
*/
|
|
1171
|
+
function isOutside(rect1, rect2, dir) {
|
|
1172
|
+
switch (dir) {
|
|
1173
|
+
case 'left':
|
|
1174
|
+
return isRightSide(rect2, rect1);
|
|
1175
|
+
case 'right':
|
|
1176
|
+
return isRightSide(rect1, rect2);
|
|
1177
|
+
case 'up':
|
|
1178
|
+
return isBelow(rect2, rect1);
|
|
1179
|
+
case 'down':
|
|
1180
|
+
return isBelow(rect1, rect2);
|
|
1181
|
+
default:
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/* rect1 is right of rect2 */
|
|
1187
|
+
function isRightSide(rect1, rect2) {
|
|
1188
|
+
return rect1.left >= rect2.right || (rect1.left >= rect2.left && rect1.right > rect2.right && rect1.bottom > rect2.top && rect1.top < rect2.bottom);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/* rect1 is below of rect2 */
|
|
1192
|
+
function isBelow(rect1, rect2) {
|
|
1193
|
+
return rect1.top >= rect2.bottom || (rect1.top >= rect2.top && rect1.bottom > rect2.bottom && rect1.left < rect2.right && rect1.right > rect2.left);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/* rect1 is completely aligned or partially aligned for the direction */
|
|
1197
|
+
function isAligned(rect1, rect2, dir) {
|
|
1198
|
+
switch (dir) {
|
|
1199
|
+
case 'left' :
|
|
1200
|
+
/* falls through */
|
|
1201
|
+
case 'right' :
|
|
1202
|
+
return rect1.bottom > rect2.top && rect1.top < rect2.bottom;
|
|
1203
|
+
case 'up' :
|
|
1204
|
+
/* falls through */
|
|
1205
|
+
case 'down' :
|
|
1206
|
+
return rect1.right > rect2.left && rect1.left < rect2.right;
|
|
1207
|
+
default:
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
|
|
1214
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
|
|
1215
|
+
* @function getDistanceFromPoint
|
|
1216
|
+
* @param point {Point} - The search origin
|
|
1217
|
+
* @param element {DOMRect} - A candidate element
|
|
1218
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1219
|
+
* @returns {Number} The euclidian distance between the spatial navigation container and an element inside it
|
|
1220
|
+
*/
|
|
1221
|
+
function getDistanceFromPoint(point, element, dir) {
|
|
1222
|
+
point = startingPoint;
|
|
1223
|
+
// Get exit point, entry point -> {x: '', y: ''};
|
|
1224
|
+
const points = getEntryAndExitPoints(dir, point, element);
|
|
1225
|
+
|
|
1226
|
+
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
|
|
1227
|
+
// that minimize the distance between these two points
|
|
1228
|
+
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
|
|
1229
|
+
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
|
|
1230
|
+
|
|
1231
|
+
// The result is euclidian distance between P1 and P2.
|
|
1232
|
+
return Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
|
|
1237
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
|
|
1238
|
+
* @function getInnerDistance
|
|
1239
|
+
* @param rect1 {DOMRect} - The search origin
|
|
1240
|
+
* @param rect2 {DOMRect} - A candidate element
|
|
1241
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1242
|
+
* @returns {Number} The euclidean distance between the spatial navigation container and an element inside it
|
|
1243
|
+
*/
|
|
1244
|
+
function getInnerDistance(rect1, rect2, dir) {
|
|
1245
|
+
const baseEdgeForEachDirection = {left: 'right', right: 'left', up: 'bottom', down: 'top'};
|
|
1246
|
+
const baseEdge = baseEdgeForEachDirection[dir];
|
|
1247
|
+
|
|
1248
|
+
return Math.abs(rect1[baseEdge] - rect2[baseEdge]);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Get the distance between the search origin and a candidate element considering the direction.
|
|
1253
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#calculating-the-distance}
|
|
1254
|
+
* @function getDistance
|
|
1255
|
+
* @param searchOrigin {DOMRect | Point} - The search origin
|
|
1256
|
+
* @param candidateRect {DOMRect} - A candidate element
|
|
1257
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1258
|
+
* @returns {Number} The distance scoring between two elements
|
|
1259
|
+
*/
|
|
1260
|
+
function getDistance(searchOrigin, candidateRect, dir) {
|
|
1261
|
+
const kOrthogonalWeightForLeftRight = 30;
|
|
1262
|
+
const kOrthogonalWeightForUpDown = 2;
|
|
1263
|
+
|
|
1264
|
+
let orthogonalBias = 0;
|
|
1265
|
+
let alignBias = 0;
|
|
1266
|
+
const alignWeight = 5.0;
|
|
1267
|
+
|
|
1268
|
+
// Get exit point, entry point -> {x: '', y: ''};
|
|
1269
|
+
const points = getEntryAndExitPoints(dir, searchOrigin, candidateRect);
|
|
1270
|
+
|
|
1271
|
+
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
|
|
1272
|
+
// that minimize the distance between these two points
|
|
1273
|
+
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
|
|
1274
|
+
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
|
|
1275
|
+
|
|
1276
|
+
// A: The euclidean distance between P1 and P2.
|
|
1277
|
+
const A = Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
|
|
1278
|
+
let B, C;
|
|
1279
|
+
|
|
1280
|
+
// B: The absolute distance in the direction which is orthogonal to dir between P1 and P2, or 0 if dir is null.
|
|
1281
|
+
// C: The intersection edges between a candidate and the starting point.
|
|
1282
|
+
|
|
1283
|
+
// D: The square root of the area of intersection between the border boxes of candidate and starting point
|
|
1284
|
+
const intersectionRect = getIntersectionRect(searchOrigin, candidateRect);
|
|
1285
|
+
const D = intersectionRect.area;
|
|
1286
|
+
|
|
1287
|
+
switch (dir) {
|
|
1288
|
+
case 'left':
|
|
1289
|
+
/* falls through */
|
|
1290
|
+
case 'right' :
|
|
1291
|
+
// If two elements are aligned, add align bias
|
|
1292
|
+
// else, add orthogonal bias
|
|
1293
|
+
if (isAligned(searchOrigin, candidateRect, dir))
|
|
1294
|
+
alignBias = Math.min(intersectionRect.height / searchOrigin.height , 1);
|
|
1295
|
+
else
|
|
1296
|
+
orthogonalBias = (searchOrigin.height / 2);
|
|
1297
|
+
|
|
1298
|
+
B = (P2 + orthogonalBias) * kOrthogonalWeightForLeftRight;
|
|
1299
|
+
C = alignWeight * alignBias;
|
|
1300
|
+
break;
|
|
1301
|
+
|
|
1302
|
+
case 'up' :
|
|
1303
|
+
/* falls through */
|
|
1304
|
+
case 'down' :
|
|
1305
|
+
// If two elements are aligned, add align bias
|
|
1306
|
+
// else, add orthogonal bias
|
|
1307
|
+
if (isAligned(searchOrigin, candidateRect, dir))
|
|
1308
|
+
alignBias = Math.min(intersectionRect.width / searchOrigin.width , 1);
|
|
1309
|
+
else
|
|
1310
|
+
orthogonalBias = (searchOrigin.width / 2);
|
|
1311
|
+
|
|
1312
|
+
B = (P1 + orthogonalBias) * kOrthogonalWeightForUpDown;
|
|
1313
|
+
C = alignWeight * alignBias;
|
|
1314
|
+
break;
|
|
1315
|
+
|
|
1316
|
+
default:
|
|
1317
|
+
B = 0;
|
|
1318
|
+
C = 0;
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return (A + B - C - D);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Get the euclidean distance between the search origin and a candidate element considering the direction.
|
|
1327
|
+
* @function getEuclideanDistance
|
|
1328
|
+
* @param rect1 {DOMRect} - The search origin
|
|
1329
|
+
* @param rect2 {DOMRect} - A candidate element
|
|
1330
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1331
|
+
* @returns {Number} The distance scoring between two elements
|
|
1332
|
+
*/
|
|
1333
|
+
function getEuclideanDistance(rect1, rect2, dir) {
|
|
1334
|
+
// Get exit point, entry point
|
|
1335
|
+
const points = getEntryAndExitPoints(dir, rect1, rect2);
|
|
1336
|
+
|
|
1337
|
+
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
|
|
1338
|
+
// that minimize the distance between these two points
|
|
1339
|
+
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
|
|
1340
|
+
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
|
|
1341
|
+
|
|
1342
|
+
// Return the euclidean distance between P1 and P2.
|
|
1343
|
+
return Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Get the absolute distance between the search origin and a candidate element considering the direction.
|
|
1348
|
+
* @function getAbsoluteDistance
|
|
1349
|
+
* @param rect1 {DOMRect} - The search origin
|
|
1350
|
+
* @param rect2 {DOMRect} - A candidate element
|
|
1351
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
|
|
1352
|
+
* @returns {Number} The distance scoring between two elements
|
|
1353
|
+
*/
|
|
1354
|
+
function getAbsoluteDistance(rect1, rect2, dir) {
|
|
1355
|
+
// Get exit point, entry point
|
|
1356
|
+
const points = getEntryAndExitPoints(dir, rect1, rect2);
|
|
1357
|
+
|
|
1358
|
+
// Return the absolute distance in the dir direction between P1 and P.
|
|
1359
|
+
return ((dir === 'left') || (dir === 'right')) ?
|
|
1360
|
+
Math.abs(points.entryPoint.x - points.exitPoint.x) : Math.abs(points.entryPoint.y - points.exitPoint.y);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Get entry point and exit point of two elements considering the direction.
|
|
1365
|
+
* @function getEntryAndExitPoints
|
|
1366
|
+
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD). Default value for dir is 'down'.
|
|
1367
|
+
* @param searchOrigin {DOMRect | Point} - The search origin which contains the exit point
|
|
1368
|
+
* @param candidateRect {DOMRect} - One of candidates which contains the entry point
|
|
1369
|
+
* @returns {Points} The exit point from the search origin and the entry point from a candidate
|
|
1370
|
+
*/
|
|
1371
|
+
function getEntryAndExitPoints(dir = 'down', searchOrigin, candidateRect) {
|
|
1372
|
+
/**
|
|
1373
|
+
* User type definition for Point
|
|
1374
|
+
* @typeof {Object} Points
|
|
1375
|
+
* @property {Point} Points.entryPoint
|
|
1376
|
+
* @property {Point} Points.exitPoint
|
|
1377
|
+
*/
|
|
1378
|
+
const points = {entryPoint: {x: 0, y: 0}, exitPoint:{x: 0, y: 0}};
|
|
1379
|
+
|
|
1380
|
+
if (startingPoint) {
|
|
1381
|
+
points.exitPoint = searchOrigin;
|
|
1382
|
+
|
|
1383
|
+
switch (dir) {
|
|
1384
|
+
case 'left':
|
|
1385
|
+
points.entryPoint.x = candidateRect.right;
|
|
1386
|
+
break;
|
|
1387
|
+
case 'up':
|
|
1388
|
+
points.entryPoint.y = candidateRect.bottom;
|
|
1389
|
+
break;
|
|
1390
|
+
case 'right':
|
|
1391
|
+
points.entryPoint.x = candidateRect.left;
|
|
1392
|
+
break;
|
|
1393
|
+
case 'down':
|
|
1394
|
+
points.entryPoint.y = candidateRect.top;
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Set orthogonal direction
|
|
1399
|
+
switch (dir) {
|
|
1400
|
+
case 'left':
|
|
1401
|
+
case 'right':
|
|
1402
|
+
if (startingPoint.y <= candidateRect.top) {
|
|
1403
|
+
points.entryPoint.y = candidateRect.top;
|
|
1404
|
+
} else if (startingPoint.y < candidateRect.bottom) {
|
|
1405
|
+
points.entryPoint.y = startingPoint.y;
|
|
1406
|
+
} else {
|
|
1407
|
+
points.entryPoint.y = candidateRect.bottom;
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
|
|
1411
|
+
case 'up':
|
|
1412
|
+
case 'down':
|
|
1413
|
+
if (startingPoint.x <= candidateRect.left) {
|
|
1414
|
+
points.entryPoint.x = candidateRect.left;
|
|
1415
|
+
} else if (startingPoint.x < candidateRect.right) {
|
|
1416
|
+
points.entryPoint.x = startingPoint.x;
|
|
1417
|
+
} else {
|
|
1418
|
+
points.entryPoint.x = candidateRect.right;
|
|
1419
|
+
}
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
// Set direction
|
|
1425
|
+
switch (dir) {
|
|
1426
|
+
case 'left':
|
|
1427
|
+
points.exitPoint.x = searchOrigin.left;
|
|
1428
|
+
points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
|
|
1429
|
+
break;
|
|
1430
|
+
case 'up':
|
|
1431
|
+
points.exitPoint.y = searchOrigin.top;
|
|
1432
|
+
points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
|
|
1433
|
+
break;
|
|
1434
|
+
case 'right':
|
|
1435
|
+
points.exitPoint.x = searchOrigin.right;
|
|
1436
|
+
points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
|
|
1437
|
+
break;
|
|
1438
|
+
case 'down':
|
|
1439
|
+
points.exitPoint.y = searchOrigin.bottom;
|
|
1440
|
+
points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Set orthogonal direction
|
|
1445
|
+
switch (dir) {
|
|
1446
|
+
case 'left':
|
|
1447
|
+
case 'right':
|
|
1448
|
+
if (isBelow(searchOrigin, candidateRect)) {
|
|
1449
|
+
points.exitPoint.y = searchOrigin.top;
|
|
1450
|
+
points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
|
|
1451
|
+
} else if (isBelow(candidateRect, searchOrigin)) {
|
|
1452
|
+
points.exitPoint.y = searchOrigin.bottom;
|
|
1453
|
+
points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
|
|
1454
|
+
} else {
|
|
1455
|
+
points.exitPoint.y = Math.max(searchOrigin.top, candidateRect.top);
|
|
1456
|
+
points.entryPoint.y = points.exitPoint.y;
|
|
1457
|
+
}
|
|
1458
|
+
break;
|
|
1459
|
+
|
|
1460
|
+
case 'up':
|
|
1461
|
+
case 'down':
|
|
1462
|
+
if (isRightSide(searchOrigin, candidateRect)) {
|
|
1463
|
+
points.exitPoint.x = searchOrigin.left;
|
|
1464
|
+
points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
|
|
1465
|
+
} else if (isRightSide(candidateRect, searchOrigin)) {
|
|
1466
|
+
points.exitPoint.x = searchOrigin.right;
|
|
1467
|
+
points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
|
|
1468
|
+
} else {
|
|
1469
|
+
points.exitPoint.x = Math.max(searchOrigin.left, candidateRect.left);
|
|
1470
|
+
points.entryPoint.x = points.exitPoint.x;
|
|
1471
|
+
}
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return points;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Find focusable elements within the container
|
|
1481
|
+
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
|
|
1482
|
+
* @function getIntersectionRect
|
|
1483
|
+
* @param rect1 {DOMRect} - The search origin which contains the exit point
|
|
1484
|
+
* @param rect2 {DOMRect} - One of candidates which contains the entry point
|
|
1485
|
+
* @returns {IntersectionArea} The intersection area between two elements.
|
|
1486
|
+
*
|
|
1487
|
+
* @typeof {Object} IntersectionArea
|
|
1488
|
+
* @property {Number} IntersectionArea.width
|
|
1489
|
+
* @property {Number} IntersectionArea.height
|
|
1490
|
+
*/
|
|
1491
|
+
function getIntersectionRect(rect1, rect2) {
|
|
1492
|
+
const intersection_rect = {width: 0, height: 0, area: 0};
|
|
1493
|
+
|
|
1494
|
+
const new_location = [Math.max(rect1.left, rect2.left), Math.max(rect1.top, rect2.top)];
|
|
1495
|
+
const new_max_point = [Math.min(rect1.right, rect2.right), Math.min(rect1.bottom, rect2.bottom)];
|
|
1496
|
+
|
|
1497
|
+
intersection_rect.width = Math.abs(new_location[0] - new_max_point[0]);
|
|
1498
|
+
intersection_rect.height = Math.abs(new_location[1] - new_max_point[1]);
|
|
1499
|
+
|
|
1500
|
+
if (!(new_location[0] >= new_max_point[0] || new_location[1] >= new_max_point[1])) {
|
|
1501
|
+
// intersecting-cases
|
|
1502
|
+
intersection_rect.area = Math.sqrt(intersection_rect.width * intersection_rect.height);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
return intersection_rect;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Handle the spatial navigation behavior for HTMLInputElement, HTMLTextAreaElement
|
|
1510
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input|HTMLInputElement (MDN)}
|
|
1511
|
+
* @function handlingEditableElement
|
|
1512
|
+
* @param e {Event} - keydownEvent
|
|
1513
|
+
* @returns {boolean}
|
|
1514
|
+
*/
|
|
1515
|
+
function handlingEditableElement(e) {
|
|
1516
|
+
const SPINNABLE_INPUT_TYPES = ['email', 'date', 'month', 'number', 'time', 'week'],
|
|
1517
|
+
TEXT_INPUT_TYPES = ['password', 'text', 'search', 'tel', 'url', null];
|
|
1518
|
+
const eventTarget = document.activeElement;
|
|
1519
|
+
const focusNavigableArrowKey = {left: false, up: false, right: false, down: false};
|
|
1520
|
+
|
|
1521
|
+
const dir = ARROW_KEY_CODE[e.keyCode];
|
|
1522
|
+
if (dir === undefined) {
|
|
1523
|
+
return focusNavigableArrowKey;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (SPINNABLE_INPUT_TYPES.includes(eventTarget.getAttribute('type')) &&
|
|
1527
|
+
(dir === 'up' || dir === 'down')) {
|
|
1528
|
+
focusNavigableArrowKey[dir] = true;
|
|
1529
|
+
} else if (TEXT_INPUT_TYPES.includes(eventTarget.getAttribute('type')) || eventTarget.nodeName === 'TEXTAREA') {
|
|
1530
|
+
// 20210606 fix selectionStart unavailable on checkboxes ~inf
|
|
1531
|
+
const startPosition = eventTarget.selectionStart;
|
|
1532
|
+
const endPosition = eventTarget.selectionEnd;
|
|
1533
|
+
if (startPosition === endPosition) { // if there isn't any selected text
|
|
1534
|
+
if (startPosition === 0) {
|
|
1535
|
+
focusNavigableArrowKey.left = true;
|
|
1536
|
+
focusNavigableArrowKey.up = true;
|
|
1537
|
+
}
|
|
1538
|
+
if (endPosition === eventTarget.value.length) {
|
|
1539
|
+
focusNavigableArrowKey.right = true;
|
|
1540
|
+
focusNavigableArrowKey.down = true;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
} else { // HTMLDataListElement, HTMLSelectElement, HTMLOptGroup
|
|
1544
|
+
focusNavigableArrowKey[dir] = true;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return focusNavigableArrowKey;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Get the DOMRect of an element
|
|
1552
|
+
* @function getBoundingClientRect
|
|
1553
|
+
* @param {Node} element
|
|
1554
|
+
* @returns {DOMRect}
|
|
1555
|
+
*/
|
|
1556
|
+
function getBoundingClientRect(element) {
|
|
1557
|
+
// memoization
|
|
1558
|
+
let rect = mapOfBoundRect && mapOfBoundRect.get(element);
|
|
1559
|
+
if (!rect) {
|
|
1560
|
+
const boundingClientRect = element.getBoundingClientRect();
|
|
1561
|
+
rect = {
|
|
1562
|
+
top: Number(boundingClientRect.top.toFixed(2)),
|
|
1563
|
+
right: Number(boundingClientRect.right.toFixed(2)),
|
|
1564
|
+
bottom: Number(boundingClientRect.bottom.toFixed(2)),
|
|
1565
|
+
left: Number(boundingClientRect.left.toFixed(2)),
|
|
1566
|
+
width: Number(boundingClientRect.width.toFixed(2)),
|
|
1567
|
+
height: Number(boundingClientRect.height.toFixed(2))
|
|
1568
|
+
};
|
|
1569
|
+
mapOfBoundRect && mapOfBoundRect.set(element, rect);
|
|
1570
|
+
}
|
|
1571
|
+
return rect;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Get the candidates which is fully inside the target element in visual
|
|
1576
|
+
* @param {Node} targetElement
|
|
1577
|
+
* @returns {sequence<Node>} overlappedCandidates
|
|
1578
|
+
*/
|
|
1579
|
+
function getOverlappedCandidates(targetElement) {
|
|
1580
|
+
const container = targetElement.getSpatialNavigationContainer();
|
|
1581
|
+
const candidates = container.focusableAreas();
|
|
1582
|
+
const overlappedCandidates = [];
|
|
1583
|
+
|
|
1584
|
+
candidates.forEach(element => {
|
|
1585
|
+
if ((targetElement !== element) && isEntirelyVisible(element, targetElement)) {
|
|
1586
|
+
overlappedCandidates.push(element);
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
return overlappedCandidates;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Get the list of the experimental APIs
|
|
1595
|
+
* @function getExperimentalAPI
|
|
1596
|
+
*/
|
|
1597
|
+
function getExperimentalAPI() {
|
|
1598
|
+
function canScroll(container, dir) {
|
|
1599
|
+
return (isScrollable(container, dir) && !isScrollBoundary(container, dir)) ||
|
|
1600
|
+
(!container.parentElement && !isHTMLScrollBoundary(container, dir));
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function findTarget(findCandidate, element, dir, option) {
|
|
1604
|
+
let eventTarget = element;
|
|
1605
|
+
let bestNextTarget = null;
|
|
1606
|
+
|
|
1607
|
+
// 4
|
|
1608
|
+
if (eventTarget === document || eventTarget === document.documentElement) {
|
|
1609
|
+
eventTarget = document.body || document.documentElement;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// 5
|
|
1613
|
+
// At this point, spatialNavigationSearch can be applied.
|
|
1614
|
+
// If startingPoint is either a scroll container or the document,
|
|
1615
|
+
// find the best candidate within startingPoint
|
|
1616
|
+
if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) {
|
|
1617
|
+
if (eventTarget.nodeName === 'IFRAME')
|
|
1618
|
+
eventTarget = eventTarget.contentDocument.body;
|
|
1619
|
+
|
|
1620
|
+
const candidates = getSpatialNavigationCandidates(eventTarget, option);
|
|
1621
|
+
|
|
1622
|
+
// 5-2
|
|
1623
|
+
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
1624
|
+
return findCandidate ? getFilteredSpatialNavigationCandidates(eventTarget, dir, candidates) : eventTarget.spatialNavigationSearch(dir, {candidates});
|
|
1625
|
+
}
|
|
1626
|
+
if (canScroll(eventTarget, dir)) {
|
|
1627
|
+
return findCandidate ? [] : eventTarget;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// 6
|
|
1632
|
+
// Let container be the nearest ancestor of eventTarget
|
|
1633
|
+
let container = eventTarget.getSpatialNavigationContainer();
|
|
1634
|
+
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
|
|
1635
|
+
|
|
1636
|
+
// When the container is the viewport of a browsing context
|
|
1637
|
+
if (!parentContainer && ( window.location !== window.parent.location)) {
|
|
1638
|
+
parentContainer = window.parent.document.documentElement;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// 7
|
|
1642
|
+
while (parentContainer) {
|
|
1643
|
+
const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container);
|
|
1644
|
+
|
|
1645
|
+
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
1646
|
+
bestNextTarget = eventTarget.spatialNavigationSearch(dir, {candidates, container});
|
|
1647
|
+
if (bestNextTarget) {
|
|
1648
|
+
return findCandidate ? candidates : bestNextTarget;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// If there isn't any candidate and the best candidate among candidate:
|
|
1653
|
+
// 1) Scroll or 2) Find candidates of the ancestor container
|
|
1654
|
+
// 8 - if
|
|
1655
|
+
else if (canScroll(container, dir)) {
|
|
1656
|
+
return findCandidate ? [] : eventTarget;
|
|
1657
|
+
} else if (container === document || container === document.documentElement) {
|
|
1658
|
+
container = window.document.documentElement;
|
|
1659
|
+
|
|
1660
|
+
// The page is in an iframe
|
|
1661
|
+
if ( window.location !== window.parent.location ) {
|
|
1662
|
+
// eventTarget needs to be reset because the position of the element in the IFRAME
|
|
1663
|
+
// is unuseful when the focus moves out of the iframe
|
|
1664
|
+
eventTarget = window.frameElement;
|
|
1665
|
+
container = window.parent.document.documentElement;
|
|
1666
|
+
if (container.parentElement)
|
|
1667
|
+
parentContainer = container.getSpatialNavigationContainer();
|
|
1668
|
+
else {
|
|
1669
|
+
parentContainer = null;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
} else {
|
|
1674
|
+
// avoiding when spatnav container with tabindex=-1
|
|
1675
|
+
if (isFocusable(container)) {
|
|
1676
|
+
eventTarget = container;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
container = parentContainer;
|
|
1680
|
+
if (container.parentElement)
|
|
1681
|
+
parentContainer = container.getSpatialNavigationContainer();
|
|
1682
|
+
else {
|
|
1683
|
+
parentContainer = null;
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (!parentContainer && container) {
|
|
1690
|
+
// Getting out from the current spatnav container
|
|
1691
|
+
const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container);
|
|
1692
|
+
|
|
1693
|
+
// 9
|
|
1694
|
+
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
1695
|
+
bestNextTarget = eventTarget.spatialNavigationSearch(dir, {candidates, container});
|
|
1696
|
+
if (bestNextTarget) {
|
|
1697
|
+
return findCandidate ? candidates : bestNextTarget;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (canScroll(container, dir)) {
|
|
1703
|
+
bestNextTarget = eventTarget;
|
|
1704
|
+
return bestNextTarget;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
return {
|
|
1709
|
+
isContainer,
|
|
1710
|
+
isScrollContainer,
|
|
1711
|
+
isVisibleInScroller,
|
|
1712
|
+
findCandidates: findTarget.bind(null, true),
|
|
1713
|
+
findNextTarget: findTarget.bind(null, false),
|
|
1714
|
+
getDistanceFromTarget: (element, candidateElement, dir) => {
|
|
1715
|
+
if ((isContainer(element) || element.nodeName === 'BODY') && !(element.nodeName === 'INPUT')) {
|
|
1716
|
+
if (getSpatialNavigationCandidates(element).includes(candidateElement)) {
|
|
1717
|
+
return getInnerDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return getDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Makes to use the experimental APIs.
|
|
1727
|
+
* @function enableExperimentalAPIs
|
|
1728
|
+
* @param option {boolean} - If it is true, the experimental APIs can be used or it cannot.
|
|
1729
|
+
*/
|
|
1730
|
+
function enableExperimentalAPIs (option) {
|
|
1731
|
+
const currentKeyMode = window.__spatialNavigation__ && window.__spatialNavigation__.keyMode;
|
|
1732
|
+
window.__spatialNavigation__ = (option === false) ? getInitialAPIs() : Object.assign(getInitialAPIs(), getExperimentalAPI());
|
|
1733
|
+
window.__spatialNavigation__.keyMode = currentKeyMode;
|
|
1734
|
+
Object.seal(window.__spatialNavigation__);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Set the environment for using the spatial navigation polyfill.
|
|
1739
|
+
* @function getInitialAPIs
|
|
1740
|
+
*/
|
|
1741
|
+
function getInitialAPIs() {
|
|
1742
|
+
return {
|
|
1743
|
+
enableExperimentalAPIs,
|
|
1744
|
+
get keyMode() { return this._keymode ? this._keymode : 'ARROW'; },
|
|
1745
|
+
set keyMode(mode) { this._keymode = (['SHIFTARROW', 'ARROW', 'NONE'].includes(mode)) ? mode : 'ARROW'; },
|
|
1746
|
+
setStartingPoint: function (x, y) {startingPoint = (x && y) ? {x, y} : null;}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
initiateSpatialNavigation();
|
|
1751
|
+
enableExperimentalAPIs(false);
|
|
1752
|
+
|
|
1753
|
+
window.addEventListener('load', () => {
|
|
1754
|
+
spatialNavigationHandler();
|
|
1755
|
+
});
|
|
1756
|
+
})();
|