400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
/* eslint-disable no-dupe-class-members */
|
|
import * as arr from "../utils/arr.js";
|
|
import browser from "../utils/browser.js";
|
|
import clone from "../utils/clone.js";
|
|
import { getElementRect, matchesUpTo, nodeContains, trySelector } from "../utils/domUtils.js";
|
|
import extend from "../utils/extend.js";
|
|
import is from "../utils/is.js";
|
|
import normalizeListeners from "../utils/normalizeListeners.js";
|
|
import { getWindow } from "../utils/window.js";
|
|
import { Eventable } from "./Eventable.js";
|
|
import isNonNativeEvent from "./isNonNativeEvent.js";
|
|
|
|
/** */
|
|
export class Interactable {
|
|
/** @internal */
|
|
get _defaults() {
|
|
return {
|
|
base: {},
|
|
perAction: {},
|
|
actions: {}
|
|
};
|
|
}
|
|
|
|
/** */
|
|
constructor(target, options, defaultContext, scopeEvents) {
|
|
this.options = void 0;
|
|
this._actions = void 0;
|
|
this.target = void 0;
|
|
this.events = new Eventable();
|
|
this._context = void 0;
|
|
this._win = void 0;
|
|
this._doc = void 0;
|
|
this._scopeEvents = void 0;
|
|
this._rectChecker = void 0;
|
|
this._actions = options.actions;
|
|
this.target = target;
|
|
this._context = options.context || defaultContext;
|
|
this._win = getWindow(trySelector(target) ? this._context : target);
|
|
this._doc = this._win.document;
|
|
this._scopeEvents = scopeEvents;
|
|
this.set(options);
|
|
}
|
|
|
|
setOnEvents(actionName, phases) {
|
|
if (is.func(phases.onstart)) {
|
|
this.on(`${actionName}start`, phases.onstart);
|
|
}
|
|
|
|
if (is.func(phases.onmove)) {
|
|
this.on(`${actionName}move`, phases.onmove);
|
|
}
|
|
|
|
if (is.func(phases.onend)) {
|
|
this.on(`${actionName}end`, phases.onend);
|
|
}
|
|
|
|
if (is.func(phases.oninertiastart)) {
|
|
this.on(`${actionName}inertiastart`, phases.oninertiastart);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
updatePerActionListeners(actionName, prev, cur) {
|
|
if (is.array(prev) || is.object(prev)) {
|
|
this.off(actionName, prev);
|
|
}
|
|
|
|
if (is.array(cur) || is.object(cur)) {
|
|
this.on(actionName, cur);
|
|
}
|
|
}
|
|
|
|
setPerAction(actionName, options) {
|
|
const defaults = this._defaults; // for all the default per-action options
|
|
|
|
for (const optionName_ in options) {
|
|
const optionName = optionName_;
|
|
const actionOptions = this.options[actionName];
|
|
const optionValue = options[optionName]; // remove old event listeners and add new ones
|
|
|
|
if (optionName === 'listeners') {
|
|
this.updatePerActionListeners(actionName, actionOptions.listeners, optionValue);
|
|
} // if the option value is an array
|
|
|
|
|
|
if (is.array(optionValue)) {
|
|
actionOptions[optionName] = arr.from(optionValue);
|
|
} // if the option value is an object
|
|
else if (is.plainObject(optionValue)) {
|
|
// copy the object
|
|
actionOptions[optionName] = extend(actionOptions[optionName] || {}, clone(optionValue)); // set anabled field to true if it exists in the defaults
|
|
|
|
if (is.object(defaults.perAction[optionName]) && 'enabled' in defaults.perAction[optionName]) {
|
|
actionOptions[optionName].enabled = optionValue.enabled !== false;
|
|
}
|
|
} // if the option value is a boolean and the default is an object
|
|
else if (is.bool(optionValue) && is.object(defaults.perAction[optionName])) {
|
|
actionOptions[optionName].enabled = optionValue;
|
|
} // if it's anything else, do a plain assignment
|
|
else {
|
|
actionOptions[optionName] = optionValue;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* The default function to get an Interactables bounding rect. Can be
|
|
* overridden using {@link Interactable.rectChecker}.
|
|
*
|
|
* @param {Element} [element] The element to measure.
|
|
* @return {Rect} The object's bounding rectangle.
|
|
*/
|
|
|
|
|
|
getRect(element) {
|
|
element = element || (is.element(this.target) ? this.target : null);
|
|
|
|
if (is.string(this.target)) {
|
|
element = element || this._context.querySelector(this.target);
|
|
}
|
|
|
|
return getElementRect(element);
|
|
}
|
|
/**
|
|
* Returns or sets the function used to calculate the interactable's
|
|
* element's rectangle
|
|
*
|
|
* @param {function} [checker] A function which returns this Interactable's
|
|
* bounding rectangle. See {@link Interactable.getRect}
|
|
* @return {function | object} The checker function or this Interactable
|
|
*/
|
|
|
|
|
|
rectChecker(checker) {
|
|
if (is.func(checker)) {
|
|
this._rectChecker = checker;
|
|
|
|
this.getRect = element => {
|
|
const rect = extend({}, this._rectChecker(element));
|
|
|
|
if (!('width' in rect)) {
|
|
rect.width = rect.right - rect.left;
|
|
rect.height = rect.bottom - rect.top;
|
|
}
|
|
|
|
return rect;
|
|
};
|
|
|
|
return this;
|
|
}
|
|
|
|
if (checker === null) {
|
|
delete this.getRect;
|
|
delete this._rectChecker;
|
|
return this;
|
|
}
|
|
|
|
return this.getRect;
|
|
}
|
|
|
|
_backCompatOption(optionName, newValue) {
|
|
if (trySelector(newValue) || is.object(newValue)) {
|
|
this.options[optionName] = newValue;
|
|
|
|
for (const action in this._actions.map) {
|
|
this.options[action][optionName] = newValue;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
return this.options[optionName];
|
|
}
|
|
/**
|
|
* Gets or sets the origin of the Interactable's element. The x and y
|
|
* of the origin will be subtracted from action event coordinates.
|
|
*
|
|
* @param {Element | object | string} [origin] An HTML or SVG Element whose
|
|
* rect will be used, an object eg. { x: 0, y: 0 } or string 'parent', 'self'
|
|
* or any CSS selector
|
|
*
|
|
* @return {object} The current origin or this Interactable
|
|
*/
|
|
|
|
|
|
origin(newValue) {
|
|
return this._backCompatOption('origin', newValue);
|
|
}
|
|
/**
|
|
* Returns or sets the mouse coordinate types used to calculate the
|
|
* movement of the pointer.
|
|
*
|
|
* @param {string} [newValue] Use 'client' if you will be scrolling while
|
|
* interacting; Use 'page' if you want autoScroll to work
|
|
* @return {string | object} The current deltaSource or this Interactable
|
|
*/
|
|
|
|
|
|
deltaSource(newValue) {
|
|
if (newValue === 'page' || newValue === 'client') {
|
|
this.options.deltaSource = newValue;
|
|
return this;
|
|
}
|
|
|
|
return this.options.deltaSource;
|
|
}
|
|
/**
|
|
* Gets the selector context Node of the Interactable. The default is
|
|
* `window.document`.
|
|
*
|
|
* @return {Node} The context Node of this Interactable
|
|
*/
|
|
|
|
|
|
context() {
|
|
return this._context;
|
|
}
|
|
|
|
inContext(element) {
|
|
return this._context === element.ownerDocument || nodeContains(this._context, element);
|
|
}
|
|
|
|
testIgnoreAllow(options, targetNode, eventTarget) {
|
|
return !this.testIgnore(options.ignoreFrom, targetNode, eventTarget) && this.testAllow(options.allowFrom, targetNode, eventTarget);
|
|
}
|
|
|
|
testAllow(allowFrom, targetNode, element) {
|
|
if (!allowFrom) {
|
|
return true;
|
|
}
|
|
|
|
if (!is.element(element)) {
|
|
return false;
|
|
}
|
|
|
|
if (is.string(allowFrom)) {
|
|
return matchesUpTo(element, allowFrom, targetNode);
|
|
} else if (is.element(allowFrom)) {
|
|
return nodeContains(allowFrom, element);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
testIgnore(ignoreFrom, targetNode, element) {
|
|
if (!ignoreFrom || !is.element(element)) {
|
|
return false;
|
|
}
|
|
|
|
if (is.string(ignoreFrom)) {
|
|
return matchesUpTo(element, ignoreFrom, targetNode);
|
|
} else if (is.element(ignoreFrom)) {
|
|
return nodeContains(ignoreFrom, element);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
/**
|
|
* Calls listeners for the given InteractEvent type bound globally
|
|
* and directly to this Interactable
|
|
*
|
|
* @param {InteractEvent} iEvent The InteractEvent object to be fired on this
|
|
* Interactable
|
|
* @return {Interactable} this Interactable
|
|
*/
|
|
|
|
|
|
fire(iEvent) {
|
|
this.events.fire(iEvent);
|
|
return this;
|
|
}
|
|
|
|
_onOff(method, typeArg, listenerArg, options) {
|
|
if (is.object(typeArg) && !is.array(typeArg)) {
|
|
options = listenerArg;
|
|
listenerArg = null;
|
|
}
|
|
|
|
const addRemove = method === 'on' ? 'add' : 'remove';
|
|
const listeners = normalizeListeners(typeArg, listenerArg);
|
|
|
|
for (let type in listeners) {
|
|
if (type === 'wheel') {
|
|
type = browser.wheelEvent;
|
|
}
|
|
|
|
for (const listener of listeners[type]) {
|
|
// if it is an action event type
|
|
if (isNonNativeEvent(type, this._actions)) {
|
|
this.events[method](type, listener);
|
|
} // delegated event
|
|
else if (is.string(this.target)) {
|
|
this._scopeEvents[`${addRemove}Delegate`](this.target, this._context, type, listener, options);
|
|
} // remove listener from this Interactable's element
|
|
else {
|
|
this._scopeEvents[addRemove](this.target, type, listener, options);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
/**
|
|
* Binds a listener for an InteractEvent, pointerEvent or DOM event.
|
|
*
|
|
* @param {string | array | object} types The types of events to listen
|
|
* for
|
|
* @param {function | array | object} [listener] The event listener function(s)
|
|
* @param {object | boolean} [options] options object or useCapture flag for
|
|
* addEventListener
|
|
* @return {Interactable} This Interactable
|
|
*/
|
|
|
|
|
|
on(types, listener, options) {
|
|
return this._onOff('on', types, listener, options);
|
|
}
|
|
/**
|
|
* Removes an InteractEvent, pointerEvent or DOM event listener.
|
|
*
|
|
* @param {string | array | object} types The types of events that were
|
|
* listened for
|
|
* @param {function | array | object} [listener] The event listener function(s)
|
|
* @param {object | boolean} [options] options object or useCapture flag for
|
|
* removeEventListener
|
|
* @return {Interactable} This Interactable
|
|
*/
|
|
|
|
|
|
off(types, listener, options) {
|
|
return this._onOff('off', types, listener, options);
|
|
}
|
|
/**
|
|
* Reset the options of this Interactable
|
|
*
|
|
* @param {object} options The new settings to apply
|
|
* @return {object} This Interactable
|
|
*/
|
|
|
|
|
|
set(options) {
|
|
const defaults = this._defaults;
|
|
|
|
if (!is.object(options)) {
|
|
options = {};
|
|
}
|
|
|
|
this.options = clone(defaults.base);
|
|
|
|
for (const actionName_ in this._actions.methodDict) {
|
|
const actionName = actionName_;
|
|
const methodName = this._actions.methodDict[actionName];
|
|
this.options[actionName] = {};
|
|
this.setPerAction(actionName, extend(extend({}, defaults.perAction), defaults.actions[actionName]));
|
|
this[methodName](options[actionName]);
|
|
}
|
|
|
|
for (const setting in options) {
|
|
if (is.func(this[setting])) {
|
|
this[setting](options[setting]);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
/**
|
|
* Remove this interactable from the list of interactables and remove it's
|
|
* action capabilities and event listeners
|
|
*/
|
|
|
|
|
|
unset() {
|
|
if (is.string(this.target)) {
|
|
// remove delegated events
|
|
for (const type in this._scopeEvents.delegatedEvents) {
|
|
const delegated = this._scopeEvents.delegatedEvents[type];
|
|
|
|
for (let i = delegated.length - 1; i >= 0; i--) {
|
|
const {
|
|
selector,
|
|
context,
|
|
listeners
|
|
} = delegated[i];
|
|
|
|
if (selector === this.target && context === this._context) {
|
|
delegated.splice(i, 1);
|
|
}
|
|
|
|
for (let l = listeners.length - 1; l >= 0; l--) {
|
|
this._scopeEvents.removeDelegate(this.target, this._context, type, listeners[l][0], listeners[l][1]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this._scopeEvents.remove(this.target, 'all');
|
|
}
|
|
}
|
|
|
|
}
|
|
//# sourceMappingURL=Interactable.js.map
|