You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
353 lines
8.6 KiB
353 lines
8.6 KiB
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;
|
|
}
|
|
}
|