UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

592 lines (488 loc) 15.3 kB
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; } }