Source: gfx/accessibility/AccessibilityManager.js

const Node = require('../core/Node');
const { removeItems } = require('engine/utils/array');
const WebGLRenderer = require('../core/renderers/webgl/WebGLRenderer');
const CanvasRenderer = require('../core/renderers/canvas/CanvasRenderer');

// add some extra variables to the container..
Object.assign(
  Node.prototype,
  require('./accessibleTarget')
);


/**
 * The Accessibility manager reacreates the ability to tab and and have content read by screen readers. This is very important as it can possibly help people with disabilities access content.
 * Much like interaction any Node can be made accessible. This manager will map the events as if the mouse was being used, minimizing the efferot required to implement.
 *
 * @class
 * @constructor
 * @param {CanvasRenderer|WebGLRenderer} renderer   A reference to the current renderer
 */
function AccessibilityManager(renderer) {
  // first we create a div that will sit over the node. This is where the div overlays will go.
  var div = document.createElement('div');

  div.style.width = 100 + 'px';
  div.style.height = 100 + 'px';
  div.style.position = 'absolute';
  div.style.top = 0;
  div.style.left = 0;
   //
  div.style.zIndex = 2;

  /**
   * This is the dom element that will sit over the node. This is where the div overlays will go.
   *
   * @type {HTMLElement}
   * @private
   */
  this.div = div;

  /**
   * A simple pool for storing divs.
   *
   * @type {Array}
   * @private
   */
  this.pool = [];

  /**
   * This is a tick used to check if an object is no longer being rendered.
   *
   * @type {Number}
   * @private
   */
  this.renderId = 0;

  /**
   * Setting this to true will visually show the divs
   *
   * @type {Boolean}
   */
  this.debug = false;

  /**
   * The renderer this accessibility manager works for.
   *
   * @member {SystemRenderer}
   */
  this.renderer = renderer;

  /**
   * The array of currently active accessible items.
   *
   * @member {Array}
   * @private
   */
  this.children = [];

  /**
   * pre bind the functions..
   */
  this._onKeyDown = this._onKeyDown.bind(this);
  this._onMouseMove = this._onMouseMove.bind(this);

  /**
   * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false.
   *
   * @member {Array}
   * @private
   */
  this.isActive = false;


  // let listen for tab.. once pressed we can fire up and show the accessibility layer
  window.addEventListener('keydown', this._onKeyDown, false);
}


AccessibilityManager.prototype.constructor = AccessibilityManager;
module.exports = AccessibilityManager;

/**
 * Activating will cause the Accessibility layer to be shown. This is called when a user preses the tab key
 * @memberof AccessibilityManager#
 * @private
 */
AccessibilityManager.prototype.activate = function() {
  if (this.isActive) {
    return;
  }

  this.isActive = true;

  window.document.addEventListener('mousemove', this._onMouseMove, true);
  window.removeEventListener('keydown', this._onKeyDown, false);

  this.renderer.on('postrender', this.update, this);

  this.renderer.view.parentNode.appendChild(this.div);
};

/**
 * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves the mouse
 * @memberof AccessibilityManager#
 * @private
 */
AccessibilityManager.prototype.deactivate = function() {
  if (!this.isActive) {
    return;
  }

  this.isActive = false;

  window.document.removeEventListener('mousemove', this._onMouseMove);
  window.addEventListener('keydown', this._onKeyDown, false);

  this.renderer.off('postrender', this.update);

  this.div.parentNode.removeChild(this.div);

};

/**
 * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer.
 * @memberof AccessibilityManager#
 * @param {Node} displayObject   The Node to check.
 * @private
 */
AccessibilityManager.prototype.updateAccessibleObjects = function(displayObject) {
  if (!displayObject.visible) {
    return;
  }

  if (displayObject.accessible && displayObject.interactive) {
    if (!displayObject._accessibleActive) {
      this.addChild(displayObject);
    }

    displayObject.renderId = this.renderId;
  }


  if (displayObject.interactiveChildren) {

    var children = displayObject.children;

    for (var i = children.length - 1; i >= 0; i--) {

      this.updateAccessibleObjects(children[i]);
    }
  }
};


/**
 * Before each render this function will ensure that all divs are mapped correctly to their Node
 * @memberof AccessibilityManager#
 * @private
 */
