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

/**
* 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();
}
}
});