@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
592 lines (488 loc) • 15.3 kB
JavaScript
import { applyEventMap, eventMap } from './event-map';
import { on, off } from './event-utils';
import getSupportedFeatures from './get-supported-features';
import noop from './noop';
import now from './now';
import grep from './grep';
import Observable from './observable';
const extend = Object.assign;
const preventDefault = (e) => {
e.preventDefault();
};
let
DEFAULT_MIN_HOLD = 800,
CLICK_DELAY = 300,
DEFAULT_THRESHOLD = 0,
PRESS = 'press',
HOLD = 'hold',
SELECT = 'select',
START = 'start',
MOVE = 'move',
END = 'end',
CANCEL = 'cancel',
TAP = 'tap',
DOUBLETAP = 'doubleTap',
RELEASE = 'release',
GESTURESTART = 'gesturestart',
GESTURECHANGE = 'gesturechange',
GESTUREEND = 'gestureend',
GESTURETAP = 'gesturetap';
let THRESHOLD = {
'api': 0,
'touch': 0,
'mouse': 9,
'pointer': 9
};
function touchDelta(touch1, touch2) {
let x1 = touch1.x.location,
y1 = touch1.y.location,
x2 = touch2.x.location,
y2 = touch2.y.location,
dx = x1 - x2,
dy = y1 - y2;
return {
center: {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
},
distance: Math.sqrt(dx * dx + dy * dy)
};
}
function getTouches(e) {
let touches = [],
originalEvent = e.originalEvent || e,
currentTarget = e.currentTarget;
if (e.api) {
touches.push({
id: 2, // hardcoded ID for API call
event: e,
target: e.target,
currentTarget: e.target,
location: e,
type: 'api'
});
} else {
touches.push({
location: originalEvent,
event: e,
target: e.target,
currentTarget: currentTarget,
id: originalEvent.pointerId,
type: 'pointer'
});
}
return touches;
}
class TouchAxis {
constructor(axis, location) {
let that = this;
that.support = getSupportedFeatures();
that.invalidZeroEvents = this.support.mobileOS && this.support.mobileOS.android;
that.axis = axis;
that._updateLocationData(location);
that.startLocation = that.location;
that.velocity = that.delta = 0;
that.timeStamp = now();
}
move(location) {
let that = this,
offset = location['page' + that.axis],
timeStamp = now(),
timeDelta = timeStamp - that.timeStamp || 1;
if (!offset && this.invalidZeroEvents) {
return;
}
that.delta = offset - that.location;
that._updateLocationData(location);
that.initialDelta = offset - that.startLocation;
that.velocity = that.delta / timeDelta;
that.timeStamp = timeStamp;
}
_updateLocationData(location) {
let that = this,
axis = that.axis;
that.location = location['page' + axis];
that.client = location['client' + axis];
that.screen = location['screen' + axis];
}
}
class Touch {
constructor(userEvents, target, touchInfo) {
extend(this, {
x: new TouchAxis('X', touchInfo.location),
y: new TouchAxis('Y', touchInfo.location),
type: touchInfo.type,
threshold: userEvents.threshold || THRESHOLD[touchInfo.type],
userEvents: userEvents,
target: target,
currentTarget: touchInfo.currentTarget,
initialTouch: touchInfo.target,
id: touchInfo.id,
pressEvent: touchInfo,
_clicks: userEvents._clicks,
supportDoubleTap: userEvents.supportDoubleTap,
_moved: false,
_finished: false
});
}
press() {
this._holdTimeout = setTimeout(() => this._hold(), this.userEvents.minHold);
this._trigger(PRESS, this.pressEvent);
}
_tap(touchInfo) {
let that = this;
that.userEvents._clicks++;
if (that.userEvents._clicks === 1) {
that._clickTimeout = setTimeout(function() {
if (that.userEvents._clicks === 1) {
that._trigger(TAP, touchInfo);
} else {
that._trigger(DOUBLETAP, touchInfo);
}
that.userEvents._clicks = 0;
}, CLICK_DELAY);
}
}
_hold() {
this._trigger(HOLD, this.pressEvent);
}
/* eslint-disable consistent-return */
move(touchInfo) {
let that = this;
let preventMove = touchInfo.type !== 'api' && that.userEvents._shouldNotMove;
if (that._finished || preventMove) {
return;
}
that.x.move(touchInfo.location);
that.y.move(touchInfo.location);
if (!that._moved) {
if (that._withinIgnoreThreshold()) {
return;
}
if (!UserEvents.current || UserEvents.current === that.userEvents) {
that._start(touchInfo);
} else {
return that.dispose();
}
}
if (!that._finished) {
that._trigger(MOVE, touchInfo);
}
}
/* eslint-enable consistent-return */
end(touchInfo) {
this.endTime = now();
if (this._finished) {
return;
}
this._finished = true;
this._trigger(RELEASE, touchInfo);
if (this._moved) {
this._trigger(END, touchInfo);
} else {
if (this.supportDoubleTap) {
this._tap(touchInfo);
} else {
this._trigger(TAP, touchInfo);
}
}
clearTimeout(this._holdTimeout);
this.dispose();
}
dispose() {
let userEvents = this.userEvents,
activeTouches = userEvents.touches || [];
this._finished = true;
this.pressEvent = null;
clearTimeout(this._holdTimeout);
// activeTouches.splice($.inArray(this, activeTouches), 1);
const activeTouchIndex = activeTouches.indexOf(this);
activeTouches.splice(activeTouchIndex, 1);
}
skip() {
this.dispose();
}
cancel() {
this.dispose();
}
isMoved() {
return this._moved;
}
_start(touchInfo) {
clearTimeout(this._holdTimeout);
this.startTime = now();
this._moved = true;
this._trigger(START, touchInfo);
}
_trigger(name, touchInfo) {
const e = touchInfo.event;
const data = {
touch: this,
x: this.x,
y: this.y,
target: this.target,
event: e
};
if (this.userEvents.notify(name, data)) {
e.preventDefault();
}
}
_withinIgnoreThreshold() {
let xDelta = this.x.initialDelta,
yDelta = this.y.initialDelta;
return Math.sqrt(xDelta * xDelta + yDelta * yDelta) <= this.threshold;
}
}
function withEachUpEvent(callback) {
let downEvents = eventMap.up.split(' '),
idx = 0,
length = downEvents.length;
for (; idx < length; idx++) {
callback(downEvents[idx]);
}
}
export default class UserEvents extends Observable {
constructor(element, options) {
super();
let that = this;
let filter;
const support = getSupportedFeatures();
this.support = support;
/* eslint-disable no-param-reassign */
options = options || {};
/* eslint-enable no-param-reassign */
this.options = options;
filter = that.filter = options.filter;
that.threshold = options.threshold || DEFAULT_THRESHOLD;
that.minHold = options.minHold || DEFAULT_MIN_HOLD;
that.touches = [];
that._maxTouches = options.multiTouch ? 2 : 1;
that.allowSelection = options.allowSelection;
that.captureUpIfMoved = options.captureUpIfMoved;
that._clicks = 0;
that.supportDoubleTap = options.supportDoubleTap;
extend(that, {
element: element,
surface: options.surface || element,
stopPropagation: options.stopPropagation,
pressed: false
});
this._surfaceMoveHandler = this._move.bind(this);
on(that.surface, applyEventMap('move'), this._surfaceMoveHandler);
this._surfaceEndHandler = this._end.bind(this);
on(that.surface, applyEventMap('up cancel'), this._surfaceEndHandler);
this._elementStartHandler = this._start.bind(this);
on(element, applyEventMap('down'), filter, this._elementStartHandler);
element.style['touch-action'] = options.touchAction || 'none';
if (options.preventDragEvent) {
this._elementDragStartHandler = preventDefault;
on(element, applyEventMap('dragstart'), this._elementDragStartHandler);
}
// element.on(kendo.applyEventMap('mousedown'), filter, {
// root: element
// } '_select');
// todo: use root
this._elementSelectHandler = this._select.bind(this);
on(element, applyEventMap('mousedown'), filter, this._elementSelectHandler);
if (that.captureUpIfMoved) {
let surfaceElement = that.surface;
that._preventIfMovingProxy = that.preventIfMoving.bind(that);
withEachUpEvent(function(eventName) {
surfaceElement.addEventListener(eventName, that._preventIfMovingProxy, true);
});
}
that.bind([
PRESS,
HOLD,
TAP,
DOUBLETAP,
START,
MOVE,
END,
RELEASE,
CANCEL,
GESTURESTART,
GESTURECHANGE,
GESTUREEND,
GESTURETAP,
SELECT
], options);
}
preventIfMoving(e) {
if (this._isMoved()) {
e.preventDefault();
}
}
destroy() {
let that = this;
const options = this.options;
const element = this.element;
if (that._destroyed) {
return;
}
that._destroyed = true;
if (that.captureUpIfMoved) {
let surfaceElement = that.surface;
withEachUpEvent(function(eventName) {
surfaceElement.removeEventListener(eventName, that._preventIfMovingProxy, true);
});
}
off(that.surface, applyEventMap('move'), this._surfaceMoveHandler);
off(that.surface, applyEventMap('up cancel'), this._surfaceEndHandler);
off(element, applyEventMap('down'), this._elementStartHandler);
if (options.preventDragEvent) {
off(element, applyEventMap('dragstart'), this._elementDragStartHandler);
}
off(element, applyEventMap('mousedown'), this._elementSelectHandler);
that._disposeAll();
that.unbind();
delete that.surface;
delete that.element;
delete that.currentTarget;
}
capture() {
UserEvents.current = this;
}
cancel() {
this._disposeAll();
this.trigger(CANCEL);
}
notify(event, data) {
let that = this,
touches = that.touches;
let eventName = event;
if (this._isMultiTouch()) {
switch (eventName) {
case MOVE:
eventName = GESTURECHANGE;
break;
case END:
eventName = GESTUREEND;
break;
case TAP:
eventName = GESTURETAP;
break;
default:
break;
}
extend(data, {
touches: touches
}, touchDelta(touches[0], touches[1]));
}
return this.trigger(eventName, extend(data, {
type: eventName
}));
}
press(x, y, target) {
this._apiCall('_start', x, y, target);
}
move(x, y) {
this._apiCall('_move', x, y);
}
end(x, y) {
this._apiCall('_end', x, y);
}
_isMultiTouch() {
return this.touches.length > 1;
}
_maxTouchesReached() {
return this.touches.length >= this._maxTouches;
}
_disposeAll() {
let touches = this.touches;
while (touches.length > 0) {
touches.pop().dispose();
}
}
_isMoved() {
return grep(this.touches, function(touch) {
return touch.isMoved();
}).length;
}
_select(e) {
if (!this.allowSelection || this.trigger(SELECT, { event: e })) {
e.preventDefault();
}
}
_start(e) {
if (e.which && e.which > 1 || this._maxTouchesReached()) {
return;
}
UserEvents.current = null;
this.currentTarget = e.currentTarget;
if (this.stopPropagation) {
e.stopPropagation();
}
let target;
const eventTouches = getTouches(e);
for (let idx = 0; idx < eventTouches.length; idx++) {
if (this._maxTouchesReached()) {
break;
}
const eventTouch = eventTouches[idx];
if (this.filter) {
target = eventTouch.currentTarget;
} else {
target = this.element;
}
if (target && target.length === 0) {
continue;
}
const touch = new Touch(this, target, eventTouch);
this.touches.push(touch);
touch.press();
if (this._isMultiTouch()) {
this.notify('gesturestart', {});
}
}
}
_move(e) {
this._eachTouch('move', e);
}
_end(e) {
this._eachTouch('end', e);
}
_eachTouch(methodName, e) {
let that = this,
dict = {},
touches = getTouches(e),
activeTouches = that.touches,
idx,
touch,
touchInfo,
matchingTouch;
for (idx = 0; idx < activeTouches.length; idx++) {
touch = activeTouches[idx];
dict[touch.id] = touch;
}
for (idx = 0; idx < touches.length; idx++) {
touchInfo = touches[idx];
matchingTouch = dict[touchInfo.id];
if (matchingTouch) {
const shouldCapture = methodName === 'move' && touchInfo.type === 'pointer' && !this.surface.hasPointerCapture(touchInfo.id);
if (shouldCapture) {
this.surface.setPointerCapture(touchInfo.id);
}
matchingTouch[methodName](touchInfo);
}
}
}
_apiCall(type, x, y, target) {
this[type]({
api: true,
pageX: x,
pageY: y,
clientX: x,
clientY: y,
target: target || this.element,
stopPropagation: noop,
preventDefault: noop
});
}
static defaultThreshold(value) {
DEFAULT_THRESHOLD = value;
}
static minHold(value) {
DEFAULT_MIN_HOLD = value;
}
}