| /**
 * KeyboardNavigation.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
/**
 * This class handles keyboard navigation of controls and elements.
 *
 * @class tinymce.ui.KeyboardNavigation
 */
define(
  'tinymce.ui.KeyboardNavigation',
  [
    'global!document'
  ],
  function (document) {
    "use strict";
    var hasTabstopData = function (elm) {
      return elm.getAttribute('data-mce-tabstop') ? true : false;
    };
    /**
     * This class handles all keyboard navigation for WAI-ARIA support. Each root container
     * gets an instance of this class.
     *
     * @constructor
     */
    return function (settings) {
      var root = settings.root, focusedElement, focusedControl;
      function isElement(node) {
        return node && node.nodeType === 1;
      }
      try {
        focusedElement = document.activeElement;
      } catch (ex) {
        // IE sometimes fails to return a proper element
        focusedElement = document.body;
      }
      focusedControl = root.getParentCtrl(focusedElement);
      /**
       * Returns the currently focused elements wai aria role of the currently
       * focused element or specified element.
       *
       * @private
       * @param {Element} elm Optional element to get role from.
       * @return {String} Role of specified element.
       */
      function getRole(elm) {
        elm = elm || focusedElement;
        if (isElement(elm)) {
          return elm.getAttribute('role');
        }
        return null;
      }
      /**
       * Returns the wai role of the parent element of the currently
       * focused element or specified element.
       *
       * @private
       * @param {Element} elm Optional element to get parent role from.
       * @return {String} Role of the first parent that has a role.
       */
      function getParentRole(elm) {
        var role, parent = elm || focusedElement;
        while ((parent = parent.parentNode)) {
          if ((role = getRole(parent))) {
            return role;
          }
        }
      }
      /**
       * Returns a wai aria property by name for example aria-selected.
       *
       * @private
       * @param {String} name Name of the aria property to get for example "disabled".
       * @return {String} Aria property value.
       */
      function getAriaProp(name) {
        var elm = focusedElement;
        if (isElement(elm)) {
          return elm.getAttribute('aria-' + name);
        }
      }
      /**
       * Is the element a text input element or not.
       *
       * @private
       * @param {Element} elm Element to check if it's an text input element or not.
       * @return {Boolean} True/false if the element is a text element or not.
       */
      function isTextInputElement(elm) {
        var tagName = elm.tagName.toUpperCase();
        // Notice: since type can be "email" etc we don't check the type
        // So all input elements gets treated as text input elements
        return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
      }
      /**
       * Returns true/false if the specified element can be focused or not.
       *
       * @private
       * @param {Element} elm DOM element to check if it can be focused or not.
       * @return {Boolean} True/false if the element can have focus.
       */
      function canFocus(elm) {
        if (isTextInputElement(elm) && !elm.hidden) {
          return true;
        }
        if (hasTabstopData(elm)) {
          return true;
        }
        if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) {
          return true;
        }
        return false;
      }
      /**
       * Returns an array of focusable visible elements within the specified container element.
       *
       * @private
       * @param {Element} elm DOM element to find focusable elements within.
       * @return {Array} Array of focusable elements.
       */
      function getFocusElements(elm) {
        var elements = [];
        function collect(elm) {
          if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) {
            return;
          }
          if (canFocus(elm)) {
            elements.push(elm);
          }
          for (var i = 0; i < elm.childNodes.length; i++) {
            collect(elm.childNodes[i]);
          }
        }
        collect(elm || root.getEl());
        return elements;
      }
      /**
       * Returns the navigation root control for the specified control. The navigation root
       * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
       * It will look for parents of the specified target control or the currently focused control if this option is omitted.
       *
       * @private
       * @param {tinymce.ui.Control} targetControl Optional target control to find root of.
       * @return {tinymce.ui.Control} Navigation root control.
       */
      function getNavigationRoot(targetControl) {
        var navigationRoot, controls;
        targetControl = targetControl || focusedControl;
        controls = targetControl.parents().toArray();
        controls.unshift(targetControl);
        for (var i = 0; i < controls.length; i++) {
          navigationRoot = controls[i];
          if (navigationRoot.settings.ariaRoot) {
            break;
          }
        }
        return navigationRoot;
      }
      /**
       * Focuses the first item in the specified targetControl element or the last aria index if the
       * navigation root has the ariaRemember option enabled.
       *
       * @private
       * @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
       */
      function focusFirst(targetControl) {
        var navigationRoot = getNavigationRoot(targetControl);
        var focusElements = getFocusElements(navigationRoot.getEl());
        if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
          moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
        } else {
          moveFocusToIndex(0, focusElements);
        }
      }
      /**
       * Moves the focus to the specified index within the elements list.
       * This will scope the index to the size of the element list if it changed.
       *
       * @private
       * @param {Number} idx Specified index to move to.
       * @param {Array} elements Array with dom elements to move focus within.
       * @return {Number} Input index or a changed index if it was out of range.
       */
      function moveFocusToIndex(idx, elements) {
        if (idx < 0) {
          idx = elements.length - 1;
        } else if (idx >= elements.length) {
          idx = 0;
        }
        if (elements[idx]) {
          elements[idx].focus();
        }
        return idx;
      }
      /**
       * Moves the focus forwards or backwards.
       *
       * @private
       * @param {Number} dir Direction to move in positive means forward, negative means backwards.
       * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
       */
      function moveFocus(dir, elements) {
        var idx = -1, navigationRoot = getNavigationRoot();
        elements = elements || getFocusElements(navigationRoot.getEl());
        for (var i = 0; i < elements.length; i++) {
          if (elements[i] === focusedElement) {
            idx = i;
          }
        }
        idx += dir;
        navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
      }
      /**
       * Moves the focus to the left this is called by the left key.
       *
       * @private
       */
      function left() {
        var parentRole = getParentRole();
        if (parentRole == "tablist") {
          moveFocus(-1, getFocusElements(focusedElement.parentNode));
        } else if (focusedControl.parent().submenu) {
          cancel();
        } else {
          moveFocus(-1);
        }
      }
      /**
       * Moves the focus to the right this is called by the right key.
       *
       * @private
       */
      function right() {
        var role = getRole(), parentRole = getParentRole();
        if (parentRole == "tablist") {
          moveFocus(1, getFocusElements(focusedElement.parentNode));
        } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
          enter();
        } else {
          moveFocus(1);
        }
      }
      /**
       * Moves the focus to the up this is called by the up key.
       *
       * @private
       */
      function up() {
        moveFocus(-1);
      }
      /**
       * Moves the focus to the up this is called by the down key.
       *
       * @private
       */
      function down() {
        var role = getRole(), parentRole = getParentRole();
        if (role == "menuitem" && parentRole == "menubar") {
          enter();
        } else if (role == "button" && getAriaProp('haspopup')) {
          enter({ key: 'down' });
        } else {
          moveFocus(1);
        }
      }
      /**
       * Moves the focus to the next item or previous item depending on shift key.
       *
       * @private
       * @param {DOMEvent} e DOM event object.
       */
      function tab(e) {
        var parentRole = getParentRole();
        if (parentRole == "tablist") {
          var elm = getFocusElements(focusedControl.getEl('body'))[0];
          if (elm) {
            elm.focus();
          }
        } else {
          moveFocus(e.shiftKey ? -1 : 1);
        }
      }
      /**
       * Calls the cancel event on the currently focused control. This is normally done using the Esc key.
       *
       * @private
       */
      function cancel() {
        focusedControl.fire('cancel');
      }
      /**
       * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
       *
       * @private
       * @param {Object} aria Optional aria data to pass along with the enter event.
       */
      function enter(aria) {
        aria = aria || {};
        focusedControl.fire('click', { target: focusedElement, aria: aria });
      }
      root.on('keydown', function (e) {
        function handleNonTabOrEscEvent(e, handler) {
          // Ignore non tab keys for text elements
          if (isTextInputElement(focusedElement) || hasTabstopData(focusedElement)) {
            return;
          }
          if (getRole(focusedElement) === 'slider') {
            return;
          }
          if (handler(e) !== false) {
            e.preventDefault();
          }
        }
        if (e.isDefaultPrevented()) {
          return;
        }
        switch (e.keyCode) {
          case 37: // DOM_VK_LEFT
            handleNonTabOrEscEvent(e, left);
            break;
          case 39: // DOM_VK_RIGHT
            handleNonTabOrEscEvent(e, right);
            break;
          case 38: // DOM_VK_UP
            handleNonTabOrEscEvent(e, up);
            break;
          case 40: // DOM_VK_DOWN
            handleNonTabOrEscEvent(e, down);
            break;
          case 27: // DOM_VK_ESCAPE
            cancel();
            break;
          case 14: // DOM_VK_ENTER
          case 13: // DOM_VK_RETURN
          case 32: // DOM_VK_SPACE
            handleNonTabOrEscEvent(e, enter);
            break;
          case 9: // DOM_VK_TAB
            if (tab(e) !== false) {
              e.preventDefault();
            }
            break;
        }
      });
      root.on('focusin', function (e) {
        focusedElement = e.target;
        focusedControl = e.control;
      });
      return {
        focusFirst: focusFirst
      };
    };
  }
);
 |