UNPKG

forceify

Version:

Simple, yet powerful touch-force handler

609 lines (598 loc) 17.2 kB
/* globals define, requestAnimationFrame, cancelAnimationFrame, performance */ (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory) } else if (typeof module !== 'undefined' && module.exports) { module.exports = factory() } else if (typeof exports !== 'undefined') { exports.default = factory() exports.__esModule = true } else if (typeof window !== 'undefined' && window.document) { window.Forceify = factory() window.Forceify.__esModule = true } else { this.Forceify = factory() this.Forceify.__esModule = true } })(function () { let tasks = [] let id = {} /* Small shim for lighter size */ let last = Date.now() let reqAnimFrame = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : fn => setTimeout(() => fn(Date.now() - last), 50) let cancelAnimFrame = typeof cancelAnimationFrame !== 'undefined' ? cancelAnimationFrame : fn => clearTimeout(fn) if (typeof performance === 'object' && !performance.now) { performance.now = () => Date.now() - last } let now = () => { return typeof performance !== 'undefined' && !!performance.now ? performance.now() : Date.now() - last } let root = typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof exports !== 'undefined' ? exports : this reqAnimFrame(function update (time) { reqAnimFrame(update) let i = 0 while (i < tasks.length) { let task = tasks[i] if (task.update(time)) { i++ } else { tasks.splice(i, 1) } } }) function _default (value, def) { if (value === null || value === undefined) { return def } return value } let _id = 0 class TimingUpdate { constructor (queueID, elem, self) { this._queueID = queueID this._onUpdate = null this._duration = 200 this._delayTime = 0 this._startTime = null this._id = _id++ this.startValue = 0 this.currentValue = { force: 0, target: elem } this.__self = self this.endValue = 1 this.callElem = elem } duration (amount) { this._duration = amount return this } start () { const { _queueID, currentValue } = this let queue = id[_queueID] if (queue) { let i = 0 let queueItem while ((queueItem = queue.shift())) { let idx = tasks.indexOf(queueItem) if (idx > -1) { tasks.splice(i, 1) } } } else { queue = id[_queueID] = [] } this.startValue = currentValue.force this.endValue = 1 queue.push(this) tasks.push(this) this._startTime = now() + this._delayTime return this } delay (amount) { this._delayTime = amount return this } onUpdate (callback) { this._onUpdate = callback return this } restart (asReverse) { let { _delayTime, currentValue } = this let i = tasks.indexOf(this) if (i === -1) { this.start() } else { this._startTime = now() + _delayTime } if (asReverse) { this.startValue = currentValue.force this.endValue = 0 } return this } update (time) { let { _startTime, _duration, _onUpdate, currentValue, startValue, endValue, callElem, __self } = this if (time < _startTime) { return true } let elapsed = (time - _startTime) / _duration elapsed = elapsed > 1 ? 1 : elapsed currentValue.force = __self.__force !== undefined && __self.__force !== 0 && __self.__force !== 1 ? __self.__force : startValue + (endValue - startValue) * elapsed if (_onUpdate) { _onUpdate.call(callElem, currentValue) } if (elapsed === 1) { return false } return true } } let navig = typeof navigator !== 'undefined' ? navigator : { userAgent: '', maxTouchPoints: 0, msMaxTouchPoints: 0 } let ua = navig.userAgent let _isIOSDevices = ua.indexOf('; CPU') !== -1 && ua.indexOf(' like Mac') !== -1 let _document = !root.document ? {} : root.document let _isTouchSimulate = (_document.body && 'ontouchend' in _document.body) || root.DocumentTouch || navig.maxTouchPoints > 0 || navig.msMaxTouchPoints > 0 let _isReal3DTouch = _document.body && 'ontouchforcechange' in _document.body && _isIOSDevices function getTouch (e, targ, changed) { let touches = changed ? e.touches : e.changedTouches if (touches) { let i = 0 const maxLen = touches.length if (maxLen > 1) { if (e.scale && e.scale !== 1) { return null } } if (e && e.preventDefault) { e.preventDefault() } while (i < maxLen) { if (!!touches[i] && touches[i].target === targ) { return touches[i] } i++ } } return null } return class Forceify { constructor (el, params = {}) { let forceifyID = 0 if (!el.forceifyQueueId) { forceifyID = el.forceifyQueueId = Math.floor( Date.now() + Math.random() * 1000 ) } else { forceifyID = el.forceifyQueueId } this.id = forceifyID this._callback = null this.el = el this._pressDuration = _default(params.pressDuration, 200) this._leaveDurationTolerance = _default( params.leaveDurationTolerance, 0.35 ) this._leaveDuration = this._pressDuration * this._leaveDurationTolerance this._delay = _default(params.delay, 0) this._eventPress = null this._eventLeave = null this._eventUp = null this._checkResult = null this._useSameDurInLeave = _default(params.useSameDurInLeave, false) this._resetOnLeave = _default(params.resetOnLeave, true) this.el = el this.polyfill = _default(params.polyfill, true) this._touchTicks = 0 this.tick = null return this } getEnv () { return this._checkResult } resetOnLeave (state = true) { this._resetOnLeave = state return this } useLongPressEqualDuration (state = true) { this._useSameDurInLeave = state return this } setLongPressLeaveTolerance (amount) { this._leaveDurationTolerance = amount this._leaveDuration = this._pressDuration * amount return this } setLongPressDelay (amount) { this._delay = amount return this } setLongPressDuration (amount) { this._pressDuration = amount this._leaveDuration = amount * this._leaveDurationTolerance return this } onForce (fn) { this._callback = fn return this.init() } on (eventName, callbackListener, capture) { if (root.addEventListener) { this.el.addEventListener(eventName, callbackListener, capture) } return this } off (eventName, callbackListener, capture) { if (root.removeEventListener) { this.el.removeEventListener(eventName, callbackListener, capture) } return this } preventTouchCallout () { const el = this.el const touchCallout = [ 'webkitTouchCallout', 'MozTouchCallout', 'msTouchCallout', 'touchCallout', 'webkitUserSelect', 'MozUserSelect', 'msUserSelect', 'userSelect', 'webkitUserDrag', 'MozUserDrag', 'msUserDrag', 'userDrag' ] for (let property of touchCallout) { if (property in el.style) { el.style[property] = 'none' } } const touchActions = [ 'webkitTouchAction', 'mozTouchAction', 'msTouchAction', 'touchAction' ] for (let property of touchActions) { if (property in el.style) { el.style[property] = 'manipulation' } } return this } handleForceChange (e) { if (e.preventDefault) { e.preventDefault() } if (e.stopPropagation) { e.stopPropagation() } this._iterateOfHandleForceChange++ let force = e.webkitForce !== undefined ? e.webkitForce > 1 ? (e.webkitForce - 1) / 2 : 0 : e.force !== undefined ? e.force : undefined if (force > 0.999) { force = 1 } else if (force < 0.003) { force = 0 } if (force === undefined) { let touches = getTouch(e, this.el, true) if (!touches) { force = 0 } else { if (touches.force !== undefined) { force = touches.force } else if (touches.webkitForce) { force = touches.force } } } const { type } = e if ( force > 0 && force < 1 && !this.nativeAPIExists && (type === 'touchforcechange' || type === 'webkitmouseforcechanged') ) { this.nativeAPIExists = true } e.force = force this.__force = force this._calledTimeout = true this._callback.call(this, e); // eslint-disable-line return false } init () { const { el, polyfill } = this const isPointerSupported = 'onpointerdown' in el const __self$1 = this this._iterateOfHandleForceChange = 0 let tickForce let perfNow let currentEvent // Rebind to self this.handleForceChange = this.handleForceChange.bind(this) const renderUntilBecomeZero = time => { const { __force } = this let force = (1 - Math.min((time - perfNow) / this._leaveDuration, 1)) * __force if (force > 0) { tickForce = reqAnimFrame(renderUntilBecomeZero) } else if (force === 0) { cancelAnimFrame(tickForce) } currentEvent.force = force this.__force = force this.handleForceChange(currentEvent) } this.preventTouchCallout() if (!_isReal3DTouch && 'onwebkitmouseforcechanged' in el) { this.on('webkitmouseforcechanged', this.handleForceChange) this.on('mousedown', function checkForceTouchVerify () { if (__self$1.nativeAPIExists) { __self$1.off('mousedown', checkForceTouchVerify) } else { if (__self$1._touchTicks === 0 && polyfill) { _isReal3DTouch = false __self$1._eventPress = 'mousedown' __self$1._eventLeave = 'mouseleave' __self$1._eventUp = 'mouseup' __self$1._checkResult = root.chrome ? 'macOS Chrome' : 'macOS Safari' __self$1.isPressed = true __self$1.handleSimulate() __self$1.handlePress() } __self$1.off('mousedown', checkForceTouchVerify) } }) this.on('mouseup', function onMousUp (e) { if (__self$1.nativeAPIExists) { __self$1.off('mouseup', onMousUp) } else { const { __force: force, _iterateOfHandleForceChange } = __self$1 currentEvent = e if (force > 0 && _iterateOfHandleForceChange > 1) { perfNow = performance.now() tickForce = reqAnimFrame(renderUntilBecomeZero) } else { __self$1.off('mouseup', onMousUp) } } }) this._checkResult = 'macOSForce' return this } else if (_isReal3DTouch) { let verify3DTouch = () => { if (this.nativeAPIExists) { this.off('touchstart', verify3DTouch) } else { if (this._touchTicks === 0 && polyfill) { _isReal3DTouch = false this._eventPress = 'touchstart' this._eventLeave = 'touchleave' this._eventUp = 'touchend' this._checkResult = root.chrome ? 'ChromeMobile' : 'Touch' this.isPressed = true this.handleSimulate() this.handlePress() } this.off('touchstart', verify3DTouch) } } const onTouchEnd = e => { if (this.nativeAPIExists) { this.off('touchend', onTouchEnd) } else { const { __force: force, _iterateOfHandleForceChange } = this currentEvent = e if (force > 0 && _iterateOfHandleForceChange > 1) { perfNow = performance.now() tickForce = reqAnimFrame(renderUntilBecomeZero) } else { this.off('touchend', onTouchEnd) } } } this.on('touchforcechange', this.handleForceChange) this.on('touchstart', verify3DTouch) this.on('touchend', onTouchEnd) this._checkResult = 'iOSForce' return this } else if (polyfill && isPointerSupported) { this._eventPress = 'pointerdown' this._eventLeave = 'pointerleave' this._eventUp = 'pointerup' this._checkResult = root.chrome ? 'Chrome' : 'Modern' } else if (polyfill && 'onmspointerdown' in el) { this._eventPress = 'mspointerdown' this._eventLeave = 'mspointerleave' this._eventUp = 'mspointerup' this._checkResult = root.chrome ? 'Chrome' : 'Modern' } else if (polyfill && _isTouchSimulate) { this._eventPress = 'touchstart' this._eventLeave = 'touchleave' this._eventUp = 'touchend' this._checkResult = root.chrome ? 'ChromeMobile' : 'Touch' } else if (polyfill && 'onmousedown' in el) { this._eventPress = 'mousedown' this._eventLeave = 'mouseup' this._eventUp = 'mouseleave' this._checkResult = root.chrome ? 'Chrome' : 'Desktop' } this.isPressed = false return this.handleSimulate() } handleLeave () { let { _simulatedCallback, _useSameDurInLeave, _pressDuration, _leaveDuration } = this if (_simulatedCallback) { _simulatedCallback .duration(_useSameDurInLeave ? _pressDuration : _leaveDuration) .delay(0) .restart(true) } return this } handlePress () { let { _simulatedCallback, _pressDuration, _delay, nativeAPIExists } = this if (_simulatedCallback && !nativeAPIExists) { _simulatedCallback .duration(_pressDuration) .delay(_delay) .start() } return this } handleSimulate () { let { _simulatedCallback, _eventPress, _eventUp, _eventLeave, isPressed, _callback, id: forceifyID, el } = this if (!_simulatedCallback) { _simulatedCallback = this._simulatedCallback = new TimingUpdate( forceifyID, el, this ) } if (_simulatedCallback) { _simulatedCallback.onUpdate(_callback) } const pressListener = e => { if (this.nativeAPIExists) { this.off(_eventPress, pressListener) return } if (!isPressed && !_isReal3DTouch) { if (e.type === _eventPress) { if (e.preventDefault) { e.preventDefault() } if (e.stopPropagation) { e.stopPropagation() } this.handlePress() isPressed = this.isPressed = true } } return false } // LONG PRESS this.on(_eventPress, pressListener) // LEAVE const leaveListener = e => { if (this.nativeAPIExists) { this.off(_eventUp, leaveListener) this.off(_eventLeave, leaveListener) return } if (isPressed) { if ( e.type === _eventUp || (e.type === _eventLeave && this._resetOnLeave) ) { if (e.preventDefault) { e.preventDefault() } if (e.stopPropagation) { e.stopPropagation() } this.handleLeave() isPressed = this.isPressed = false } } return false } this.on('contextmenu', e => e.preventDefault()) this.on(_eventUp, leaveListener) this.on(_eventLeave, leaveListener) return this } isChrome () { return ( this._checkResult === 'Chrome' || this._checkResult === 'ChromeMobile' ) } isMacOSForceTouch () { return this._checkResult === 'macOSForce' } isIOS3DTouch () { return this._checkResult === 'iOSForce' } isTouch () { return this._checkResult === 'Touch' } isMouse () { return this._checkResult === 'Desktop' } } })