1188 lines
37 KiB
JavaScript
Executable File
1188 lines
37 KiB
JavaScript
Executable File
/* eslint-disable no-unused-vars */
|
|
/* globals Hammer, propagating */
|
|
/*eslint no-console: ["error", { allow: ["log", "warn", "info", "error"] }]*/
|
|
|
|
import Interface from './interface.js'
|
|
import { Points, MapProxy } from './utils.js'
|
|
import Events from './events.js'
|
|
import Logging from './logging.js'
|
|
|
|
/** Interaction patterns
|
|
|
|
See interaction.html for explanation
|
|
*/
|
|
|
|
export class IInteractionTarget extends Interface {
|
|
capture(event) {
|
|
return typeof true
|
|
}
|
|
|
|
onStart(event, interaction) {}
|
|
onMove(event, interaction) {}
|
|
onEnd(event, interaction) {}
|
|
|
|
onMouseWheel(event) {}
|
|
}
|
|
|
|
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) {
|
|
this.x = x
|
|
this.y = y
|
|
this.zoom = zoom
|
|
this.rotate = rotate
|
|
this.about = about
|
|
this.number = number
|
|
this.distance = distance
|
|
}
|
|
|
|
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 = []
|
|
let cm = { x: 0, y: 0 }
|
|
let pm = { x: 0, y: 0 }
|
|
let count = 0
|
|
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
|
|
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)
|
|
|
|
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() {
|
|
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)
|
|
|
|
// Using the mean leads to jumps between time slices with 3 and 2 fingers
|
|
// We use the mean of deltas instead
|
|
let delta = Points.mean(d1, d2)
|
|
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)
|
|
return new InteractionDelta(delta.x, delta.y, zoom, alpha, cm, csize, distance2)
|
|
} else if (csize == 1 && psize == 1 && this.current.firstKey() == this.previous.firstKey()) {
|
|
// We need to ensure that the keys are the same, since single points with different keys
|
|
// can jump
|
|
let current = this.current.first()
|
|
let previous = this.previous.first()
|
|
let delta = Points.subtract(current, previous)
|
|
return new InteractionDelta(delta.x, delta.y, 1.0, 0.0, current, csize)
|
|
}
|
|
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)
|
|
} else {
|
|
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)
|
|
if (start && ended && Points.distance(ended, start) < this.tapDistance) {
|
|
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)
|
|
}
|
|
}
|
|
if (this.tapTimestamps.has(key) && performance.now() > this.tapTimestamps.get(key) + this.tapDuration) {
|
|
//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
|
|
} else {
|
|
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)
|
|
if (start && ended && Points.distance(ended, start) < this.tapDistance) {
|
|
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,
|
|
{
|
|
mouseWheelElement = null,
|
|
useCapture = true,
|
|
capturePointerEvents = true,
|
|
cancelOnWindowOut = true,
|
|
debug = false
|
|
} = {}
|
|
) {
|
|
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) {
|
|
let error = this.targetInterface.implementationError(this.target.constructor)
|
|
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')
|
|
}
|
|
}
|
|
this.onStart(e)
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
element.addEventListener(
|
|
'pointermove',
|
|
e => {
|
|
if (this.debug) console.log('pointermove', e.pointerId, e.pointerType)
|
|
|
|
if (e.pointerType == 'touch' || (e.pointerType == 'mouse' && Events.isPointerDown(e))) {
|
|
// this.capture(e) &&
|
|
if (this.debug) console.log('pointermove captured', e.pointerId)
|
|
this.onMove(e)
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
element.addEventListener(
|
|
'pointerup',
|
|
e => {
|
|
if (this.debug) console.log('pointerup', e.pointerId, e.pointerType)
|
|
this.onEnd(e)
|
|
if (this.capturePointerEvents) {
|
|
try {
|
|
element.releasePointerCapture(e.pointerId)
|
|
} catch (e) {
|
|
console.warn('Cannot release pointer')
|
|
}
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
element.addEventListener(
|
|
'pointercancel',
|
|
e => {
|
|
if (this.debug) console.log('pointercancel', e.pointerId, e.pointerType)
|
|
this.onEnd(e)
|
|
if (this.capturePointerEvents) element.releasePointerCapture(e.pointerId)
|
|
},
|
|
useCapture
|
|
)
|
|
|
|
if (!this.capturePointerEvents) {
|
|
element.addEventListener(
|
|
'pointerleave',
|
|
e => {
|
|
if (this.debug) console.log('pointerleave', e.pointerId, e.pointerType)
|
|
if (e.target == element) this.onEnd(e)
|
|
},
|
|
useCapture
|
|
)
|
|
}
|
|
|
|
if (!this.capturePointerEvents) {
|
|
element.addEventListener(
|
|
'pointerout',
|
|
e => {
|
|
if (this.debug) console.log('pointerout', e.pointerId, e.pointerType)
|
|
if (e.target == element) this.onEnd(e)
|
|
},
|
|
useCapture
|
|
)
|
|
}
|
|
|
|
if (this.cancelOnWindowOut) {
|
|
window.addEventListener(
|
|
'pointerout',
|
|
e => {
|
|
if (this.debug) console.log('pointerout', e.pointerId, e.pointerType, e.target)
|
|
if (e.target == element) {
|
|
this.onEnd(e)
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
}
|
|
} else if (window.TouchEvent) {
|
|
if (this.debug) console.log('Touch API')
|
|
element.addEventListener(
|
|
'touchstart',
|
|
e => {
|
|
if (this.debug) console.log('touchstart', this.touchPoints(e))
|
|
if (this.capture(e)) {
|
|
for (let touch of e.changedTouches) {
|
|
this.onStart(touch)
|
|
}
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
element.addEventListener(
|
|
'touchmove',
|
|
e => {
|
|
if (this.debug) console.log('touchmove', this.touchPoints(e), e)
|
|
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 => {
|
|
if (this.debug) console.log('touchcancel', e.targetTouches.length, e.changedTouches.length)
|
|
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)) {
|
|
if (this.debug) console.log('mousemove', e)
|
|
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)
|
|
console.warn("Shouldn't happen: mouseout ends interaction")
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
}
|
|
if (this.cancelOnWindowOut) {
|
|
window.addEventListener(
|
|
'mouseout',
|
|
e => {
|
|
if (e.target == element) {
|
|
this.onEnd(e)
|
|
}
|
|
},
|
|
useCapture
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
this.mouseWheelElement.addEventListener('mousewheel', this.onMouseWheel.bind(this), true)
|
|
this.mouseWheelElement.addEventListener('DOMMouseScroll', this.onMouseWheel.bind(this), true)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return this.target.capture ? this.target.capture(event) : false
|
|
}
|
|
|
|
getPosition(event) {
|
|
return { x: event.clientX, y: event.clientY }
|
|
}
|
|
|
|
extractPoint(event, touchEventKey = 'all') {
|
|
// 'targetTouches'
|
|
let result = {}
|
|
switch (event.constructor.name) {
|
|
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': {
|
|
let id = event.touchType === 'stylus' ? 'stylus' : event.identifier.toString()
|
|
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
|
|
}
|
|
return result
|
|
}
|
|
|
|
interactionStarted(event, key, point) {
|
|
// Callback: can be overwritten
|
|
}
|
|
|
|
interactionEnded(event, key, point) {
|
|
// Callback: can be overwritten
|
|
}
|
|
|
|
interactionFinished(event, key, point) {}
|
|
|
|
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) {
|
|
console.warn("new pointer in updateInteraction shouldn't happen", key)
|
|
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,
|
|
{
|
|
tapDistance = 10,
|
|
longPressTime = 500.0,
|
|
useCapture = true,
|
|
capturePointerEvents = true,
|
|
mouseWheelElement = null,
|
|
logInteractionsAbove = 12
|
|
} = {}
|
|
) {
|
|
super(element, target, {
|
|
tapDistance,
|
|
useCapture,
|
|
capturePointerEvents,
|
|
longPressTime,
|
|
mouseWheelElement
|
|
})
|
|
this.logInteractionsAbove = logInteractionsAbove
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
let size = this.interaction.current.size
|
|
let limit = this.logInteractionsAbove
|
|
if (size > limit) {
|
|
Logging.log(`Number of interactions ${size} exceeds ${limit}`)
|
|
}
|
|
}
|
|
|
|
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)
|
|
let mapped = this.interaction.mapInteraction(extracted, ['ended'], this.mapPositionToPoint.bind(this))
|
|
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 = {}) {
|
|
opts = Object.assign({}, {}, opts)
|
|
|
|
if (typeof Hammer === 'undefined') {
|
|
console.error('Hammer.js not found!')
|
|
return this
|
|
}
|
|
|
|
if (typeof Hammer.__hammers === 'undefined') {
|
|
Hammer.__hammers = new Map()
|
|
}
|
|
|
|
// convert to array
|
|
types = Array.isArray(types) ? types : types.split(/\s/)
|
|
if (elements instanceof NodeList || elements instanceof HTMLCollection) {
|
|
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
|
|
const useHammer = /^(tap|doubletap|press|pan|swipe|pinch|rotate).*$/.test(type)
|
|
|
|
// 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')) {
|
|
hammer.get('pan').set(Object.assign({ direction: Hammer.DIRECTION_ALL }, opts))
|
|
} else if (type.startsWith('pinch')) {
|
|
hammer.get('pinch').set(Object.assign({ enable: true }, opts))
|
|
} else if (type.startsWith('press')) {
|
|
hammer.get('press').set(opts)
|
|
} else if (type.startsWith('rotate')) {
|
|
hammer.get('rotate').set(Object.assign({ enable: true }, opts))
|
|
} else if (type.startsWith('swipe')) {
|
|
hammer.get('swipe').set(Object.assign({ direction: Hammer.DIRECTION_ALL }, opts))
|
|
} else if (type.startsWith('tap')) {
|
|
hammer.get('tap').set(opts)
|
|
}
|
|
|
|
hammer.on(type, event => {
|
|
cb(event)
|
|
})
|
|
|
|
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])
|
|
}
|
|
}
|
|
} else {
|
|
for (let j = 0; j < elements.length; j++) {
|
|
Hammer.on(elements[j], type, event => {
|
|
cb(event)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
window.InteractionMapper = InteractionMapper
|