src/core/classes/State.js
/**
* @file State.js
*/
import Gesture from './../../gestures/Gesture.js';
import Expand from './../../gestures/Expand.js';
import Pan from './../../gestures/Pan.js';
import Pinch from './../../gestures/Pinch.js';
import Rotate from './../../gestures/Rotate.js';
import Swipe from './../../gestures/Swipe.js';
import Tap from './../../gestures/Tap.js';
import Binding from './Binding.js';
import Input from './Input.js';
import util from './../util.js';
const DEFAULT_MOUSE_ID = 0;
/**
* Creates an object related to a Region's state, and contains helper methods to update and clean up different
* states.
*/
class State {
/**
* Constructor for the State class.
*/
constructor(regionId) {
/**
* The id for the region this state is bound to.
* @type {String}
*/
this.regionId = regionId;
/**
* An array of current and recently inactive Input objects related to a gesture.
* @type {Input}
*/
this.inputs = [];
/**
* An array of Binding objects; The list of relations between elements, their gestures, and the handlers.
* @type {Binding}
*/
this.bindings = [];
/**
* The number of gestures that have been registered with this state
* @type {Number}
*/
this.numGestures = 0;
/**
* A key/value map all the registered gestures for the listener. Note: Can only have one gesture registered to one key.
* @type {Object}
*/
this.registeredGestures = {};
this.registerGesture(new Expand(), 'expand');
this.registerGesture(new Pan(), 'pan');
this.registerGesture(new Rotate(), 'rotate');
this.registerGesture(new Pinch(), 'pinch');
this.registerGesture(new Swipe(), 'swipe');
this.registerGesture(new Tap(), 'tap');
}
/**
* Creates a new binding with the given element and gesture object.
* If the gesture object provided is unregistered, it's reference will be saved in as a binding to
* be later referenced.
* @param {Element} element - The element the gesture is bound to.
* @param {String|Object} gesture - Either a name of a registered gesture, or an unregistered
* Gesture object.
* @param {Function} handler - The function handler to be called when the event is emitted.
* Used to bind/unbind.
* @param {Boolean} capture - Whether the gesture is to be detected in the capture of bubble
* phase. Used to bind/unbind.
* @param {Boolean} bindOnce - Option to bind once and only emit the event once.
*/
addBinding(element, gesture, handler, capture, bindOnce) {
var boundGesture;
//Error type checking.
if (element && typeof element.tagName === 'undefined') {
throw new Error('Parameter element is an invalid object.');
}
if (typeof handler !== 'function') {
throw new Error('Parameter handler is invalid.');
}
if (typeof gesture === 'string' && Object.keys(this.registeredGestures).indexOf(gesture) === -1) {
throw new Error('Parameter ' + gesture + ' is not a registered gesture');
} else if (typeof gesture === 'object' && !(gesture instanceof Gesture)) {
throw new Error('Parameter for the gesture is not of a Gesture type');
}
if (typeof gesture === 'string') {
boundGesture = this.registeredGestures[gesture];
} else {
boundGesture = gesture;
this.assignGestureId(boundGesture);
}
this.bindings.push(new Binding(element, boundGesture, handler, capture, bindOnce));
element.addEventListener(boundGesture.getId(), handler, capture);
}
/*addBinding*/
/**
* Retrieves the Binding by which an element is associated to.
* @param {Element} element - The element to find bindings to.
* @returns {Array} - An array of Bindings to which that element is bound
*/
retrieveBindingsByElement(element) {
var matches = [];
this.bindings.map(binding => {
if (binding.element === element) {
matches.push(binding);
}
});
return matches;
}
/*retrieveBindingsByElement*/
/**
* Retrieves all bindings based upon the initial X/Y position of the inputs.
* e.g. if gesture started on the correct target element, but diverted away into the correct region,
* this would still be valid.
* @returns {Array} - An array of Bindings to which that element is bound
*/
retrieveBindingsByInitialPos() {
var matches = [];
this.bindings.forEach(binding => {
// Determine if at least one input is in the target element. They should all be in the region based upon a prior check
var inputsInside = this.inputs.filter(input => {
return util.isInside(input.initial.x, input.initial.y, binding.element);
});
if (inputsInside.length > 0) {
matches.push(binding);
}
});
return matches;
}
/* retrieveBindingsByInitialPos */
/**
* Updates the inputs with new information based upon a new event being fired.
* @param {Event} event - The event being captured
* @param {Element} regionElement - The element where this current Region is bound to.
* @returns {boolean} - returns true for a successful update, false if the event is invalid.
*/
updateInputs(event, regionElement) {
var identifier = DEFAULT_MOUSE_ID;
var eventType = (event.touches) ? 'TouchEvent' : (event.pointerType) ? 'PointerEvent' : 'MouseEvent';
switch (eventType) {
case 'TouchEvent':
//Return if all gestures did not originate from the same target
if (event.touches.length !== event.targetTouches.length) {
return false;
}
for (var index in event.changedTouches) {
if (event.changedTouches.hasOwnProperty(index) && util.isInteger((parseInt(index)))) {
identifier = event.changedTouches[index].identifier;
update(event, this, identifier, regionElement);
}
}
break;
case 'PointerEvent':
identifier = event.pointerId;
update(event, this, identifier, regionElement);
break;
case 'MouseEvent':
default:
update(event, this, DEFAULT_MOUSE_ID, regionElement);
break;
}
return true;
function update(event, state, identifier, regionElement) {
var eventType = util.normalizeEvent(event.type);
var input = findInputById(state.inputs, identifier);
//A starting input was not cleaned up properly and still exists.
if (eventType === 'start' && input) {
state.resetInputs();
return;
}
//An input has moved outside the region.
if (eventType !== 'start' && input && !util.isInside(input.current.x, input.current.y, regionElement)) {
state.resetInputs();
return;
}
if (eventType !== 'start' && !input) {
state.resetInputs();
return;
}
if (eventType === 'start') {
state.inputs.push(new Input(event, identifier));
} else {
input.update(event, identifier);
}
}
}
/* updateInputs */
/**
* Removes all inputs from the state, allowing for a new gesture.
*/
resetInputs() {
this.inputs = [];
}
/* resetInputs */
/**
* Counts the number of active inputs at any given time.
* @returns {Number} - The number of active inputs.
*/
numActiveInputs() {
var endType = this.inputs.filter(input => {
return input.current.type !== 'end';
});
return endType.length;
}
/* numActiveInputs */
/**
* Register the gesture to the current region.
* @param {Object} gesture - The gesture to register
* @param {String} key - The key to define the new gesture as.
*/
registerGesture(gesture, key) {
this.assignGestureId(gesture);
this.registeredGestures[key] = gesture;
}
/* registerGesture */
/**
* Tracks the gesture to this state object to become uniquely identifiable.
* Useful for nested Regions.
* @param {Gesture} gesture - The gesture to track
*/
assignGestureId(gesture) {
gesture.setId(this.regionId + '-' + this.numGestures++);
}
/* assignGestureId */
}
/**
* Searches through each input, comparing the browser's identifier key for touches, to the stored one
* in each input
* @param {Array} inputs - The array of inputs in state.
* @param {String} identifier - The identifier the browser has assigned.
* @returns {Input} - The input object with the corresponding identifier, null if it did not find any.
*/
function findInputById(inputs, identifier) {
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].identifier === identifier) {
return inputs[i];
}
}
return null;
}
/* findInputById */
export default State;