AccessibilityManager.prototype.update = function() {
  // update children...
  this.updateAccessibleObjects(this.renderer._lastObjectRendered);

  var rect = this.renderer.view.getBoundingClientRect();
  var sx = rect.width / this.renderer.width;
  var sy = rect.height / this.renderer.height;

  var div = this.div;

  div.style.left = rect.left + 'px';
  div.style.top = rect.top + 'px';
  div.style.width = this.renderer.width + 'px';
  div.style.height = this.renderer.height + 'px';

  for (var i = 0; i < this.children.length; i++) {

    var child = this.children[i];

    if (child.renderId !== this.renderId) {
      child._accessibleActive = false;

      removeItems(this.children, i, 1);
      this.div.removeChild(child._accessibleDiv);
      this.pool.push(child._accessibleDiv);
      child._accessibleDiv = null;

      i--;

      if (this.children.length === 0) {
        this.deactivate();
      }
    }
    else {
      // map div to display..
      div = child._accessibleDiv;
      var hitArea = child.hitArea;
      var wt = child.worldTransform;

      if (child.hitArea) {
        div.style.left = ((wt.tx + (hitArea.x * wt.a)) * sx) + 'px';
        div.style.top = ((wt.ty + (hitArea.y * wt.d)) * sy) + 'px';

        div.style.width = (hitArea.width * wt.a * sx) + 'px';
        div.style.height = (hitArea.height * wt.d * sy) + 'px';

      }
      else {
        hitArea = child.getBounds();

        this.capHitArea(hitArea);

        div.style.left = (hitArea.x * sx) + 'px';
        div.style.top = (hitArea.y * sy) + 'px';

        div.style.width = (hitArea.width * sx) + 'px';
        div.style.height = (hitArea.height * sy) + 'px';
      }
    }
  }

  // increment the render id..
  this.renderId++;
};

AccessibilityManager.prototype.capHitArea = function(hitArea) {
  if (hitArea.x < 0) {
    hitArea.width += hitArea.x;
    hitArea.x = 0;
  }

  if (hitArea.y < 0) {
    hitArea.height += hitArea.y;
    hitArea.y = 0;
  }

  if (hitArea.x + hitArea.width > this.renderer.width) {
    hitArea.width = this.renderer.width - hitArea.x;
  }

  if (hitArea.y + hitArea.height > this.renderer.height) {
    hitArea.height = this.renderer.height - hitArea.y;
  }
};


/**
 * Adds a Node to the accessibility manager.
 * @memberof AccessibilityManager#
 * @param {Node} displayObject Object to add
 * @private
 */
AccessibilityManager.prototype.addChild = function(displayObject) {
//  this.activate();

  var div = this.pool.pop();

  if (!div) {
    div = document.createElement('button');

    div.style.width = 100 + 'px';
    div.style.height = 100 + 'px';
    div.style.backgroundColor = this.debug ? 'rgba(255,0,0,0.5)' : 'transparent';
    div.style.position = 'absolute';
    div.style.zIndex = 2;
    div.style.borderStyle = 'none';


    div.addEventListener('click', this._onClick.bind(this));
    div.addEventListener('focus', this._onFocus.bind(this));
    div.addEventListener('focusout', this._onFocusOut.bind(this));
  }


  div.title = displayObject.accessibleTitle || 'displayObject ' + this.tabIndex;

  //

  displayObject._accessibleActive = true;
  displayObject._accessibleDiv = div;
  div.displayObject = displayObject;


  this.children.push(displayObject);
  this.div.appendChild(displayObject._accessibleDiv);
  displayObject._accessibleDiv.tabIndex = displayObject.tabIndex;
};


/**
 * Maps the div button press to InteractionManager (click)
 * @memberof AccessibilityManager#
 * @param {Event} e Mouse event
 * @private
 */
AccessibilityManager.prototype._onClick = function(e) {
  var interactionManager = this.renderer.plugins.interaction;
  interactionManager.dispatchEvent(e.target.displayObject, 'click', interactionManager.eventData);
};

/**
 * Maps the div focus events to InteractionManager (mouseover)
 * @memberof AccessibilityManager#
 * @param {Event} e Mouse event
 * @private
 */
AccessibilityManager.prototype._onFocus = function(e) {
  var interactionManager = this.renderer.plugins.interaction;
  interactionManager.dispatchEvent(e.target.displayObject, 'mouseover', interactionManager.eventData);
};

/**
 * Maps the div focus events to InteractionManager (mouseout)
 * @memberof AccessibilityManager#
 * @param {Event} e Mouse event
 * @private
 */
AccessibilityManager.prototype._onFocusOut = function(e) {
  var interactionManager = this.renderer.plugins.interaction;
  interactionManager.dispatchEvent(e.target.displayObject, 'mouseout', interactionManager.eventData);
};

/**
 * Is called when a key is pressed
 * @memberof AccessibilityManager#
 * @param {Event} e Key down event
 * @private
 */
AccessibilityManager.prototype._onKeyDown = function(e) {
  if (e.keyCode !== 9) {
    return;
  }

  this.activate();
};

/**
 * Is called when the mouse moves across the renderer element
 * @memberof AccessibilityManager#
 * @private
 */
AccessibilityManager.prototype._onMouseMove = function() {
  this.deactivate();
};


/**
 * Destroys the accessibility manager
 * @memberof AccessibilityManager#
 */
AccessibilityManager.prototype.destroy = function() {
  this.div = null;

  for (var i = 0; i < this.children.length; i++) {
    this.children[i].div = null;
  }


  window.document.removeEventListener('mousemove', this._onMouseMove);
  window.removeEventListener('keydown', this._onKeyDown);

  this.pool = null;
  this.children = null;
  this.renderer = null;
};

WebGLRenderer.registerPlugin('accessibility', AccessibilityManager);
CanvasRenderer.registerPlugin('accessibility', AccessibilityManager);