iwmlib/lib/events.js

318 lines
10 KiB
JavaScript
Executable File

export default class Events {
static stop(event) {
event.preventDefault()
// I removed this, as it destroys all the Hammer.js events.
// And also the click event.
// It seems to have no (apparent) negative impact. -SO
// event.stopPropagation()
}
static extractPoint(event) {
switch (event.constructor.name) {
case 'TouchEvent':
for (let i = 0; i < event.targetTouches.length; i++) {
let t = event.targetTouches[i]
return { x: t.clientX, y: t.clientY }
}
break
default:
return { x: event.clientX, y: event.clientY }
}
}
static isCaptured(event) {
if (event.__capturedBy) return true
return false
}
static capturedBy(event, obj) {
event.__capturedBy = obj
}
static isPointerDown(event) {
// According to
// https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
// pointer events use the buttons feature to represent pressed buttons
return event.buttons
}
static isMouseDown(event) {
// Attempts to clone the which attribute of events failed in WebKit. May
// be this is a bug or a security feature. Workaround: we introduce
// a mouseDownSubstitute attribute that can be assigned to cloned
// events after instantiation.
if (Reflect.has(event, 'mouseDownSubstitute')) return event.mouseDownSubstitute
return event.buttons || event.which
}
static isSimulatedEvent(event) {
return Reflect.has(event, 'mouseDownSubstitute')
}
static isMouseRightClick(event) {
return event.buttons || event.which
}
static extractTouches(targets) {
let touches = []
for (let i = 0; i < targets.length; i++) {
let t = targets[i]
touches.push({
targetSelector: this.selector(t.target),
identifier: t.identifier,
screenX: t.screenX,
screenY: t.screenY,
clientX: t.clientX,
clientY: t.clientY,
pageX: t.pageX,
pageY: t.pageY
})
}
return touches
}
static createTouchList(targets) {
let touches = []
for (let i = 0; i < targets.length; i++) {
let t = targets[i]
let touchTarget = document.elementFromPoint(t.pageX, t.pageY)
let touch = new Touch(undefined, touchTarget, t.identifier, t.pageX, t.pageY, t.screenX, t.screenY)
touches.push(touch)
}
return new TouchList(...touches)
}
static extractEvent(timestamp, event) {
let targetSelector = this.selector(event.target)
let infos = {
type: event.type,
time: timestamp,
constructor: event.constructor,
data: {
targetSelector: targetSelector,
view: event.view,
mouseDownSubstitute: event.buttons || event.which, // which cannot be cloned directly
bubbles: event.bubbles,
cancelable: event.cancelable,
screenX: event.screenX,
screenY: event.screenY,
clientX: event.clientX,
clientY: event.clientY,
layerX: event.layerX,
layerY: event.layerY,
pageX: event.pageX,
pageY: event.pageY,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
}
}
if (event.type.startsWith('touch')) {
// On Safari-WebKit the TouchEvent has layerX, layerY coordinates
let data = infos.data
data.targetTouches = this.extractTouches(event.targetTouches)
data.changedTouches = this.extractTouches(event.changedTouches)
data.touches = this.extractTouches(event.touches)
}
if (event.type.startsWith('pointer')) {
let data = infos.data
data.pointerId = event.pointerId
data.pointerType = event.pointerType
}
if (Events.debug) {
Events.extracted.push(this.toLine(event))
}
return infos
}
static cloneEvent(type, constructor, data) {
if (type.startsWith('touch')) {
// We need to find target from layerX, layerY
//var target = document.querySelector(data.targetSelector)
// elementFromPoint(data.layerX, data.layerY)
//data.target = target
data.targetTouches = this.createTouchList(data.targetTouches)
data.changedTouches = this.createTouchList(data.changedTouches)
data.touches = this.createTouchList(data.touches)
}
// We need to find target from pageX, pageY which are only
// available after construction. They seem to getter items.
let clone = Reflect.construct(constructor, [type, data])
clone.mouseDownSubstitute = data.mouseDownSubstitute
return clone
}
static simulateEvent(type, constructor, data) {
data.target = document.querySelector(data.targetSelector)
let clone = this.cloneEvent(type, constructor, data)
if (data.target != null) {
data.target.dispatchEvent(clone)
}
if (Events.debug) {
Events.simulated.push(this.toLine(clone))
}
}
static toLine(event) {
return `${event.type} #${event.target.id} ${event.clientX} ${event.clientY}`
let result = event.type
let selector = this.selector(event.target)
result += ' selector: ' + selector
if (event.target != document.querySelector(selector)) console.log('Cannot resolve', selector)
let keys = ['layerX', 'layerY', 'pageX', 'pageY', 'clientX', 'clientY']
for (let key of keys) {
try {
result += ' ' + key + ':' + event[key]
} catch (e) {
console.log('Invalid key: ' + key)
}
}
return result
}
static compareExtractedWithSimulated() {
var diffs = 0
if (this.extracted.length != this.simulated.length) {
alert(
'Unequal length of extracted [' +
this.extracted.length +
'] and simulated events [' +
this.simulated.length +
'].'
)
diffs += 1
} else {
for (let i = 0; i < this.extracted.length; i++) {
var extracted = this.extracted[i]
var simulated = this.simulated[i]
if (extracted != simulated) {
console.log('Events differ:' + extracted + '|' + simulated)
diffs += 1
}
}
}
}
static selector(context) {
return OptimalSelect.select(context)
}
static reset() {
this.extracted = []
this.simulated = []
}
static resetSimulated() {
this.simulated = []
}
static showExtractedEvents(event) {
if (!event.shiftKey) {
return
}
if (this.popup == null) {
let element = document.createElement('div')
Elements.setStyle(element, {
position: 'absolute',
width: '480px',
height: '640px',
overflow: 'auto',
backgroundColor: 'lightgray'
})
document.body.appendChild(element)
this.popup = element
}
this.popup.innerHTML = ''
for (let line of this.extracted) {
let div = document.createElement('div')
div.innerHTML = line
this.popup.appendChild(div)
}
let div = document.createElement('div')
div.innerHTML = '------------ Simulated -----------'
this.popup.appendChild(div)
for (let line of this.simulated) {
let div = document.createElement('div')
div.innerHTML = line
this.popup.appendChild(div)
}
Elements.setStyle(this.popup, {
left: event.clientX + 'px',
top: event.clientY + 'px'
})
}
}
Events.popup = null
Events.debug = true
Events.extracted = []
Events.simulated = []
Events.simulationRunning = false
export class EventRecorder {
constructor() {
this.recording = []
this.recorded = []
this.step = 0
}
record(event) {
let length = this.recording.length
if (length == 0) {
this.startTime = event.timeStamp
Events.reset()
} else {
let last = this.recording[length - 1]
if (event.timeStamp < last.time) {
console.log('warning: wrong temporal order')
}
}
let t = event.timeStamp - this.startTime
this.recording.push(Events.extractEvent(t, event))
}
stopRecording() {
this.recorded = this.recording
this.recording = []
console.log('Recorded ' + this.recorded.length + ' events')
}
startReplay(whileCondition = null, onComplete = null) {
this.step = 0
Events.resetSimulated()
console.log('Start replay')
Events.simulationRunning = true
this.replay(whileCondition, onComplete)
}
replay(whileCondition = null, onComplete = null) {
if (this.step < this.recorded.length) {
let { type, time, constructor, data } = this.recorded[this.step]
Events.simulateEvent(type, constructor, data)
this.step += 1
let dt = 0
if (this.step < this.recorded.length) {
var next = this.recorded[this.step]
dt = next.time - time
if (dt < 0) {
console.log('warning: wrong temporal order')
}
}
if (whileCondition == null || whileCondition()) {
let delta = Math.round(dt)
setTimeout(() => this.replay(whileCondition, onComplete), delta)
}
} else {
console.log('Played ' + this.step + ' events' + onComplete)
Events.simulationRunning = false
if (onComplete != null) {
onComplete()
}
//Events.compareExtractedWithSimulated()
}
}
}