import assign from "./utils/assign"; import TouchAction from "./touchactionjs/touchaction-constructor"; import createInputInstance from "./inputjs/create-input-instance"; import each from "./utils/each"; import inArray from "./utils/in-array"; import invokeArrayArg from "./utils/invoke-array-arg"; import splitStr from "./utils/split-str"; import prefixed from "./utils/prefixed"; import Recognizer from "./recognizerjs/recognizer-constructor"; import { STATE_BEGAN, STATE_ENDED, STATE_CHANGED, STATE_RECOGNIZED, } from "./recognizerjs/recognizer-consts"; import defaults from "./defaults"; const STOP = 1; const FORCED_STOP = 2; /** * @private * add/remove the css properties as defined in manager.options.cssProps * @param {Manager} manager * @param {Boolean} add */ function toggleCssProps(manager, add) { const { element } = manager; if (!element.style) { return; } let prop; each(manager.options.cssProps, (value, name) => { prop = prefixed(element.style, name); if (add) { manager.oldCssProps[prop] = element.style[prop]; element.style[prop] = value; } else { element.style[prop] = manager.oldCssProps[prop] || ""; } }); if (!add) { manager.oldCssProps = {}; } } /** * @private * trigger dom event * @param {String} event * @param {Object} data */ function triggerDomEvent(event, data) { const gestureEvent = document.createEvent("Event"); gestureEvent.initEvent(event, true, true); gestureEvent.gesture = data; data.target.dispatchEvent(gestureEvent); } /** * @private * Manager * @param {HTMLElement} element * @param {Object} [options] * @constructor */ export default class Manager { constructor(element, options) { this.options = assign({}, defaults, options || {}); this.options.inputTarget = this.options.inputTarget || element; this.handlers = {}; this.session = {}; this.recognizers = []; this.oldCssProps = {}; this.element = element; this.input = createInputInstance(this); this.touchAction = new TouchAction(this, this.options.touchAction); toggleCssProps(this, true); each(this.options.recognizers, item => { const recognizer = this.add(new (item[0])(item[1])); item[2] && recognizer.recognizeWith(item[2]); item[3] && recognizer.requireFailure(item[3]); }, this); } /** * @private * set options * @param {Object} options * @returns {Manager} */ set(options) { assign(this.options, options); // Options that need a little more setup if (options.touchAction) { this.touchAction.update(); } if (options.inputTarget) { // Clean up existing event listeners and reinitialize this.input.destroy(); this.input.target = options.inputTarget; this.input.init(); } return this; } /** * @private * stop recognizing for this session. * This session will be discarded, when a new [input]start event is fired. * When forced, the recognizer cycle is stopped immediately. * @param {Boolean} [force] */ stop(force) { this.session.stopped = force ? FORCED_STOP : STOP; } /** * @private * run the recognizers! * called by the inputHandler function on every movement of the pointers (touches) * it walks through all the recognizers and tries to detect the gesture that is being made * @param {Object} inputData */ recognize(inputData) { const { session } = this; if (session.stopped) { return; } // run the touch-action polyfill this.touchAction.preventDefaults(inputData); let recognizer; const { recognizers } = this; // this holds the recognizer that is being recognized. // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED // if no recognizer is detecting a thing, it is set to `null` let { curRecognizer } = session; // reset when the last recognizer is recognized // or when we're in a new session if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { session.curRecognizer = null; curRecognizer = null; } let i = 0; while (i < recognizers.length) { recognizer = recognizers[i]; // find out if we are allowed try to recognize the input for this one. // 1. allow if the session is NOT forced stopped (see the .stop() method) // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one // that is being recognized. // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. // this can be setup with the `recognizeWith()` method on the recognizer. if (session.stopped !== FORCED_STOP && (// 1 !curRecognizer || recognizer === curRecognizer || // 2 recognizer.canRecognizeWith(curRecognizer))) { // 3 recognizer.recognize(inputData); } else { recognizer.reset(); } // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the // current active recognizer. but only if we don't already have an active recognizer if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { session.curRecognizer = recognizer; curRecognizer = recognizer; } i++; } } /** * @private * get a recognizer by its event name. * @param {Recognizer|String} recognizer * @returns {Recognizer|Null} */ get(recognizer) { if (recognizer instanceof Recognizer) { return recognizer; } const { recognizers } = this; for (let i = 0; i < recognizers.length; i++) { if (recognizers[i].options.event === recognizer) { return recognizers[i]; } } return null; } /** * @private add a recognizer to the manager * existing recognizers with the same event name will be removed * @param {Recognizer} recognizer * @returns {Recognizer|Manager} */ add(recognizer) { if (invokeArrayArg(recognizer, "add", this)) { return this; } // remove existing const existing = this.get(recognizer.options.event); if (existing) { this.remove(existing); } this.recognizers.push(recognizer); recognizer.manager = this; this.touchAction.update(); return recognizer; } /** * @private * remove a recognizer by name or instance * @param {Recognizer|String} recognizer * @returns {Manager} */ remove(recognizer) { if (invokeArrayArg(recognizer, "remove", this)) { return this; } const targetRecognizer = this.get(recognizer); // let's make sure this recognizer exists if (recognizer) { const { recognizers } = this; const index = inArray(recognizers, targetRecognizer); if (index !== -1) { recognizers.splice(index, 1); this.touchAction.update(); } } return this; } /** * @private * bind event * @param {String} events * @param {Function} handler * @returns {EventEmitter} this */ on(events, handler) { if (events === undefined || handler === undefined) { return this; } const { handlers } = this; each(splitStr(events), event => { handlers[event] = handlers[event] || []; handlers[event].push(handler); }); return this; } /** * @private unbind event, leave emit blank to remove all handlers * @param {String} events * @param {Function} [handler] * @returns {EventEmitter} this */ off(events, handler) { if (events === undefined) { return this; } const { handlers } = this; each(splitStr(events), event => { if (!handler) { delete handlers[event]; } else { handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1); } }); return this; } /** * @private emit event to the listeners * @param {String} event * @param {Object} data */ emit(event, data) { // we also want to trigger dom events if (this.options.domEvents) { triggerDomEvent(event, data); } // no handlers, so skip it all const handlers = this.handlers[event] && this.handlers[event].slice(); if (!handlers || !handlers.length) { return; } data.type = event; data.preventDefault = function () { data.srcEvent.preventDefault(); }; let i = 0; while (i < handlers.length) { handlers[i](data); i++; } } /** * @private * destroy the manager and unbinds all events * it doesn't unbind dom events, that is the user own responsibility */ destroy() { this.element && toggleCssProps(this, false); this.handlers = {}; this.session = {}; this.input.destroy(); this.element = null; } }