You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
2 years ago
|
/**
|
||
|
* This class manages the playback of an array of "event descriptors". For details on the
|
||
|
* contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the
|
||
|
* {@link Ext.ux.event.Recorder} class are designed to serve as input for this class.
|
||
|
*
|
||
|
* The simplest use of this class is to instantiate it with an {@link #eventQueue} and call
|
||
|
* {@link #method-start}. Like so:
|
||
|
*
|
||
|
* var player = Ext.create('Ext.ux.event.Player', {
|
||
|
* eventQueue: [ ... ],
|
||
|
* speed: 2, // play at 2x speed
|
||
|
* listeners: {
|
||
|
* stop: function () {
|
||
|
* player = null; // all done
|
||
|
* }
|
||
|
* }
|
||
|
* });
|
||
|
*
|
||
|
* player.start();
|
||
|
*
|
||
|
* A more complex use would be to incorporate keyframe generation after playing certain
|
||
|
* events.
|
||
|
*
|
||
|
* var player = Ext.create('Ext.ux.event.Player', {
|
||
|
* eventQueue: [ ... ],
|
||
|
* keyFrameEvents: {
|
||
|
* click: true
|
||
|
* },
|
||
|
* listeners: {
|
||
|
* stop: function () {
|
||
|
* // play has completed... probably time for another keyframe...
|
||
|
* player = null;
|
||
|
* },
|
||
|
* keyframe: onKeyFrame
|
||
|
* }
|
||
|
* });
|
||
|
*
|
||
|
* player.start();
|
||
|
*
|
||
|
* If a keyframe can be handled immediately (synchronously), the listener would be:
|
||
|
*
|
||
|
* function onKeyFrame () {
|
||
|
* handleKeyFrame();
|
||
|
* }
|
||
|
*
|
||
|
* If the keyframe event is always handled asynchronously, then the event listener is only
|
||
|
* a bit more:
|
||
|
*
|
||
|
* function onKeyFrame (p, eventDescriptor) {
|
||
|
* eventDescriptor.defer(); // pause event playback...
|
||
|
*
|
||
|
* handleKeyFrame(function () {
|
||
|
* eventDescriptor.finish(); // ...resume event playback
|
||
|
* });
|
||
|
* }
|
||
|
*
|
||
|
* Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps
|
||
|
* differently by browser), a slightly more complex listener is required.
|
||
|
*
|
||
|
* function onKeyFrame (p, eventDescriptor) {
|
||
|
* var async;
|
||
|
*
|
||
|
* handleKeyFrame(function () {
|
||
|
* // either this callback is being called immediately by handleKeyFrame (in
|
||
|
* // which case async is undefined) or it is being called later (in which case
|
||
|
* // async will be true).
|
||
|
*
|
||
|
* if (async) {
|
||
|
* eventDescriptor.finish();
|
||
|
* } else {
|
||
|
* async = false;
|
||
|
* }
|
||
|
* });
|
||
|
*
|
||
|
* // either the callback was called (and async is now false) or it was not
|
||
|
* // called (and async remains undefined).
|
||
|
*
|
||
|
* if (async !== false) {
|
||
|
* eventDescriptor.defer();
|
||
|
* async = true; // let the callback know that we have gone async
|
||
|
* }
|
||
|
* }
|
||
|
*/
|
||
|
Ext.define('Ext.ux.event.Player', {
|
||
|
extend: 'Ext.ux.event.Driver',
|
||
|
|
||
|
/**
|
||
|
* @cfg {Array} eventQueue The event queue to playback. This must be provided before
|
||
|
* the {@link #method-start} method is called.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @cfg {Object} keyFrameEvents An object that describes the events that should generate
|
||
|
* keyframe events. For example, `{ click: true }` would generate keyframe events after
|
||
|
* each `click` event.
|
||
|
*/
|
||
|
keyFrameEvents: {
|
||
|
click: true
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false
|
||
|
* to ignore animations. Default is true.
|
||
|
*/
|
||
|
pauseForAnimations: true,
|
||
|
|
||
|
/**
|
||
|
* @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the
|
||
|
* recorded speed). A value of 2 would playback at 2x speed.
|
||
|
*/
|
||
|
speed: 1.0,
|
||
|
|
||
|
stallTime: 0,
|
||
|
|
||
|
tagPathRegEx: /(\w+)(?:\[(\d+)\])?/,
|
||
|
|
||
|
constructor: function (config) {
|
||
|
var me = this;
|
||
|
|
||
|
me.callParent(arguments);
|
||
|
|
||
|
me.addEvents(
|
||
|
/**
|
||
|
* @event beforeplay
|
||
|
* Fires before an event is played.
|
||
|
* @param {Ext.ux.event.Player} this
|
||
|
* @param {Object} eventDescriptor The event descriptor about to be played.
|
||
|
*/
|
||
|
'beforeplay',
|
||
|
|
||
|
/**
|
||
|
* @event keyframe
|
||
|
* Fires when this player reaches a keyframe. Typically, this is after events
|
||
|
* like `click` are injected and any resulting animations have been completed.
|
||
|
* @param {Ext.ux.event.Player} this
|
||
|
* @param {Object} eventDescriptor The keyframe event descriptor.
|
||
|
*/
|
||
|
'keyframe'
|
||
|
);
|
||
|
|
||
|
me.eventObject = new Ext.EventObjectImpl();
|
||
|
|
||
|
me.timerFn = function () {
|
||
|
me.onTick();
|
||
|
};
|
||
|
me.attachTo = me.attachTo || window;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns the element given is XPath-like description.
|
||
|
* @param {String} xpath The XPath-like description of the element.
|
||
|
* @return {HTMLElement}
|
||
|
*/
|
||
|
getElementFromXPath: function (xpath) {
|
||
|
var me = this,
|
||
|
parts = xpath.split('/'),
|
||
|
regex = me.tagPathRegEx,
|
||
|
i, n, m, count, tag, child,
|
||
|
el = me.attachTo.document;
|
||
|
|
||
|
el = (parts[0] == '~') ? el.body
|
||
|
: el.getElementById(parts[0].substring(1)); // remove '#'
|
||
|
|
||
|
for (i = 1, n = parts.length; el && i < n; ++i) {
|
||
|
m = regex.exec(parts[i]);
|
||
|
count = m[2] ? parseInt(m[2], 10) : 1;
|
||
|
tag = m[1].toUpperCase();
|
||
|
|
||
|
for (child = el.firstChild; child; child = child.nextSibling) {
|
||
|
if (child.tagName == tag) {
|
||
|
if (count == 1) {
|
||
|
break;
|
||
|
}
|
||
|
--count;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
el = child;
|
||
|
}
|
||
|
|
||
|
return el;
|
||
|
},
|
||
|
|
||
|
getTimeIndex: function () {
|
||
|
var t = this.getTimestamp() - this.stallTime;
|
||
|
return t * this.speed;
|
||
|
},
|
||
|
|
||
|
makeToken: function (eventDescriptor, signal) {
|
||
|
var me = this,
|
||
|
t0;
|
||
|
|
||
|
eventDescriptor[signal] = true;
|
||
|
|
||
|
eventDescriptor.defer = function () {
|
||
|
eventDescriptor[signal] = false;
|
||
|
t0 = me.getTime();
|
||
|
};
|
||
|
|
||
|
eventDescriptor.finish = function () {
|
||
|
eventDescriptor[signal] = true;
|
||
|
me.stallTime += me.getTime() - t0;
|
||
|
|
||
|
me.schedule();
|
||
|
};
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This method is called after an event has been played to prepare for the next event.
|
||
|
* @param {Object} eventDescriptor The descriptor of the event just played.
|
||
|
*/
|
||
|
nextEvent: function (eventDescriptor) {
|
||
|
var me = this,
|
||
|
index = ++me.queueIndex;
|
||
|
|
||
|
// keyframe events are inserted after a keyFrameEvent is played.
|
||
|
if (me.keyFrameEvents[eventDescriptor.type]) {
|
||
|
Ext.Array.insert(me.eventQueue, index, [
|
||
|
{ keyframe: true, ts: eventDescriptor.ts }
|
||
|
]);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This method returns the event descriptor at the front of the queue. This does not
|
||
|
* dequeue the event. Repeated calls return the same object (until {@link #nextEvent}
|
||
|
* is called).
|
||
|
*/
|
||
|
peekEvent: function () {
|
||
|
var me = this,
|
||
|
queue = me.eventQueue,
|
||
|
index = me.queueIndex,
|
||
|
eventDescriptor = queue[index],
|
||
|
type = eventDescriptor && eventDescriptor.type,
|
||
|
tmp;
|
||
|
|
||
|
if (type == 'mduclick') {
|
||
|
tmp = [
|
||
|
Ext.applyIf({ type: 'mousedown' }, eventDescriptor),
|
||
|
Ext.applyIf({ type: 'mouseup' }, eventDescriptor),
|
||
|
Ext.applyIf({ type: 'click' }, eventDescriptor)
|
||
|
];
|
||
|
|
||
|
me.replaceEvent(index, tmp);
|
||
|
}
|
||
|
|
||
|
return queue[index] || null;
|
||
|
},
|
||
|
|
||
|
replaceEvent: function (index, events) {
|
||
|
for (var t, i = 0, n = events.length; i < n; ++i) {
|
||
|
if (i) {
|
||
|
t = events[i-1];
|
||
|
delete t.afterplay;
|
||
|
delete t.screenshot;
|
||
|
|
||
|
delete events[i].beforeplay;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Ext.Array.replace(this.eventQueue, index, 1, events);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This method dequeues and injects events until it has arrived at the time index. If
|
||
|
* no events are ready (based on the time index), this method does nothing.
|
||
|
* @return {Boolean} True if there is more to do; false if not (at least for now).
|
||
|
*/
|
||
|
processEvents: function () {
|
||
|
var me = this,
|
||
|
animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items,
|
||
|
eventDescriptor;
|
||
|
|
||
|
while ((eventDescriptor = me.peekEvent()) !== null) {
|
||
|
if (animations && animations.getCount()) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (eventDescriptor.keyframe) {
|
||
|
if (!me.processKeyFrame(eventDescriptor)) {
|
||
|
return false;
|
||
|
}
|
||
|
me.nextEvent(eventDescriptor);
|
||
|
} else if (eventDescriptor.ts <= me.getTimeIndex() &&
|
||
|
me.fireEvent('beforeplay', me, eventDescriptor) !== false &&
|
||
|
me.playEvent(eventDescriptor)) {
|
||
|
if(window.__x && eventDescriptor.screenshot) {
|
||
|
__x.poll.sendSyncRequest({cmd: 'screenshot'});
|
||
|
}
|
||
|
me.nextEvent(eventDescriptor);
|
||
|
} else {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
me.stop();
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This method is called when a keyframe is reached. This will fire the keyframe event.
|
||
|
* If the keyframe has been handled, true is returned. Otherwise, false is returned.
|
||
|
* @param {Object} The event descriptor of the keyframe.
|
||
|
* @return {Boolean} True if the keyframe was handled, false if not.
|
||
|
*/
|
||
|
processKeyFrame: function (eventDescriptor) {
|
||
|
var me = this;
|
||
|
|
||
|
// only fire keyframe event (and setup the eventDescriptor) once...
|
||
|
if (!eventDescriptor.defer) {
|
||
|
me.makeToken(eventDescriptor, 'done');
|
||
|
me.fireEvent('keyframe', me, eventDescriptor);
|
||
|
}
|
||
|
|
||
|
return eventDescriptor.done;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Called to inject the given event on the specified target.
|
||
|
* @param {HTMLElement} target The target of the event.
|
||
|
* @param {Ext.EventObject} The event to inject.
|
||
|
*/
|
||
|
injectEvent: function (target, event) {
|
||
|
event.injectEvent(target);
|
||
|
},
|
||
|
|
||
|
playEvent: function (eventDescriptor) {
|
||
|
var me = this,
|
||
|
target = me.getElementFromXPath(eventDescriptor.target),
|
||
|
event;
|
||
|
|
||
|
if (!target) {
|
||
|
// not present (yet)... wait for element present...
|
||
|
// TODO - need a timeout here
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!me.playEventHook(eventDescriptor, 'beforeplay')) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!eventDescriptor.injected) {
|
||
|
eventDescriptor.injected = true;
|
||
|
event = me.translateEvent(eventDescriptor, target);
|
||
|
me.injectEvent(target, event);
|
||
|
}
|
||
|
|
||
|
return me.playEventHook(eventDescriptor, 'afterplay');
|
||
|
},
|
||
|
|
||
|
playEventHook: function (eventDescriptor, hookName) {
|
||
|
var me = this,
|
||
|
doneName = hookName + '.done',
|
||
|
firedName = hookName + '.fired',
|
||
|
hook = eventDescriptor[hookName];
|
||
|
|
||
|
if (hook && !eventDescriptor[doneName]) {
|
||
|
if (!eventDescriptor[firedName]) {
|
||
|
eventDescriptor[firedName] = true;
|
||
|
me.makeToken(eventDescriptor, doneName);
|
||
|
|
||
|
me.eventScope[hook](eventDescriptor);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
schedule: function () {
|
||
|
var me = this;
|
||
|
if (!me.timer) {
|
||
|
me.timer = setTimeout(me.timerFn, 10);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
translateEvent: function (eventDescriptor, target) {
|
||
|
var me = this,
|
||
|
event = me.eventObject,
|
||
|
modKeys = eventDescriptor.modKeys || '',
|
||
|
xy;
|
||
|
|
||
|
if ('x' in eventDescriptor) {
|
||
|
event.xy = xy = Ext.fly(target).getXY();
|
||
|
xy[0] += eventDescriptor.x;
|
||
|
xy[1] += eventDescriptor.y;
|
||
|
}
|
||
|
|
||
|
if ('wheel' in eventDescriptor) {
|
||
|
// see getWheelDelta
|
||
|
}
|
||
|
|
||
|
event.type = eventDescriptor.type;
|
||
|
event.button = eventDescriptor.button;
|
||
|
event.altKey = modKeys.indexOf('A') > 0;
|
||
|
event.ctrlKey = modKeys.indexOf('C') > 0;
|
||
|
event.metaKey = modKeys.indexOf('M') > 0;
|
||
|
event.shiftKey = modKeys.indexOf('S') > 0;
|
||
|
|
||
|
return event;
|
||
|
},
|
||
|
|
||
|
//---------------------------------
|
||
|
// Driver overrides
|
||
|
|
||
|
onStart: function () {
|
||
|
var me = this;
|
||
|
|
||
|
me.queueIndex = 0;
|
||
|
me.schedule();
|
||
|
},
|
||
|
|
||
|
onStop: function () {
|
||
|
var me = this;
|
||
|
|
||
|
if (me.timer) {
|
||
|
clearTimeout(me.timer);
|
||
|
me.timer = null;
|
||
|
}
|
||
|
if (window.__x) {
|
||
|
__x.poll.sendSyncRequest({cmd: 'finish'});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
//---------------------------------
|
||
|
|
||
|
onTick: function () {
|
||
|
var me = this;
|
||
|
|
||
|
me.timer = null;
|
||
|
|
||
|
if (me.processEvents()) {
|
||
|
me.schedule();
|
||
|
}
|
||
|
}
|
||
|
});
|