318 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			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()
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |