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.
@@ -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
+ })();