UNPKG

spatial-navigation-polyfill

Version:
1,630 lines 67.5 kB
/* Spatial Navigation Polyfill
 *
 * It follows W3C official specification
 * https://drafts.csswg.org/css-nav-1/
 *
 * Copyright (c) 2018-2019 LG Electronics Inc.
 * https://github.com/WICG/spatial-navigation/polyfill
 *
 * Licensed under the MIT license (MIT)
 */


(function () {
  // For chrome extension.
  let simulationElement = null;
  let simulationBestTarget = null;
  let simulationCandidates = null;

  // Turn on/off interest
  const focuslessSpatialNavigation = false;
  let _interestElement = null;

  const createInterestEvents = function (eventType, target) {
    if(target && ['interestblur', 'interest'].includes(eventType)) {
      const triggeredEvent = new CustomEvent(eventType, {bubbles: false, cancelable: false});
      return target.dispatchEvent(triggeredEvent);
    }
  };

  const currentInterest = function () {
    // get current interest element.
    return _interestElement;
  };

  const interest = function (element) {
    // Assign interest to element.
    const prevInterest = currentInterest();
    if (prevInterest) {
      prevInterest.classList.remove('interest');
      createInterestEvents('interestblur', prevInterest);
    }

    _interestElement = element;

    if (element) {
      document.activeElement.blur();
      _interestElement.classList.add('interest');
      createInterestEvents('interest', _interestElement);
    }
  };

  if (focuslessSpatialNavigation) {
    window.addEventListener('load', () => {
      const style = document.createElement('style');
      style.type = 'text/css';
      style.innerHTML = '.interest { outline: 3px solid skyblue; }';
      document.getElementsByTagName('head')[0].appendChild(style);

    });
  }

  // The polyfill must not be executed, if it's already enabled via browser engine or browser extensions.
  if (window.navigate !== undefined) {
    return;
  }

  const ARROW_KEY_CODE = {37: 'left', 38: 'up', 39: 'right', 40: 'down'};
  const INTEREST_KEY_CODE = {13: 'enter', 27: 'esc'};
  const TAB_KEY_CODE = 9;
  let mapOfBoundRect = null;
  let mapOfVisible = null;
  let startingPoint = null; // Indicates global variables for spatnav (starting position)

  /**
   * Initiate the spatial navigation features of the polyfill.
   * This function defines which input methods trigger the spatial navigation behavior.
   * @function initiateSpatialNavigation
   */
  function initiateSpatialNavigation() {
    /*
     * Bind the standards APIs to be exposed to the window object for authors
     */
    window.navigate = navigate;
    window.Element.prototype.spatialNavigationSearch = spatialNavigationSearch;
    window.Element.prototype.focusableAreas = focusableAreas;
    window.Element.prototype.getSpatialNavigationContainer = getSpatialNavigationContainer;

    /*
     * CSS.registerProperty() from the Properties and Values API
     * Reference: https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function
     */
    if (window.CSS && CSS.registerProperty) {
      if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-contain') === '') {
        CSS.registerProperty({
          name: '--spatial-navigation-contain',
          syntax: 'auto | contain | delegable',
          inherits: false,
          initialValue: 'auto'
        });
      }

      if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-action') === '') {
        CSS.registerProperty({
          name: '--spatial-navigation-action',
          syntax: 'auto | focus | scroll',
          inherits: false,
          initialValue: 'auto'
        });
      }

      if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-function') === '') {
        CSS.registerProperty({
          name: '--spatial-navigation-function',
          syntax: 'normal | euclidean | grid',
          inherits: true,
          initialValue: 'normal'
        });
      }
    }

    /*
     * keydown EventListener :
     * If arrow key pressed, get the next focusing element and send it to focusing controller
     */
    window.addEventListener('keydown', e => {
      const currentKeyMode = (parent && parent.__spatialNavigation__.keyMode) || window.__spatialNavigation__.keyMode;
      const interestElement = focuslessSpatialNavigation ? currentInterest() : null;
      const eventTarget = focuslessSpatialNavigation && interestElement ? interestElement : document.activeElement;
      const dir = ARROW_KEY_CODE[e.keyCode];
      const interestKey = INTEREST_KEY_CODE[e.keyCode];

      if (e.keyCode === TAB_KEY_CODE) {
        if (focuslessSpatialNavigation) {
          interest(null); // remove interest
          eventTarget.focus();
        }
        startingPoint = null;
      }

      if (!currentKeyMode ||
          (currentKeyMode === 'NONE') ||
          ((currentKeyMode === 'SHIFTARROW') && !e.shiftKey) ||
          ((currentKeyMode === 'ARROW') && e.shiftKey))
        return;

      if (!e.defaultPrevented) {
        let focusNavigableArrowKey = {left: true, up: true, right: true, down: true};

        if(focuslessSpatialNavigation && interestKey === 'enter' && e.isTrusted) {
          interest(null);
          eventTarget.focus();
          let keyDownEvent = new KeyboardEvent('keydown', {
            bubbles: true, cancelable: true, keyCode: 13
          });
          eventTarget.dispatchEvent(keyDownEvent);

          // Edge case
          if (eventTarget.type === 'checkbox') {
            eventTarget.checked = !eventTarget.checked;
          } else if (eventTarget.type === 'radio') {
            eventTarget.checked = true;
          }
          return;
        } else if (focuslessSpatialNavigation && interestKey === 'esc' && e.isTrusted) {
          interest(eventTarget);
          return;
        }

        // Edge case (text input, area) : Don't move focus, just navigate cursor in text area
        if (eventTarget.nodeName === 'INPUT' || eventTarget.nodeName === 'TEXTAREA') {
          if (!focuslessSpatialNavigation || currentInterest() === null) {
            // Input element have focus.
            focusNavigableArrowKey = handlingEditableElement(e);
          }
        }

        if (focusNavigableArrowKey[dir]) {
          e.preventDefault();
          mapOfBoundRect = new Map();
          mapOfVisible = new Map();

          navigate(dir);

          mapOfBoundRect = null;
          mapOfVisible = null;
          startingPoint = null;
        }
      }
    });

    /*
     * mouseup EventListener :
     * If the mouse click a point in the page, the point will be the starting point.
     * NOTE: Let UA set the spatial navigation starting point based on click
     */
    document.addEventListener('mouseup', e => {
      startingPoint = {x: e.clientX, y: e.clientY};
      if (focuslessSpatialNavigation) {
        interest(null);
      }
    });
  }

  /**
   * Enable the author to trigger spatial navigation programmatically, as if the user had done so manually.
   * @see {@link https://drafts.csswg.org/css-nav-1/#dom-window-navigate}
   * @function navigate
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   */
  function navigate(dir) {
    // spatial navigation steps

    // 1
    const searchOrigin = findSearchOrigin();
    let eventTarget = searchOrigin;

    let elementFromPosition = null;

    // 2 Optional step, UA defined starting point
    if (startingPoint) {
      // if there is a starting point, set eventTarget as the element from position for getting the spatnav container
      elementFromPosition = document.elementFromPoint(startingPoint.x, startingPoint.y);

      // Use starting point if the starting point isn't inside the focusable element (but not container)
      // * Starting point is meaningfull when:
      // 1) starting point is inside the spatnav container
      // 2) starting point is inside the non-focusable element
      if(elementFromPosition === null) {
        elementFromPosition = document.body;
      }
      if (isFocusable(elementFromPosition) && !isContainer(elementFromPosition)) {
        startingPoint = null;
      } else if (isContainer(elementFromPosition)) {
        eventTarget = elementFromPosition;
      } else {
        eventTarget = elementFromPosition.getSpatialNavigationContainer();
      }
    }

    // 4
    if (eventTarget === document || eventTarget === document.documentElement) {
      eventTarget = document.body || document.documentElement;
    }

    if (simulationElement) {
      eventTarget = simulationElement;
    }

    // 5
    // At this point, spatialNavigationSearch can be applied.
    // If startingPoint is either a scroll container or the document,
    // find the best candidate within startingPoint
    let container = null;
    if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) {
      container = eventTarget;

      if (eventTarget.nodeName === 'IFRAME')
        eventTarget = eventTarget.contentDocument.body;

      // 5-2
      if (getCSSSpatNavAction(eventTarget) === 'scroll') {
        if (scrollingController(eventTarget, dir)) return;
      } else if (getCSSSpatNavAction(eventTarget) === 'focus') {
        if (focusingController(eventTarget.spatialNavigationSearch(dir, {candidates: getSpatialNavigationCandidates(eventTarget, {mode: 'all'}), inside: true}), dir)) return;
      } else if (getCSSSpatNavAction(eventTarget) === 'auto') {
        if (focusingController(eventTarget.spatialNavigationSearch(dir, {inside: true}), dir)) return;
        if (scrollingController(eventTarget, dir)) return;
      }
    }

    // 6
    // Let container be the nearest ancestor of eventTarget
    container = container || eventTarget.getSpatialNavigationContainer();
    let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;

    // When the container is the viewport of a browsing context
    if (!parentContainer && ( window.location !== window.parent.location)) {
      parentContainer = window.parent.document.documentElement;
    }

    if (getCSSSpatNavAction(eventTarget) === 'scroll') {
      if (scrollingController(container, dir)) return;
    }
    else if (getCSSSpatNavAction(eventTarget) === 'focus') {
      navigateChain(eventTarget, container, parentContainer, dir, 'all');
    }
    else if (getCSSSpatNavAction(eventTarget) === 'auto') {
      navigateChain(eventTarget, container, parentContainer, dir, 'visible');
    }
  }

  /**
   * Move the focus to the best candidate or do nothing.
   * @function focusingController
   * @param bestCandidate {Node} - The best candidate of the spatial navigation
   * @param dir {SpatialNavigationDirection}- The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function focusingController(bestCandidate, dir) {
    // 10 & 11
    // When bestCandidate is found
    if (bestCandidate) {
      const container = bestCandidate.getSpatialNavigationContainer();

      // Scrolling container or document when the next focusing element isn't entirely visible
      if (isScrollContainer(container) && !isEntirelyVisible(bestCandidate))
        bestCandidate.scrollIntoView();

      // When bestCandidate is a focusable element and not a container : move focus
      /*
       * [event] navbeforefocus : Fired before spatial or sequential navigation changes the focus.
       */
      if (!createSpatNavEvents('beforefocus', bestCandidate, null, dir))
        return true;

      if (simulationElement) {
        simulationBestTarget = bestCandidate;
      } else {
        focuslessSpatialNavigation ? interest(bestCandidate) : bestCandidate.focus();
      }
      return true;
    }

    // When bestCandidate is not found within the scrollport of a container: Nothing
    return false;
  }

  /**
   * Directionally scroll the scrollable spatial navigation container if it can be manually scrolled more.
   * @function scrollingController
   * @param container {Node} - The spatial navigation container which can scroll
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function scrollingController(container, dir) {

    // If there is any scrollable area among parent elements and it can be manually scrolled, scroll the document
    if (isScrollable(container, dir) && !isScrollBoundary(container, dir)) {
      if(!simulationElement) {
        moveScroll(container, dir);
      }
      return true;
    }

    // If the spatnav container is document and it can be scrolled, scroll the document
    if (!container.parentElement && !isHTMLScrollBoundary(container, dir)) {
      if(!simulationElement) {
        moveScroll(document.documentElement, dir);
      }
      return true;
    }
    return false;
  }

  /**
   * Find the candidates within a spatial navigation container include delegable container.
   * This function does not search inside delegable container or focusable container.
   * In other words, this return candidates set is not included focusable elements inside delegable container or focusable container.
   *
   * @function getSpatialNavigationCandidates
   * @param container {Node} - The spatial navigation container
   * @param option {FocusableAreasOptions} - 'mode' attribute takes visible' or 'all' for searching the boundary of focusable elements.
   *                                          Default value is 'visible'.
   * @returns {sequence<Node>} candidate elements within the container
   */
  function getSpatialNavigationCandidates (container, option = {mode: 'visible'}) {
    let candidates = [];

    if (container.childElementCount > 0) {
      if (!container.parentElement) {
        container = container.getElementsByTagName('body')[0] || document.body;
      }
      const children = container.children;
      for (const elem of children) {
        if (isDelegableContainer(elem)) {
          candidates.push(elem);
        } else if(isFocusable(elem)) {
          candidates.push(elem);

          if(!isContainer(elem) && elem.childElementCount) {
            candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
          }
        } else if (elem.childElementCount) {
          candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
        }
      }
    }
    return (option.mode === 'all') ? candidates : candidates.filter(isVisible);
  }

  /**
   * Find the candidates among focusable elements within a spatial navigation container from the search origin (currently focused element)
   * depending on the directional information.
   * @function getFilteredSpatialNavigationCandidates
   * @param element {Node} - The currently focused element which is defined as 'search origin' in the spec
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @param candidates {sequence<Node>} - The candidates for spatial navigation without the directional information
   * @param container {Node} - The spatial navigation container
   * @returns {Node} The candidates for spatial navigation considering the directional information
   */
  function getFilteredSpatialNavigationCandidates (element, dir, candidates, container) {
    const targetElement = element;
    // Removed below line due to a bug. (iframe body rect is sometime weird.)
    // const targetElement = (element.nodeName === 'IFRAME') ? element.contentDocument.body : element;
    // If the container is unknown, get the closest container from the element
    container = container || targetElement.getSpatialNavigationContainer();

    // If the candidates is unknown, find candidates
    // 5-1
    candidates = (!candidates || candidates.length <= 0) ? getSpatialNavigationCandidates(container) : candidates;
    return filteredCandidates(targetElement, candidates, dir, container);
  }

  /**
   * Find the best candidate among the candidates within the container from the search origin (currently focused element)
   * @see {@link https://wicg.github.io/spatial-navigation/#js-api}
   * @function spatialNavigationSearch
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @param candidates {sequence<Node>} - The candidates for spatial navigation
   * @param container {Node} - The spatial navigation container
   * @param inside {boolean} - Whether candidates should be elements outside of the target element or not.
   * @returns {Node} The best candidate which will gain the focus
   */
  function spatialNavigationSearch (dir, args) {
    const targetElement = this;
    let internalCandidates = [];
    let externalCandidates = [];
    let bestTarget;

    // Set default parameter value
    if (!args)
      args = {};
    const container = args.container || targetElement.getSpatialNavigationContainer();
    const candidates = (args.candidates && args.candidates.length > 0) ?
      args.candidates.filter((candidate) => container.contains(candidate)) : getSpatialNavigationCandidates(container);
    const inside = args.inside || false;

    // Find the best candidate
    // 5
    // If startingPoint is either a scroll container or the document,
    // find the best candidate within startingPoint
    if (candidates && candidates.length > 0) {

      // Divide internal or external candidates
      candidates.forEach(candidate => {
        (targetElement.contains(candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate);
      });
      if (!(args.candidates && args.candidates.length > 0) && (internalCandidates.length === 0)) {
        // If targetElement is focusable container or delegable container,
        // getFilteredSpatialNavigationCandidates not return internal candidate.
        internalCandidates = getSpatialNavigationCandidates(targetElement);
      }
      // Filter external Candidates
      if (externalCandidates.length > 0) {
        externalCandidates = getFilteredSpatialNavigationCandidates (targetElement, dir, externalCandidates, container);
      }

      if (inside && (isContainer(targetElement) || targetElement.nodeName === 'BODY') && !(targetElement.nodeName === 'INPUT')) {
        simulationCandidates = internalCandidates;
        bestTarget = selectBestCandidateFromEdge(targetElement, internalCandidates, dir);
      }
      if (!bestTarget) {
        simulationCandidates = externalCandidates;
        bestTarget = selectBestCandidate(targetElement, externalCandidates, dir);
      }

      if (bestTarget && isDelegableContainer(bestTarget)) {
        // if best target is delegable container, then find descendants candidate inside delegable container.
        const innerTarget = getSpatialNavigationCandidates(bestTarget, {mode: 'all'});
        const descendantsBest = innerTarget.length > 0 ? targetElement.spatialNavigationSearch(dir, {candidates: innerTarget, container: bestTarget}) : null;
        simulationCandidates = innerTarget;
        if (descendantsBest) {
          bestTarget = descendantsBest;
        } else if (!isFocusable(bestTarget)) {
          // if there is no target inside bestTarget and delegable container is not focusable,
          // then try to find another best target without curren best target.
          candidates.splice(candidates.indexOf(bestTarget), 1);
          bestTarget = candidates.length ? targetElement.spatialNavigationSearch(dir, {candidates: candidates, container: container}) : null;
          simulationCandidates = candidates;
        }
      }
      return bestTarget;
    }

    return null;
  }

  /**
   * Get the filtered candidate among candidates.
   * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
   * @function filteredCandidates
   * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
   * @param candidates {sequence<Node>} - The candidates for spatial navigation
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @param container {Node} - The spatial navigation container
   * @returns {sequence<Node>} The filtered candidates which are not the search origin and not in the given spatial navigation direction from the search origin
   */
  function filteredCandidates(currentElm, candidates, dir, container) {
    const originalContainer = currentElm.getSpatialNavigationContainer();
    let eventTargetRect;

    // If D(dir) is null, let candidates be the same as visibles
    if (dir === undefined)
      return candidates;

    // to do
    // Offscreen handling when originalContainer is not <HTML>
    if (originalContainer.parentElement && container !== originalContainer && !isVisible(currentElm))
      eventTargetRect = getBoundingClientRect(originalContainer);
    else eventTargetRect = getBoundingClientRect(currentElm);

    /*
     * Else, let candidates be the subset of the elements in visibles
     * whose principal box’s geometric center is within the closed half plane
     * whose boundary goes through the geometric center of starting point and is perpendicular to D.
     */
    if ((isContainer(currentElm) || currentElm.nodeName === 'BODY') && !(currentElm.nodeName === 'INPUT')) {
      return candidates.filter(candidate => {
        const candidateRect = getBoundingClientRect(candidate);
        return container.contains(candidate) &&
          ((currentElm.contains(candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm) ||
          isOutside(candidateRect, eventTargetRect, dir));
      });
    } else {
      return candidates.filter(candidate => {
        const candidateRect = getBoundingClientRect(candidate);
        const candidateBody = (candidate.nodeName === 'IFRAME') ? (candidate.contentDocument && candidate.contentDocument.body) : null;
        return container.contains(candidate) &&
          candidate !== currentElm && candidateBody !== currentElm &&
          isOutside(candidateRect, eventTargetRect, dir);
      });
    }
  }

  /**
   * Select the best candidate among given candidates.
   * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
   * @function selectBestCandidate
   * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
   * @param candidates {sequence<Node>} - The candidates for spatial navigation
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Node} The best candidate which will gain the focus
   */
  function selectBestCandidate(currentElm, candidates, dir) {
    const spatialNavigationFunction = getComputedStyle(currentElm).getPropertyValue('--spatial-navigation-function');
    const currentElmRect = getBoundingClientRect(currentElm);
    let distanceFunction;
    let alignedCandidates;

    switch (spatialNavigationFunction) {
    case 'grid':
      alignedCandidates = candidates.filter(elm => isAligned(currentElmRect, getBoundingClientRect(elm), dir));
      if (alignedCandidates.length > 0) {
        candidates = alignedCandidates;
      }
      distanceFunction = getAbsoluteDistance;
      break;
    case 'euclidean':
      distanceFunction = getEuclideanDistance;
      break;
    default:
      distanceFunction = getDistance;
      break;
    }
    return getClosestElement(currentElm, candidates, dir, distanceFunction);
  }

  /**
   * Select the best candidate among candidates by finding the closet candidate from the edge of the currently focused element (search origin).
   * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate (Step 5)}
   * @function selectBestCandidateFromEdge
   * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
   * @param candidates {sequence<Node>} - The candidates for spatial navigation
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Node} The best candidate which will gain the focus
   */
  function selectBestCandidateFromEdge(currentElm, candidates, dir) {
    if (startingPoint)
      return getClosestElement(currentElm, candidates, dir, getDistanceFromPoint);
    else
      return getClosestElement(currentElm, candidates, dir, getInnerDistance);
  }

  /**
   * Select the closest candidate from the currently focused element (search origin) among candidates by using the distance function.
   * @function getClosestElement
   * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
   * @param candidates {sequence<Node>} - The candidates for spatial navigation
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @param distanceFunction {function} - The distance function which measures the distance from the search origin to each candidate
   * @returns {Node} The candidate which is the closest one from the search origin
   */
  function getClosestElement(currentElm, candidates, dir, distanceFunction) {
    const eventTargetRect = getBoundingClientRect(currentElm);
    let minDistance = Number.POSITIVE_INFINITY;
    let minDistanceElements = [];

    if (candidates) {
      for (let i = 0; i < candidates.length; i++) {
        const distance = distanceFunction(eventTargetRect, getBoundingClientRect(candidates[i]), dir);

        // If the same distance, the candidate will be selected in the DOM order
        if (distance < minDistance) {
          minDistance = distance;
          minDistanceElements = [candidates[i]];
        } else if (distance === minDistance) {
          minDistanceElements.push(candidates[i]);
        }
      }
    }
    if (minDistanceElements.length === 0)
      return null;

    return (minDistanceElements.length > 1 && distanceFunction === getAbsoluteDistance) ?
      getClosestElement(currentElm, minDistanceElements, dir, getEuclideanDistance) : minDistanceElements[0];
  }

  /**
   * Get container of an element.
   * @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-getspatialnavigationcontainer}
   * @module Element
   * @function getSpatialNavigationContainer
   * @returns {Node} The spatial navigation container
   */
  function getSpatialNavigationContainer() {
    let container = this;

    do {
      if (!container.parentElement) {
        if (window.location !== window.parent.location)
          container = window.parent.document.documentElement;
        else
          container = window.document.documentElement;
        break;
      }
      else {
        container = container.parentElement;
      }
    } while (!isContainer(container));
    return container;
  }

  /**
   * Find focusable elements within the spatial navigation container.
   * @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-focusableareas}
   * @function focusableAreas
   * @param option {FocusableAreasOptions} - 'mode' attribute takes visible' or 'all' for searching the boundary of focusable elements.
   *                                          Default value is 'visible'.
   * @returns {sequence<Node>} All focusable elements or only visible focusable elements within the container
   */
  function focusableAreas(option = {mode: 'visible'}) {
    const container = this.parentElement ? this : document.body;
    const focusables = Array.prototype.filter.call(container.getElementsByTagName('*'), isFocusable);
    return (option.mode === 'all') ? focusables : focusables.filter(isVisible);
  }

  /**
   * Create the NavigatoinEvent: navbeforefocus, navnotarget
   * @see {@link https://drafts.csswg.org/css-nav-1/#events-navigationevent}
   * @function createSpatNavEvents
   * @param option {string} - Type of the navigation event (beforefocus, notarget)
   * @param element {Node} - The target element of the event
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   */
  function createSpatNavEvents(eventType, containerElement, currentElement, direction) {
    if(['beforefocus', 'notarget'].includes(eventType)) {
      const data = {
        causedTarget: currentElement,
        dir: direction
      };
      const triggeredEvent = new CustomEvent('nav' + eventType, {bubbles: true, cancelable: true, detail: data});
      return containerElement.dispatchEvent(triggeredEvent);
    }
  }

  /**
   * Get the value of the CSS custom property of the element
   * @function readCssVar
   * @param element {Node}
   * @param varName {string} - The name of the css custom property without '--'
   * @returns {string} The value of the css custom property
   */
  function readCssVar(element, varName) {
    return element.style.getPropertyValue(`--${varName}`).trim();
  }

  /**
   * Decide whether or not the 'contain' value is given to 'spatial-navigation-contain' css property of an element
   * @function isCSSSpatNavContain
   * @param element {Node}
   * @returns {boolean}
   */
  function isCSSSpatNavContain(element) {
    const spatialNavigationCSS = readCssVar(element, 'spatial-navigation-contain');
    return spatialNavigationCSS === 'contain' || spatialNavigationCSS === 'delegable';
  }

  /**
   * Return the value of 'spatial-navigation-action' css property of an element
   * @function getCSSSpatNavAction
   * @param element {Node} - would be the spatial navigation container
   * @returns {string} auto | focus | scroll
   */
  function getCSSSpatNavAction(element) {
    return readCssVar(element, 'spatial-navigation-action') || 'auto';
  }

  /**
   * Only move the focus with spatial navigation. Manually scrolling isn't available.
   * @function navigateChain
   * @param eventTarget {Node} - currently focused element
   * @param container {SpatialNavigationContainer} - container
   * @param parentContainer {SpatialNavigationContainer} - parent container
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   */
  function navigateChain(eventTarget, container, parentContainer, dir, option) {
    let currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};

    while (parentContainer) {
      if (focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) {
        return;
      }
      else {
        if ((option === 'visible') && scrollingController(container, dir)) return;
        else {
          if (!createSpatNavEvents('notarget', container, eventTarget, dir)) return;

          // find the container
          if (container === document || container === document.documentElement) {
            if ( window.location !== window.parent.location ) {
              // The page is in an iframe. eventTarget needs to be reset because the position of the element in the iframe
              eventTarget = window.frameElement;
              container = window.parent.document.documentElement;

              if (container.parentElement)
                parentContainer = container.getSpatialNavigationContainer();
              else {
                parentContainer = null;
                break;
              }
            }
          }
          else {
            container = parentContainer;
            currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};

            if (container.parentElement)
              parentContainer = container.getSpatialNavigationContainer();
            else {
              parentContainer = null;
              break;
            }
          }
        }
      }
    }

    currentOption = {candidates: getSpatialNavigationCandidates(container, {mode: option}), container};

    // Behavior after 'navnotarget' - Getting out from the current spatnav container
    if ((!parentContainer && container) && focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) return;

    if (!createSpatNavEvents('notarget', currentOption.container, eventTarget, dir)) return;

    if ((getCSSSpatNavAction(container) === 'auto') && (option === 'visible'))
      if (scrollingController(container, dir)) return;
  }

  /**
   * Find search origin
   * @see {@link https://drafts.csswg.org/css-nav-1/#nav}
   * @function findSearchOrigin
   * @returns {Node} The search origin for the spatial navigation
   */
  function findSearchOrigin() {
    const interestElement = focuslessSpatialNavigation ? currentInterest() : null;
    let searchOrigin = focuslessSpatialNavigation && interestElement ? interestElement : document.activeElement;

    if (!searchOrigin ||
      (searchOrigin === document.body && !document.querySelector(':focus')) /* body isn't actually focused*/
    ) {
      searchOrigin = document;
    }
    return searchOrigin;
  }

  /**
   * Move the scroll of an element depending on the given spatial navigation directrion
   * (Assume that User Agent defined distance is '40px')
   * @see {@link https://drafts.csswg.org/css-nav-1/#directionally-scroll-an-element}
   * @function moveScroll
   * @param element {Node} - The scrollable element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @param offset {Number} - The explicit amount of offset for scrolling. Default value is 0.
   */
  function moveScroll(element, dir, offset = 0) {
    if (element) {
      switch (dir) {
      case 'left': element.scrollLeft -= (40 + offset); break;
      case 'right': element.scrollLeft += (40 + offset); break;
      case 'up': element.scrollTop -= (40 + offset); break;
      case 'down': element.scrollTop += (40 + offset); break;
      }
    }
  }

  /**
   * Decide whether an element is container or not.
   * @function isContainer
   * @param element {Node} element
   * @returns {boolean}
   */
  function isContainer(element) {
    return (!element.parentElement) ||
            (element.nodeName === 'IFRAME') ||
            (isScrollContainer(element)) ||
            (isCSSSpatNavContain(element));
  }

  /**
   * Decide whether an element is delegable container or not.
   * @function isDelegableContainer
   * @param element {Node} element
   * @returns {boolean}
   */
  function isDelegableContainer(element) {
    return readCssVar(element, 'spatial-navigation-contain') === 'delegable';
  }

  /**
   * Decide whether an element is a scrollable container or not.
   * @see {@link https://drafts.csswg.org/css-overflow-3/#scroll-container}
   * @function isScrollContainer
   * @param element {Node}
   * @returns {boolean}
   */
  function isScrollContainer(element) {
    const elementStyle = window.getComputedStyle(element, null);
    const overflowX = elementStyle.getPropertyValue('overflow-x');
    const overflowY = elementStyle.getPropertyValue('overflow-y');
    return (overflowX !== 'visible' && overflowX !== 'clip') && (overflowY !== 'visible' && overflowY !== 'clip');
  }

  /**
   * Decide whether this element is scrollable or not.
   * @function isScrollable
   * @param element {Node}
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function isScrollable(element, dir) { // element, dir
    if (element && typeof element === 'object') {
      if (dir && typeof dir === 'string') { // parameter: dir, element
        if (isOverflow(element, dir)) {
          // style property
          const elementStyle = window.getComputedStyle(element, null);
          const overflowX = elementStyle.getPropertyValue('overflow-x');
          const overflowY = elementStyle.getPropertyValue('overflow-y');

          switch (dir) {
          case 'left':
            /* falls through */
          case 'right':
            return (overflowX !== 'visible' && overflowX !== 'clip');
          case 'up':
            /* falls through */
          case 'down':
            return (overflowY !== 'visible' && overflowY !== 'clip');
          }
        }
        return false;
      } else { // parameter: element
        return (element.nodeName === 'HTML' || element.nodeName === 'BODY') ||
                (isScrollContainer(element) && isOverflow(element));
      }
    }
  }

  /**
   * Decide whether an element is overflow or not.
   * @function isOverflow
   * @param element {Node}
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function isOverflow(element, dir) {
    if (element && typeof element === 'object') {
      if (dir && typeof dir === 'string') { // parameter: element, dir
        switch (dir) {
        case 'left':
          /* falls through */
        case 'right':
          return (element.scrollWidth > element.clientWidth);
        case 'up':
          /* falls through */
        case 'down':
          return (element.scrollHeight > element.clientHeight);
        }
      } else { // parameter: element
        return (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight);
      }
      return false;
    }
  }

  /**
   * Decide whether the scrollbar of the browsing context reaches to the end or not.
   * @function isHTMLScrollBoundary
   * @param element {Node} - The top browsing context
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function isHTMLScrollBoundary(element, dir) {
    let result = false;
    switch (dir) {
    case 'left':
      result = window.scrollX === 0;
      break;
    case 'right':
      result = (element.scrollWidth - element.scrollLeft - element.clientWidth) === 0;
      break;
    case 'up':
      result = window.scrollY === 0;
      break;
    case 'down':
      result = (element.scrollHeight - element.scrollTop - element.clientHeight) === 0;
      break;
    }
    return result;
  }

  /**
   * Decide whether the scrollbar of an element reaches to the end or not.
   * @function isScrollBoundary
   * @param element {Node}
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function isScrollBoundary(element, dir) {
    if (isScrollable(element, dir)) {
      const winScrollY = element.scrollTop;
      const winScrollX = element.scrollLeft;

      const height = element.scrollHeight - element.clientHeight;
      const width = element.scrollWidth - element.clientWidth;

      switch (dir) {
      case 'left': return (winScrollX === 0);
      case 'right': return (Math.abs(winScrollX - width) <= 1);
      case 'up': return (winScrollY === 0);
      case 'down': return (Math.abs(winScrollY - height) <= 1);
      }
    }
    return false;
  }

  /**
   * Decide whether an element is focusable for spatial navigation.
   * 1. If element is the browsing context (document, iframe), then it's focusable,
   * 2. If the element is scrollable container (regardless of scrollable axis), then it's focusable,
   * 3. The value of tabIndex >= 0, then it's focusable,
   * 4. If the element is disabled, it isn't focusable,
   * 5. If the element is expressly inert, it isn't focusable,
   * 6. Whether the element is being rendered or not.
   *
   * @function isFocusable
   * @param element {Node}
   * @returns {boolean}
   *
   * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#focusable-area}
   */
  function isFocusable(element) {
    if ((element.tabIndex < 0) || isAtagWithoutHref(element) || isActuallyDisabled(element) || isExpresslyInert(element) || !isBeingRendered(element))
      return false;
    else if ((!element.parentElement) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0))
      return true;
  }

  /**
   * Decide whether an element is a tag without href attribute or not.
   *
   * @function isAtagWithoutHref
   * @param element {Node}
   * @returns {boolean}
   */
  function isAtagWithoutHref(element) {
    return (element.tagName === 'A' && element.getAttribute('href') === null && element.getAttribute('tabIndex') === null);
  }

  /**
   * Decide whether an element is actually disabled or not.
   *
   * @function isActuallyDisabled
   * @param element {Node}
   * @returns {boolean}
   *
   * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
   */
  function isActuallyDisabled(element) {
    if (['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(element.tagName))
      return (element.disabled);
    else
      return false;
  }

  /**
   * Decide whether the element is expressly inert or not.
   * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
   * @function isExpresslyInert
   * @param element {Node}
   * @returns {boolean}
   */
  function isExpresslyInert(element) {
    return ((element.inert) && (!element.ownerDocument.documentElement.inert));
  }

  /**
   * Decide whether the element is being rendered or not.
   * 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
   * 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
   * 3. If width and height of an element are explicitly set to 0, it is not being rendered.
   * 4. If a parent element is hidden, an element itself is not being rendered.
   * (CSS visibility property and display property are inherited.)
   * @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
   * @function isBeingRendered
   * @param element {Node}
   * @returns {boolean}
   */
  function isBeingRendered(element) {
    if (!isVisibleStyleProperty(element.parentElement))
      return false;
    if (!isVisibleStyleProperty(element) || (element.style.opacity === 0) ||
        ((element.style.width === '0px' || element.style.width === 0) && (element.style.height === '0px' || element.style.height === 0)))
      return false;
    return true;
  }

  /**
   * Decide whether this element is partially or completely visible to user agent.
   * @function isVisible
   * @param element {Node}
   * @returns {boolean}
   */
  function isVisible(element) {
    // Memoization for performance
    let visible = mapOfVisible && mapOfVisible.get(element);
    if(typeof visible !== "boolean") {
      visible = (!element.parentElement) || (isVisibleStyleProperty(element) && hitTest(element));
      mapOfVisible && mapOfVisible.set(element, visible);
    }
    return visible;
  }

  /**
   * Decide whether this element is completely visible in this viewport for the arrow direction.
   * @function isEntirelyVisible
   * @param element {Node}
   * @returns {boolean}
   */
  function isEntirelyVisible(element) {
    const rect = getBoundingClientRect(element);
    const containerRect = getBoundingClientRect(element.getSpatialNavigationContainer());

    // FIXME: when element is bigger than container?
    const entirelyVisible = !((rect.left < containerRect.left) ||
      (rect.right > containerRect.right) ||
      (rect.top < containerRect.top) ||
      (rect.bottom > containerRect.bottom));

    return entirelyVisible;
  }

  /**
   * Decide the style property of this element is specified whether it's visible or not.
   * @function isVisibleStyleProperty
   * @param element {CSSStyleDeclaration}
   * @returns {boolean}
   */
  function isVisibleStyleProperty(element) {
    const elementStyle = window.getComputedStyle(element, null);
    const thisVisibility = elementStyle.getPropertyValue('visibility');
    const thisDisplay = elementStyle.getPropertyValue('display');
    const invisibleStyle = ['hidden', 'collapse'];

    return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility));
  }

  /**
   * Decide whether this element is entirely or partially visible within the viewport.
   * @function hitTest
   * @param element {Node}
   * @returns {boolean}
   */
  function hitTest(element) {
    const elementRect = getBoundingClientRect(element);
    if (element.nodeName !== 'IFRAME' && (elementRect.top < 0 || elementRect.left < 0 ||
      elementRect.top > window.innerHeight || elementRect.left > window.innerWidth))
      return false;

    let offsetX = parseInt(element.offsetWidth) / 10;
    let offsetY = parseInt(element.offsetHeight) / 10;

    offsetX = isNaN(offsetX) ? 1 : offsetX;
    offsetY = isNaN(offsetY) ? 1 : offsetY;

    const hitTestPoint = {
      middle: [(elementRect.left + elementRect.right) / 2, (elementRect.top + elementRect.bottom) / 2],
      leftTop: [elementRect.left + offsetX, elementRect.top + offsetY],
      // For performance
    //  leftBoottom: [elementRect.left + offsetX, elementRect.bottom - offsetY],
    //  rightTop: [elementRect.right - offsetX, elementRect.top + offsetY],
      rightBottom: [elementRect.right - offsetX, elementRect.bottom - offsetY]
    };

    for(const point in hitTestPoint) {
      const elemFromPoint = document.elementFromPoint(...hitTestPoint[point]);
      if (element === elemFromPoint || element.contains(elemFromPoint)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Decide whether a child element is entirely or partially Included within container visually.
   * @function isInside
   * @param containerRect {DOMRect}
   * @param childRect {DOMRect}
   * @returns {boolean}
   */
  function isInside(containerRect, childRect) {
    const rightEdgeCheck = (containerRect.left <= childRect.right && containerRect.right >= childRect.right);
    const leftEdgeCheck = (containerRect.left <= childRect.left && containerRect.right >= childRect.left);
    const topEdgeCheck = (containerRect.top <= childRect.top && containerRect.bottom >= childRect.top);
    const bottomEdgeCheck = (containerRect.top <= childRect.bottom && containerRect.bottom >= childRect.bottom);
    return (rightEdgeCheck || leftEdgeCheck) && (topEdgeCheck || bottomEdgeCheck);
  }

  /**
   * Decide whether this element is entirely or partially visible within the viewport.
   * Note: rect1 is outside of rect2 for the dir
   * @function isOutside
   * @param rect1 {DOMRect}
   * @param rect2 {DOMRect}
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {boolean}
   */
  function isOutside(rect1, rect2, dir) {
    switch (dir) {
    case 'left':
      return isRightSide(rect2, rect1);
    case 'right':
      return isRightSide(rect1, rect2);
    case 'up':
      return isBelow(rect2, rect1);
    case 'down':
      return isBelow(rect1, rect2);
    default:
      return false;
    }
  }

  /* rect1 is right of rect2 */
  function isRightSide(rect1, rect2) {
    return rect1.left >= rect2.right || (rect1.left >= rect2.left && rect1.right > rect2.right && rect1.bottom > rect2.top && rect1.top < rect2.bottom);
  }

  /* rect1 is below of rect2 */
  function isBelow(rect1, rect2) {
    return rect1.top >= rect2.bottom || (rect1.top >= rect2.top && rect1.bottom > rect2.bottom && rect1.left < rect2.right && rect1.right > rect2.left);
  }

  /* rect1 is completely aligned or partially aligned for the direction */
  function isAligned(rect1, rect2, dir) {
    switch (dir) {
    case 'left' :
      /* falls through */
    case 'right' :
      return rect1.bottom > rect2.top && rect1.top < rect2.bottom;
    case 'up' :
      /* falls through */
    case 'down' :
      return rect1.right > rect2.left && rect1.left < rect2.right;
    default:
      return false;
    }
  }

  /**
   * Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
   * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
   * @function getDistanceFromPoint
   * @param point {Point} - The search origin
   * @param element {DOMRect} - A candidate element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Number} The euclidian distance between the spatial navigation container and an element inside it
   */
  function getDistanceFromPoint(point, element, dir) {
    point = startingPoint;
    // Get exit point, entry point -> {x: '', y: ''};
    const points = getEntryAndExitPoints(dir, point, element);

    // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
    // that minimize the distance between these two points
    const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
    const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);

    // The result is euclidian distance between P1 and P2.
    return Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
  }

  /**
   * Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
   * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
   * @function getInnerDistance
   * @param rect1 {DOMRect} - The search origin
   * @param rect2 {DOMRect} - A candidate element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Number} The euclidean distance between the spatial navigation container and an element inside it
   */
  function getInnerDistance(rect1, rect2, dir) {
    const baseEdgeForEachDirection = {left: 'right', right: 'left', up: 'bottom', down: 'top'};
    const baseEdge = baseEdgeForEachDirection[dir];

    return Math.abs(rect1[baseEdge] - rect2[baseEdge]);
  }

  /**
   * Get the distance between the search origin and a candidate element considering the direction.
   * @see {@link https://drafts.csswg.org/css-nav-1/#calculating-the-distance}
   * @function getDistance
   * @param searchOrigin {DOMRect || Point} - The search origin
   * @param candidateRect {DOMRect} - A candidate element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Number} The distance scoring between two elements
   */
  function getDistance(searchOrigin, candidateRect, dir) {
    const kOrthogonalWeightForLeftRight = 30;
    const kOrthogonalWeightForUpDown = 2;

    let orthogonalBias = 0;
    let alignBias = 0;
    const alignWeight = 5.0;

    // Get exit point, entry point -> {x: '', y: ''};
    const points = getEntryAndExitPoints(dir, searchOrigin, candidateRect);

    // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
    // that minimize the distance between these two points
    const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
    const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);

    // A: The euclidean distance between P1 and P2.
    const A = Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
    let B, C;

    // B: The absolute distance in the direction which is orthogonal to dir between P1 and P2, or 0 if dir is null.
    // C: The intersection edges between a candidate and the starting point.

    // D: The square root of the area of intersection between the border boxes of candidate and starting point
    const intersectionRect = getIntersectionRect(searchOrigin, candidateRect);
    const D = intersectionRect.area;

    switch (dir) {
    case 'left':
      /* falls through */
    case 'right' :
      // If two elements are aligned, add align bias
      // else, add orthogonal bias
      if (isAligned(searchOrigin, candidateRect, dir))
        alignBias = Math.min(intersectionRect.height / searchOrigin.height , 1);
      else
        orthogonalBias = (searchOrigin.height / 2);

      B = (P2 + orthogonalBias) * kOrthogonalWeightForLeftRight;
      C = alignWeight * alignBias;
      break;

    case 'up' :
      /* falls through */
    case 'down' :
      // If two elements are aligned, add align bias
      // else, add orthogonal bias
      if (isAligned(searchOrigin, candidateRect, dir))
        alignBias = Math.min(intersectionRect.width / searchOrigin.width , 1);
      else
        orthogonalBias = (searchOrigin.width / 2);

      B = (P1 + orthogonalBias) * kOrthogonalWeightForUpDown;
      C = alignWeight * alignBias;
      break;

    default:
      B = 0;
      C = 0;
      break;
    }

    return (A + B - C - D);
  }

  /**
   * Get the euclidean distance between the search origin and a candidate element considering the direction.
   * @function getEuclideanDistance
   * @param rect1 {DOMRect} - The search origin
   * @param rect2 {DOMRect} - A candidate element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Number} The distance scoring between two elements
   */
  function getEuclideanDistance(rect1, rect2, dir) {
    // Get exit point, entry point
    const points = getEntryAndExitPoints(dir, rect1, rect2);

    // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
    // that minimize the distance between these two points
    const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
    const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);

    // Return the euclidean distance between P1 and P2.
    return Math.sqrt(Math.pow(P1, 2) + Math.pow(P2, 2));
  }

  /**
   * Get the absolute distance between the search origin and a candidate element considering the direction.
   * @function getAbsoluteDistance
   * @param rect1 {DOMRect} - The search origin
   * @param rect2 {DOMRect} - A candidate element
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
   * @returns {Number} The distance scoring between two elements
   */
  function getAbsoluteDistance(rect1, rect2, dir) {
    // Get exit point, entry point
    const points = getEntryAndExitPoints(dir, rect1, rect2);

    // Return the absolute distance in the dir direction between P1 and P.
    return ((dir === 'left') || (dir === 'right')) ?
      Math.abs(points.entryPoint.x - points.exitPoint.x) : Math.abs(points.entryPoint.y - points.exitPoint.y);
  }

  /**
   * Get entry point and exit point of two elements considering the direction.
   * @function getEntryAndExitPoints
   * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD). Default value for dir is 'down'.
   * @param searchOrigin {DOMRect | Point} - The search origin which contains the exit point
   * @param candidateRect {DOMRect} - One of candidates which contains the entry point
   * @returns {Points} The exit point from the search origin and the entry point from a candidate
   */
  function getEntryAndExitPoints(dir = 'down', searchOrigin, candidateRect) {
    /**
     * User type definition for Point
     * @typeof {Object} Points
     * @property {Point} Points.entryPoint
     * @property {Point} Points.exitPoint
     */
    const points = {entryPoint: {x: 0, y: 0}, exitPoint:{x: 0, y: 0}};

    if (startingPoint) {
      points.exitPoint = searchOrigin;

      switch (dir) {
      case 'left':
        points.entryPoint.x = candidateRect.right;
        break;
      case 'up':
        points.entryPoint.y = candidateRect.bottom;
        break;
      case 'right':
        points.entryPoint.x = candidateRect.left;
        break;
      case 'down':
        points.entryPoint.y = candidateRect.top;
        break;
      }

      // Set orthogonal direction
      switch (dir) {
      case 'left':
      case 'right':
        if (startingPoint.y <= candidateRect.top) {
          points.entryPoint.y = candidateRect.top;
        } else if (startingPoint.y < candidateRect.bottom) {
          points.entryPoint.y = startingPoint.y;
        } else {
          points.entryPoint.y = candidateRect.bottom;
        }
        break;

      case 'up':
      case 'down':
        if (startingPoint.x <= candidateRect.left) {
          points.entryPoint.x = candidateRect.left;
        } else if (startingPoint.x < candidateRect.right) {
          points.entryPoint.x = startingPoint.x;
        } else {
          points.entryPoint.x = candidateRect.right;
        }
        break;
      }
    }
    else {
      // Set direction
      switch (dir) {
      case 'left':
        points.exitPoint.x = searchOrigin.left;
        points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
        break;
      case 'up':
        points.exitPoint.y = searchOrigin.top;
        points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
        break;
      case 'right':
        points.exitPoint.x = searchOrigin.right;
        points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
        break;
      case 'down':
        points.exitPoint.y = searchOrigin.bottom;
        points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
        break;
      }

      // Set orthogonal direction
      switch (dir) {
      case 'left':
      case 'right':
        if (isBelow(searchOrigin, candidateRect)) {
          points.exitPoint.y = searchOrigin.top;
          points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
        } else if (isBelow(candidateRect, searchOrigin)) {
          points.exitPoint.y = searchOrigin.bottom;
          points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
        } else {
          points.exitPoint.y = Math.max(searchOrigin.top, candidateRect.top);
          points.entryPoint.y = points.exitPoint.y;
        }
        break;

      case 'up':
      case 'down':
        if (isRightSide(searchOrigin, candidateRect)) {
          points.exitPoint.x = searchOrigin.left;
          points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
        } else if (isRightSide(candidateRect, searchOrigin)) {
          points.exitPoint.x = searchOrigin.right;
          points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
        } else {
          points.exitPoint.x = Math.max(searchOrigin.left, candidateRect.left);
          points.entryPoint.x = points.exitPoint.x;
        }
        break;
      }
    }

    return points;
  }

  /**
   * Find focusable elements within the container
   * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
   * @function getIntersectionRect
   * @param rect1 {DOMRect} - The search origin which contains the exit point
   * @param rect2 {DOMRect} - One of candidates which contains the entry point
   * @returns {IntersectionArea} The intersection area between two elements.
   *
   * @typeof {Object} IntersectionArea
   * @property {Number} IntersectionArea.width
   * @property {Number} IntersectionArea.height
   */
  function getIntersectionRect(rect1, rect2) {
    const intersection_rect = {width: 0, height: 0, area: 0};

    const new_location = [Math.max(rect1.left, rect2.left), Math.max(rect1.top, rect2.top)];
    const new_max_point = [Math.min(rect1.right, rect2.right), Math.min(rect1.bottom, rect2.bottom)];

    intersection_rect.width = Math.abs(new_location[0] - new_max_point[0]);
    intersection_rect.height = Math.abs(new_location[1] - new_max_point[1]);

    if (!(new_location[0] >= new_max_point[0] || new_location[1] >= new_max_point[1])) {
      // intersecting-cases
      intersection_rect.area = Math.sqrt(intersection_rect.width * intersection_rect.height);
    }

    return intersection_rect;
  }

  /**
   * Handle the spatial navigation behavior for HTMLInputElement, HTMLTextAreaElement
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input|HTMLInputElement (MDN)}
   * @function handlingEditableElement
   * @param e {Event} - keydownEvent
   * @returns {boolean}
   */
  function handlingEditableElement(e) {
    const SPINNABLE_INPUT_TYPES = ['email', 'date', 'month', 'number', 'time', 'week'],
      TEXT_INPUT_TYPES = ['password', 'text', 'search', 'tel', 'url', null];
    const interestElement = focuslessSpatialNavigation ? currentInterest() : null;
    const eventTarget = focuslessSpatialNavigation && interestElement ? interestElement : document.activeElement;
    const startPosition = eventTarget.selectionStart;
    const endPosition = eventTarget.selectionEnd;
    const focusNavigableArrowKey = {left: false, up: false, right: false, down: false};

    const dir = ARROW_KEY_CODE[e.keyCode];
    if (dir === undefined) {
      return focusNavigableArrowKey;
    }

    if (SPINNABLE_INPUT_TYPES.includes(eventTarget.getAttribute('type')) &&
      (dir === 'up' || dir === 'down')) {
      focusNavigableArrowKey[dir] = true;
    } else if (TEXT_INPUT_TYPES.includes(eventTarget.getAttribute('type')) || eventTarget.nodeName === 'TEXTAREA') {
      if (startPosition === endPosition) { // if there isn't any selected text
        if (startPosition === 0) {
          focusNavigableArrowKey.left = true;
          focusNavigableArrowKey.up = true;
        }
        if (endPosition === eventTarget.value.length) {
          focusNavigableArrowKey.right = true;
          focusNavigableArrowKey.down = true;
        }
      }
    } else { // HTMLDataListElement, HTMLSelectElement, HTMLOptGroup
      focusNavigableArrowKey[dir] = true;
    }

    return focusNavigableArrowKey;
  }

  /**
   * Get the DOMRect of an element
   * @function getBoundingClientRect
   * @param element {Node}
   * @returns {DOMRect}
   */
  function getBoundingClientRect(element) {
    // memoization
    let rect = mapOfBoundRect && mapOfBoundRect.get(element);
    if (!rect) {
      const boundingClientRect = element.getBoundingClientRect();
      rect = {
        top: Number(boundingClientRect.top.toFixed(2)),
        right: Number(boundingClientRect.right.toFixed(2)),
        bottom: Number(boundingClientRect.bottom.toFixed(2)),
        left: Number(boundingClientRect.left.toFixed(2)),
        width: Number(boundingClientRect.width.toFixed(2)),
        height: Number(boundingClientRect.height.toFixed(2))
      };
      mapOfBoundRect && mapOfBoundRect.set(element, rect);
    }
    return rect;
  }

  /**
   * Get the list of the experimental APIs
   * @function getExperimentalAPI
   */
  function getExperimentalAPI() {
    function canScroll(container, dir) {
      return (isScrollable(container, dir) && !isScrollBoundary(container, dir)) ||
             (!container.parentElement && !isHTMLScrollBoundary(container, dir));
    }

    function findTarget(findCandidate, element, dir, option) {
      simulationElement = element;
      simulationBestTarget = null;
      simulationCandidates = null;

      navigate(dir);
      simulationElement = null;
      return findCandidate ? simulationCandidates : simulationBestTarget;
    }

    return {
      isContainer,
      findCandidates: findTarget.bind(null, true),
      findNextTarget: findTarget.bind(null, false),
      getDistanceFromTarget: (element, candidateElement, dir) => {
        if ((isContainer(element) || element.nodeName === 'BODY') && !(element.nodeName === 'INPUT')) {
          if (getSpatialNavigationCandidates(element).includes(candidateElement)) {
            return getInnerDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
          }
        }
        return getDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
      },
      useMemoizationForIsVisible : (option) => {
        mapOfBoundRect = option ? new Map() : null;
        mapOfVisible = option ? new Map() : null;
      }
    };
  }

  /**
   * Makes to use the experimental APIs.
   * @function enableExperimentalAPIs
   * @param option {boolean} - If it is true, the experimental APIs can be used or it cannot.
   */
  function enableExperimentalAPIs (option) {
    const currentKeyMode = window.__spatialNavigation__ && window.__spatialNavigation__.keyMode;
    window.__spatialNavigation__ = (option === false) ? getInitialAPIs() : Object.assign(getInitialAPIs(), getExperimentalAPI());
    window.__spatialNavigation__.keyMode = currentKeyMode;
    Object.seal(window.__spatialNavigation__);
  }

  /**
   * Set the environment for using the spatial navigation polyfill.
   * @function getInitialAPIs
   */
  function getInitialAPIs() {
    if (focuslessSpatialNavigation)
      return {
        enableExperimentalAPIs,
        get keyMode() { return this._keymode ? this._keymode : 'ARROW'; },
        set keyMode(mode) { this._keymode = (['SHIFTARROW', 'ARROW', 'NONE'].includes(mode)) ? mode : 'ARROW'; },
        currentInterest,
        interest,
        setStartingPoint: function (x, y) {startingPoint = (x && y) ? {x, y} : null;}
      };
    else
      return {
        enableExperimentalAPIs,
        get keyMode() { return this._keymode ? this._keymode : 'ARROW'; },
        set keyMode(mode) { this._keymode = (['SHIFTARROW', 'ARROW', 'NONE'].includes(mode)) ? mode : 'ARROW'; },
        setStartingPoint: function (x, y) {startingPoint = (x && y) ? {x, y} : null;}
      };
  }

  window.addEventListener('load', () => {
    initiateSpatialNavigation();
    enableExperimentalAPIs(false);
  });
})();