iwmlib/lib/interaction.js

1186 lines
37 KiB
JavaScript
Raw Normal View History

/* eslint-disable no-unused-vars */
2019-03-21 09:57:27 +01:00
/* globals Hammer, propagating */
/*eslint no-console: ["error", { allow: ["log", "warn", "info", "error"] }]*/
import Interface from './interface.js'
import { Points, MapProxy } from './utils.js'
2019-03-21 09:57:27 +01:00
import Events from './events.js'
2019-05-24 09:53:27 +02:00
import Logging from './logging.js'
2019-03-21 09:57:27 +01:00
/** Interaction patterns
See interaction.html for explanation
*/
export class IInteractionTarget extends Interface {
capture(event) {
return typeof true
}
2019-07-18 12:26:39 +02:00
onStart(event, interaction) {}
onMove(event, interaction) {}
onEnd(event, interaction) {}
2019-03-21 09:57:27 +01:00
2019-07-18 12:26:39 +02:00
onMouseWheel(event) {}
2019-03-21 09:57:27 +01:00
}
export class IInteractionMapperTarget extends Interface {
capture(event) {
return typeof true
}
findTarget(event, local, global) {
return IInteractionTarget
}
}
export class PointMap extends MapProxy {
// Collects touch points, mouse coordinates, etc. as key value pairs.
// Keys are pointer and touch ids, the special "mouse" key.
// Values are points, i.e. all objects with numeric x and y properties.
constructor(points = {}) {
super()
for (let key in points) {
this.set(key, points[key])
}
}
toString() {
let points = []
for (let key of this.keys()) {
let value = this.get(key)
points.push(`${key}:{x:${value.x}, y:${value.y}}`)
}
let attrs = points.join(', ')
return `[PointMap ${attrs}]`
}
clone() {
let result = new PointMap()
for (let key of this.keys()) {
let value = this.get(key)
result.set(key, { x: value.x, y: value.y })
}
return result
}
keyOf(value) {
for (let key of this.keys()) {
let p = this.get(key)
if (p.x == value.x && p.y == value.y) {
return key
}
}
return null
}
firstKey() {
for (let key of this.keys()) {
return key
}
return null
}
first() {
for (let key of this.keys()) {
return this.get(key)
}
return null
}
farthests() {
if (this.size == 0) {
return null
}
let pairs = []
for (let key of this.keys()) {
let p = this.get(key)
p.key = key
for (let k of this.keys()) {
let q = this.get(k)
q.key = k
pairs.push([p, q])
}
}
let sorted = pairs.sort((a, b) => {
return Points.distance(b[0], b[1]) - Points.distance(a[0], a[1])
})
return sorted[0]
}
mean() {
if (this.size == 0) {
return null
}
let x = 0.0,
y = 0.0
for (let p of this.values()) {
x += p.x
y += p.y
}
return { x: x / this.size, y: y / this.size }
}
}
export class InteractionDelta {
/**
*Creates an instance of InteractionDelta.
* @param {*} x
* @param {*} y
* @param {*} zoom
* @param {*} rotate
* @param {*} about
* @param {*} number - number of involved pointer
* @param {*} distance - distance of farthests touch points
* @memberof InteractionDelta
*/
constructor(x, y, zoom, rotate, about, number, distance) {
2019-03-21 09:57:27 +01:00
this.x = x
this.y = y
this.zoom = zoom
this.rotate = rotate
this.about = about
this.number = number
this.distance = distance
2019-03-21 09:57:27 +01:00
}
toString() {
let values = []
for (let key of Object.keys(this)) {
let value = this[key]
if (key == 'about') {
values.push(`${key}:{x:${value.x}, y:${value.y}}`)
} else {
values.push(`${key}:${value}`)
}
}
let attrs = values.join(', ')
return `[InteractionDelta ${attrs}]`
}
}
export class InteractionPoints {
constructor(parent = null) {
this.parent = parent
this.current = new PointMap()
this.previous = new PointMap()
this.start = new PointMap()
this.ended = new PointMap()
this.timestamps = new Map()
}
moved(key) {
let current = this.current.get(key)
let previous = this.previous.get(key)
return Points.subtract(current, previous)
}
move() {
let current = this.current.mean()
let previous = this.previous.mean()
return Points.subtract(current, previous)
}
/**
* Computes the delta between previous and current angles. Corrects
* value that are larger than 45°
* @param {*} a
* @param {*} b
* @returns delta
*/
diffAngle(a, b) {
let alpha = Math.atan2(Math.sin(a - b), Math.cos(a - b))
if (Math.abs(alpha) > Math.PI / 4) {
alpha -= Math.PI
}
return alpha
}
/**
* Computes the delta between interaction points at t and t+1.
*
* @returns InteractionDelta
* @memberof InteractionPoints
*/
delta() {
let prev = []
let curr = []
2019-07-30 16:56:29 +02:00
let cm = { x: 0, y: 0 }
let pm = { x: 0, y: 0 }
let count = 0
2019-07-30 16:56:29 +02:00
for (let key of this.current.keys()) {
if (this.previous.has(key)) {
let p = this.previous.get(key)
let c = this.current.get(key)
pm = Points.add(pm, p)
cm = Points.add(cm, c)
prev.push(p)
curr.push(c)
count += 1
}
}
if (count > 0) {
pm = Points.multiplyScalar(pm, 1 / count)
cm = Points.multiplyScalar(cm, 1 / count)
let delta = Points.subtract(cm, pm)
let scale = 0
let scaled = 0
let alpha = 0
let zoom = 1
2019-07-30 16:56:29 +02:00
for (let i = 0; i < count; i++) {
let p = prev[i]
let c = curr[i]
let previousAngle = Points.angle(p, pm)
let currentAngle = Points.angle(c, cm)
let diff = this.diffAngle(currentAngle, previousAngle)
alpha += diff
let distance1 = Points.distance(p, pm)
let distance2 = Points.distance(c, cm)
if (distance1 != 0 && distance2 != 0) {
scale += distance2 / distance1
scaled += 1
}
}
if (scaled > 0) {
zoom = scale / scaled
}
alpha /= count
let current = this.current.farthests()
let c1 = current[0]
let c2 = current[1]
let distance2 = Points.distance(c1, c2)
2019-07-30 16:56:29 +02:00
return new InteractionDelta(delta.x, delta.y, zoom, alpha, cm, count, distance2)
} else {
return null
}
}
/**
* Computes the delta between interaction points at t and t+1.
*
* @returns InteractionDelta
* @memberof InteractionPoints
*/
deltaByTwoFarthestsPoints() {
2019-03-21 09:57:27 +01:00
let csize = this.current.size
let psize = this.previous.size
if (csize >= 2 && csize == psize) {
// Reduce to the two farthests points
let current = this.current.farthests()
let c1 = current[0]
let c2 = current[1]
let p1 = this.previous.get(c1.key)
let p2 = this.previous.get(c2.key)
let d1 = Points.subtract(c1, p1)
let d2 = Points.subtract(c2, p2)
let cm = Points.mean(c1, c2)
2019-05-31 06:31:09 +02:00
// Using the mean leads to jumps between time slices with 3 and 2 fingers
2019-03-21 09:57:27 +01:00
// We use the mean of deltas instead
let delta = Points.mean(d1, d2)
2019-03-21 09:57:27 +01:00
let zoom = 1.0
let distance1 = Points.distance(p1, p2)
let distance2 = Points.distance(c1, c2)
if (distance1 != 0 && distance2 != 0) {
zoom = distance2 / distance1
}
let currentAngle = Points.angle(c1, c2)
let previousAngle = Points.angle(p1, p2)
let alpha = this.diffAngle(currentAngle, previousAngle)
2019-07-30 16:56:29 +02:00
return new InteractionDelta(delta.x, delta.y, zoom, alpha, cm, csize, distance2)
} else if (csize == 1 && psize == 1 && this.current.firstKey() == this.previous.firstKey()) {
2019-05-31 06:31:09 +02:00
// We need to ensure that the keys are the same, since single points with different keys
// can jump
2019-03-21 09:57:27 +01:00
let current = this.current.first()
let previous = this.previous.first()
let delta = Points.subtract(current, previous)
2019-07-30 16:56:29 +02:00
return new InteractionDelta(delta.x, delta.y, 1.0, 0.0, current, csize)
2019-03-21 09:57:27 +01:00
}
return null
}
started(key, point) {
this.current.set(key, point)
this.start.set(key, point)
this.previous.set(key, point)
this.timestamps.set(key, performance.now())
}
update(key, point) {
// Returns true iff the key is new
this.current.set(key, point)
if (!this.start.has(key)) {
this.start.set(key, point)
this.previous.set(key, point)
this.timestamps.set(key, performance.now())
return true
}
return false
}
updatePrevious() {
for (let key of this.current.keys()) {
this.previous.set(key, this.current.get(key))
}
}
stop(key, point) {
if (this.current.has(key)) {
this.current.delete(key)
this.previous.delete(key)
this.ended.set(key, point)
}
}
finish(key, point) {
this.current.delete(key)
this.previous.delete(key)
this.start.delete(key)
this.timestamps.delete(key)
this.ended.delete(key)
}
isFinished() {
return this.current.size == 0
}
isNoLongerTwoFinger() {
return this.previous.size > 1 && this.current.size < 2
}
isTap(key) {
return this.parent.isTap(key)
}
isDoubleTap(key) {
return this.parent.isDoubleTap(key)
}
isLongPress(key) {
return this.parent.isLongPress(key)
}
}
export class Interaction extends InteractionPoints {
constructor(tapDistance = 10, tapDuration = 250.0, longPressTime = 500.0) {
super()
this.tapDistance = tapDistance
this.tapCounts = new Map()
this.tapPositions = new Map()
this.tapTimestamps = new Map()
this.tapDuration = tapDuration
this.longPressTime = longPressTime
this.targets = new Map()
this.subInteractions = new Map() // target:Object : InteractionPoints
}
stop(key, point) {
super.stop(key, point)
for (let points of this.subInteractions.values()) {
points.stop(key, point)
}
}
addTarget(key, target) {
this.targets.set(key, target)
this.subInteractions.set(target, new InteractionPoints(this))
}
removeTarget(key) {
let target = this.targets.get(key)
this.targets.delete(key)
// Only remove target if no keys are refering to the target
let remove = true
for (let t of this.targets.values()) {
if (target === t) {
remove = false
}
}
if (remove) {
this.subInteractions.delete(target)
}
}
finish(key, point) {
super.finish(key, point)
this.removeTarget(key)
}
mapInteraction(points, aspects, mappingFunc) {
// Map centrally registered points to target interactions
// Returns an array of [target, updated subInteraction] pairs
let result = new Map()
for (let key in points) {
if (this.targets.has(key)) {
let target = this.targets.get(key)
if (this.subInteractions.has(target)) {
let interaction = this.subInteractions.get(target)
for (let aspect of aspects) {
let pointMap = this[aspect]
let point = pointMap.get(key)
let mapped = mappingFunc(point, target)
interaction[aspect].set(key, mapped)
}
result.set(target, interaction)
}
}
}
return result
}
registerTap(key, point) {
if (this.tapCounts.has(key)) {
let count = this.tapCounts.get(key)
this.tapCounts.set(key, count + 1)
2019-07-18 12:26:39 +02:00
} else {
2019-03-21 09:57:27 +01:00
this.tapCounts.set(key, 1)
}
this.tapPositions.set(key, point)
this.tapTimestamps.set(key, performance.now())
}
unregisterTap(key) {
this.tapCounts.delete(key)
this.tapPositions.delete(key)
this.tapTimestamps.delete(key)
}
isTap(key) {
let ended = this.ended.get(key)
let start = this.start.get(key)
2019-07-30 16:56:29 +02:00
if (start && ended && Points.distance(ended, start) < this.tapDistance) {
2019-03-21 09:57:27 +01:00
let t1 = this.timestamps.get(key)
let tookLong = performance.now() > t1 + this.longPressTime
if (tookLong) {
return false
}
return true
}
return false
}
isDoubleTap(key) {
let ended = this.ended.get(key)
if (this.tapCounts.has(key) && this.tapCounts.get(key) > 2) {
this.unregisterTap(key)
}
if (this.tapPositions.has(key)) {
let pos = this.tapPositions.get(key)
if (Points.distance(ended, pos) > this.tapDistance) {
this.unregisterTap(key)
}
}
2019-07-30 16:56:29 +02:00
if (this.tapTimestamps.has(key) && performance.now() > this.tapTimestamps.get(key) + this.tapDuration) {
2019-03-21 09:57:27 +01:00
//console.log("tap too long")
this.unregisterTap(key)
}
let result = false
if (this.isTap(key)) {
this.registerTap(key, ended)
result = this.tapCounts.get(key) == 2
2019-07-18 12:26:39 +02:00
} else {
2019-03-21 09:57:27 +01:00
this.unregisterTap(key)
}
//console.log("isDoubleTap", this.tapCounts.get(key), result)
return result
}
isAnyTap() {
for (let key of this.ended.keys()) {
if (this.isTap(key)) return true
}
return false
}
isLongPress(key) {
let ended = this.ended.get(key)
let start = this.start.get(key)
2019-07-30 16:56:29 +02:00
if (start && ended && Points.distance(ended, start) < this.tapDistance) {
2019-03-21 09:57:27 +01:00
let t1 = this.timestamps.get(key)
let tookLong = performance.now() > t1 + this.longPressTime
if (tookLong) {
return true
}
return false
}
return false
}
isAnyLongPress() {
for (let key of this.ended.keys()) {
if (this.isLongPress(key)) return true
}
return false
}
isStylus(key) {
return key === 'stylus'
}
}
/**
* This class implements the main delegate functionality: All necessary event handlers are registered for the
* given element. Uses PointerEvents if available or TouchEvents on iOS. The fallback is on mouse events.
* Collects the events if the interaction target captures the start event (i.e. declares that
* the target wants the start event as well as all following move and end evcents.)
*
* @export
* @class InteractionDelegate
*/
export class InteractionDelegate {
// Long press: http://stackoverflow.com/questions/1930895/how-long-is-the-event-onlongpress-in-the-android
// Stylus support: https://w3c.github.io/touch-events/
/**
* Creates an instance of InteractionDelegate.
* @param {any} element
* @param {any} target
* @param {any} [{ mouseWheelElement = null, useCapture = true, capturePointerEvents = true, debug = false }={}]
* @memberof InteractionDelegate
*/
constructor(
element,
target,
2019-07-18 12:26:39 +02:00
{
mouseWheelElement = null,
useCapture = true,
capturePointerEvents = true,
cancelOnWindowOut = true,
debug = false
} = {}
2019-03-21 09:57:27 +01:00
) {
this.debug = debug
this.interaction = new Interaction()
this.element = element
this.mouseWheelElement = mouseWheelElement || element
this.target = target
this.useCapture = useCapture
this.capturePointerEvents = capturePointerEvents
this.cancelOnWindowOut = cancelOnWindowOut
this.setupInteraction()
}
setupInteraction() {
if (this.debug) {
2019-07-30 16:56:29 +02:00
let error = this.targetInterface.implementationError(this.target.constructor)
2019-03-21 09:57:27 +01:00
if (error != null) {
throw new Error('Expected IInteractionTarget: ' + error)
}
}
this.setupTouchInteraction()
this.setupMouseWheelInteraction()
}
get targetInterface() {
return IInteractionTarget
}
setupTouchInteraction() {
let element = this.element
let useCapture = this.useCapture
if (window.PointerEvent) {
if (this.debug) console.log('Pointer API' + window.PointerEvent)
element.addEventListener(
'pointerdown',
e => {
if (this.debug) console.log('pointerdown', e.pointerId)
if (this.capture(e)) {
if (this.capturePointerEvents) {
try {
element.setPointerCapture(e.pointerId)
} catch (e) {
console.warn('Cannot setPointerCapture')
}
2019-03-21 09:57:27 +01:00
}
this.onStart(e)
}
},
useCapture
)
element.addEventListener(
'pointermove',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointermove', e.pointerId, e.pointerType)
2019-03-21 09:57:27 +01:00
2019-07-30 16:56:29 +02:00
if (e.pointerType == 'touch' || (e.pointerType == 'mouse' && Events.isPointerDown(e))) {
2019-03-21 09:57:27 +01:00
// this.capture(e) &&
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointermove captured', e.pointerId)
2019-03-21 09:57:27 +01:00
this.onMove(e)
}
},
useCapture
)
element.addEventListener(
'pointerup',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointerup', e.pointerId, e.pointerType)
2019-03-21 09:57:27 +01:00
this.onEnd(e)
if (this.capturePointerEvents) {
try {
element.releasePointerCapture(e.pointerId)
} catch (e) {
console.warn('Cannot release pointer')
}
2019-03-21 09:57:27 +01:00
}
},
useCapture
)
element.addEventListener(
'pointercancel',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointercancel', e.pointerId, e.pointerType)
2019-03-21 09:57:27 +01:00
this.onEnd(e)
2019-07-30 16:56:29 +02:00
if (this.capturePointerEvents) element.releasePointerCapture(e.pointerId)
2019-03-21 09:57:27 +01:00
},
useCapture
)
if (!this.capturePointerEvents) {
element.addEventListener(
'pointerleave',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointerleave', e.pointerId, e.pointerType)
2019-03-21 09:57:27 +01:00
if (e.target == element) this.onEnd(e)
},
useCapture
)
}
if (!this.capturePointerEvents) {
element.addEventListener(
'pointerout',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointerout', e.pointerId, e.pointerType)
2019-03-21 09:57:27 +01:00
if (e.target == element) this.onEnd(e)
},
useCapture
)
}
if (this.cancelOnWindowOut) {
window.addEventListener(
'pointerout',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('pointerout', e.pointerId, e.pointerType, e.target)
2019-03-21 09:57:27 +01:00
if (e.target == element) {
this.onEnd(e)
2019-03-21 09:57:27 +01:00
}
},
2019-07-18 12:26:39 +02:00
useCapture
)
2019-03-21 09:57:27 +01:00
}
} else if (window.TouchEvent) {
if (this.debug) console.log('Touch API')
element.addEventListener(
'touchstart',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('touchstart', this.touchPoints(e))
2019-03-21 09:57:27 +01:00
if (this.capture(e)) {
for (let touch of e.changedTouches) {
this.onStart(touch)
}
}
},
useCapture
)
element.addEventListener(
'touchmove',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('touchmove', this.touchPoints(e), e)
2019-03-21 09:57:27 +01:00
for (let touch of e.changedTouches) {
this.onMove(touch)
}
for (let touch of e.targetTouches) {
this.onMove(touch)
}
},
useCapture
)
element.addEventListener(
'touchend',
e => {
if (this.debug) console.log('touchend', this.touchPoints(e))
for (let touch of e.changedTouches) {
this.onEnd(touch)
}
},
useCapture
)
element.addEventListener(
'touchcancel',
e => {
2019-07-30 16:56:29 +02:00
if (this.debug) console.log('touchcancel', e.targetTouches.length, e.changedTouches.length)
2019-03-21 09:57:27 +01:00
for (let touch of e.changedTouches) {
this.onEnd(touch)
}
},
useCapture
)
} else {
if (this.debug) console.log('Mouse API')
element.addEventListener(
'mousedown',
e => {
if (this.debug) console.log('mousedown', e)
if (this.capture(e)) {
this.onStart(e)
}
},
useCapture
)
element.addEventListener(
'mousemove',
e => {
// Dow we only use move events if the mouse is down?
// HOver effects have to be implemented by other means
// && Events.isMouseDown(e))
if (Events.isMouseDown(e)) {
2019-07-18 12:26:39 +02:00
if (this.debug) console.log('mousemove', e)
2019-03-21 09:57:27 +01:00
this.onMove(e)
}
},
useCapture
)
element.addEventListener(
'mouseup',
e => {
if (this.debug) console.log('mouseup', e)
this.onEnd(e)
},
true
)
if (!this.capturePointerEvents) {
element.addEventListener(
'mouseout',
e => {
if (e.target == element) {
this.onEnd(e)
2019-07-30 16:56:29 +02:00
console.warn("Shouldn't happen: mouseout ends interaction")
2019-03-21 09:57:27 +01:00
}
},
useCapture
)
}
if (this.cancelOnWindowOut) {
window.addEventListener(
'mouseout',
e => {
if (e.target == element) {
this.onEnd(e)
}
},
2019-07-18 12:26:39 +02:00
useCapture
)
2019-03-21 09:57:27 +01:00
}
}
}
isDescendant(parent, child) {
if (parent == child) return true
let node = child.parentNode
while (node != null) {
if (node == parent) {
return true
}
node = node.parentNode
}
return false
}
touchPoints(event) {
let result = []
for (let touch of event.changedTouches) {
result.push(this.extractPoint(touch))
}
return result
}
setupMouseWheelInteraction() {
2019-07-30 16:56:29 +02:00
this.mouseWheelElement.addEventListener('mousewheel', this.onMouseWheel.bind(this), true)
this.mouseWheelElement.addEventListener('DOMMouseScroll', this.onMouseWheel.bind(this), true)
2019-03-21 09:57:27 +01:00
}
onMouseWheel(event) {
if (this.capture(event) && this.target.onMouseWheel) {
this.target.onMouseWheel(event)
} else {
//console.warn('Target has no onMouseWheel callback')
}
}
onStart(event) {
let extracted = this.extractPoint(event)
this.startInteraction(event, extracted)
this.target.onStart(event, this.interaction)
}
onMove(event) {
let extracted = this.extractPoint(event, 'all')
this.updateInteraction(event, extracted)
this.target.onMove(event, this.interaction)
this.interaction.updatePrevious()
}
onEnd(event) {
let extracted = this.extractPoint(event, 'changedTouches')
this.endInteraction(event, extracted)
this.target.onEnd(event, this.interaction)
this.finishInteraction(event, extracted)
}
/**
* Asks the target whether the event should be captured
*
* @param {any} event
* @returns {bool}
* @memberof InteractionDelegate
*/
capture(event) {
if (Events.isCaptured(event)) {
return false
}
let captured = this.target.capture(event)
return captured
}
getPosition(event) {
return { x: event.clientX, y: event.clientY }
}
extractPoint(event, touchEventKey = 'all') {
// 'targetTouches'
let result = {}
switch (event.constructor.name) {
2019-07-18 12:26:39 +02:00
case 'MouseEvent': {
let buttons = event.buttons || event.which
if (buttons) result['mouse'] = this.getPosition(event)
break
}
case 'PointerEvent': {
result[event.pointerId.toString()] = this.getPosition(event)
break
}
case 'Touch': {
2019-07-30 16:56:29 +02:00
let id = event.touchType === 'stylus' ? 'stylus' : event.identifier.toString()
2019-07-18 12:26:39 +02:00
result[id] = this.getPosition(event)
break
}
// case 'TouchEvent':
// // Needs to be observed: Perhaps changedTouches are all we need. If so
// // we can remove the touchEventKey default parameter
// if (touchEventKey == 'all') {
// for(let t of event.targetTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// for(let t of event.changedTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// }
// else {
// for(let t of event.changedTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// }
// break
default:
break
2019-03-21 09:57:27 +01:00
}
return result
}
interactionStarted(event, key, point) {
// Callback: can be overwritten
}
interactionEnded(event, key, point) {
// Callback: can be overwritten
}
2019-07-18 12:26:39 +02:00
interactionFinished(event, key, point) {}
2019-03-21 09:57:27 +01:00
startInteraction(event, extracted) {
for (let key in extracted) {
let point = extracted[key]
this.interaction.started(key, point)
this.interactionStarted(event, key, point)
}
}
updateInteraction(event, extracted) {
for (let key in extracted) {
let point = extracted[key]
let updated = this.interaction.update(key, point)
if (updated) {
2019-07-30 16:56:29 +02:00
console.warn("new pointer in updateInteraction shouldn't happen", key)
2019-03-21 09:57:27 +01:00
this.interactionStarted(event, key, point)
}
}
}
endInteraction(event, ended) {
for (let key in ended) {
let point = ended[key]
this.interaction.stop(key, point)
this.interactionEnded(event, key, point)
}
}
finishInteraction(event, ended) {
for (let key in ended) {
let point = ended[key]
this.interaction.finish(key, point)
this.interactionFinished(event, key, point)
}
}
}
/**
* A special InteractionDelegate that maps events to specific parts of
* the interaction target. The InteractionTarget must implement a findTarget
* method that returns an object implementing the IInteractionTarget interface.
*
* If the InteractionTarget also implements a mapPositionToPoint method this
* is used to map the points to the local coordinate space of the the target.
*
* This makes it easier to lookup elements and relate events to local
* positions.
*
* @export
* @class InteractionMapper
* @extends {InteractionDelegate}
*/
export class InteractionMapper extends InteractionDelegate {
constructor(
element,
target,
2019-07-18 12:26:39 +02:00
{
tapDistance = 10,
longPressTime = 500.0,
useCapture = true,
mouseWheelElement = null,
logInteractionsAbove = 12
} = {}
2019-03-21 09:57:27 +01:00
) {
2019-07-18 12:26:39 +02:00
super(element, target, {
tapDistance,
useCapture,
longPressTime,
mouseWheelElement
})
2019-05-24 09:21:02 +02:00
this.logInteractionsAbove = logInteractionsAbove
}
2019-03-21 09:57:27 +01:00
get targetInterface() {
return IInteractionMapperTarget
}
mapPositionToPoint(point, element = null) {
if (this.target.mapPositionToPoint) {
return this.target.mapPositionToPoint(point, element)
}
return point
}
interactionStarted(event, key, point) {
if (this.target.findTarget) {
let local = this.mapPositionToPoint(point)
let found = this.target.findTarget(event, local, point)
if (found != null) {
this.interaction.addTarget(key, found)
}
}
2019-05-24 09:21:02 +02:00
let size = this.interaction.current.size
let limit = this.logInteractionsAbove
if (size > limit) {
2019-05-24 09:53:27 +02:00
Logging.log(`Number of interactions ${size} exceeds ${limit}`)
2019-05-24 09:21:02 +02:00
}
2019-03-21 09:57:27 +01:00
}
onMouseWheel(event) {
if (this.capture(event)) {
if (this.target.findTarget) {
let point = this.getPosition(event)
let local = this.mapPositionToPoint(point)
let found = this.target.findTarget(event, local, point)
if (found != null && found.onMouseWheel) {
found.onMouseWheel(event)
return
}
}
if (this.target.onMouseWheel) {
this.target.onMouseWheel(event)
} else {
//console.warn('Target has no onMouseWheel callback', this.target)
}
}
}
onStart(event) {
let extracted = this.extractPoint(event)
this.startInteraction(event, extracted)
let mapped = this.interaction.mapInteraction(
extracted,
['current', 'start'],
this.mapPositionToPoint.bind(this)
)
for (let [target, interaction] of mapped.entries()) {
target.onStart(event, interaction)
}
}
onMove(event) {
let extracted = this.extractPoint(event, 'all')
this.updateInteraction(event, extracted)
let mapped = this.interaction.mapInteraction(
extracted,
['current', 'previous'],
this.mapPositionToPoint.bind(this)
)
for (let [target, interaction] of mapped.entries()) {
target.onMove(event, interaction)
interaction.updatePrevious()
}
this.interaction.updatePrevious()
}
onEnd(event) {
let extracted = this.extractPoint(event, 'changedTouches')
this.endInteraction(event, extracted)
2019-07-30 16:56:29 +02:00
let mapped = this.interaction.mapInteraction(extracted, ['ended'], this.mapPositionToPoint.bind(this))
2019-03-21 09:57:27 +01:00
for (let [target, interaction] of mapped.entries()) {
target.onEnd(event, interaction)
}
this.finishInteraction(event, extracted)
}
/**
*
*
* @static
* @param {string|array} types - An event type, an array of event types or event types seperated by a space sign. The following
* events are possible:
* pan, panstart, panmove, panend, pancancel, panleft, panright, panup, pandown
* pinch, pinchstart, pinchmove, pinchend, pinchcancel, pinchin, pinchout
* press, pressup
* rotate, rotatestart, rotatemove, rotateend, rotatecancel
* swipe, swipeleft, swiperight, swipeup, swipedown
* tap
* @param {HTMLElement|HTMLElement[]} elements - An HTML element or an array of HTML elements.
* @param {function} [cb] - The callback. A function which is executed after the event occurs. Receives the event object as the
* first paramter
* @param {object} [opts] - An options object. See the hammer documentation for more details.
*/
static on(types, elements, cb, opts = {}) {
2019-07-18 12:26:39 +02:00
opts = Object.assign({}, {}, opts)
2019-03-21 09:57:27 +01:00
if (typeof Hammer === 'undefined') {
console.error('Hammer.js not found!')
return this
}
2019-08-13 12:46:24 +02:00
if (typeof Hammer.__hammers === 'undefined') {
Hammer.__hammers = new Map()
}
2019-03-21 09:57:27 +01:00
// convert to array
types = Array.isArray(types) ? types : types.split(/\s/)
2019-07-30 16:56:29 +02:00
if (elements instanceof NodeList || elements instanceof HTMLCollection) {
2019-03-21 09:57:27 +01:00
elements = Array.from(elements)
}
elements = Array.isArray(elements) ? elements : [elements]
for (let i = 0; i < types.length; i++) {
const type = types[i].toLowerCase()
// list of hammer events
2019-07-30 16:56:29 +02:00
const useHammer = /^(tap|doubletap|press|pan|swipe|pinch|rotate).*$/.test(type)
2019-03-21 09:57:27 +01:00
// if it is a hammer event
if (useHammer) {
for (let j = 0; j < elements.length; j++) {
// if(elements[j].tagName == "svg") return false;
let hammer = new Hammer(elements[j], opts)
if (window.propagating !== 'undefined') {
hammer = propagating(hammer)
}
// recognizers
if (type.startsWith('pan')) {
2019-07-30 16:56:29 +02:00
hammer.get('pan').set(Object.assign({ direction: Hammer.DIRECTION_ALL }, opts))
2019-03-21 09:57:27 +01:00
} else if (type.startsWith('pinch')) {
2019-07-30 16:56:29 +02:00
hammer.get('pinch').set(Object.assign({ enable: true }, opts))
2019-03-21 09:57:27 +01:00
} else if (type.startsWith('press')) {
hammer.get('press').set(opts)
} else if (type.startsWith('rotate')) {
2019-07-30 16:56:29 +02:00
hammer.get('rotate').set(Object.assign({ enable: true }, opts))
2019-03-21 09:57:27 +01:00
} else if (type.startsWith('swipe')) {
2019-07-30 16:56:29 +02:00
hammer.get('swipe').set(Object.assign({ direction: Hammer.DIRECTION_ALL }, opts))
2019-03-21 09:57:27 +01:00
} else if (type.startsWith('tap')) {
hammer.get('tap').set(opts)
}
hammer.on(type, event => {
cb(event)
})
2019-08-13 12:46:24 +02:00
if (Hammer.__hammers.has(elements[j])) {
const elementHammers = Hammer.__hammers.get(elements[j])
elementHammers.push(hammer)
Hammer.__hammers.set(elements[j], elementHammers)
} else {
Hammer.__hammers.set(elements[j], [hammer])
}
2019-03-21 09:57:27 +01:00
}
} else {
for (let j = 0; j < elements.length; j++) {
Hammer.on(elements[j], type, event => {
cb(event)
})
}
}
}
2019-08-13 12:46:24 +02:00
}
2019-03-21 09:57:27 +01:00
2019-08-13 12:46:24 +02:00
/**
*
*
* @static
* @param {HTMLElement|HTMLElement[]} elements - An HTML element or an array of HTML elements.
*/
static off(elements) {
if (typeof Hammer === 'undefined') {
console.error('Hammer.js not found!')
return this
}
// convert to array
if (elements instanceof NodeList || elements instanceof HTMLCollection) {
elements = Array.from(elements)
}
elements = Array.isArray(elements) ? elements : [elements]
for (let i = 0; i < elements.length; i++) {
const element = elements[i]
if (Hammer.__hammers.has(element)) {
const elementHammers = Hammer.__hammers.get(element)
elementHammers.forEach(it => it.destroy())
Hammer.__hammers.delete(element)
}
}
2019-03-21 09:57:27 +01:00
}
}
window.InteractionMapper = InteractionMapper