PageSourceSearch

https://paracosm.pages.dev/js/polyfill.js

js paracosm.pages.dev collected 2026-10-03 05:30:14 UTC 73,510 bytes, 1,749 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.