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);