/** * @class ST.playable.Playable * This class is instantiated for each event record passed to the `{@link ST#play}` * method. The items in the array passed to `{@link ST#play}` are passed to the * `constructor` as the config object. */ ST.playable = ST.playable || {}; // setup ST.playable namespace. ST.playable.Playable = ST.define({ isPlayable: true, statics: { /** * From an playable config object (event) determine if the Playable * type has an fn to execute when played. Allows the Player to decide * whether to playFn() or inject() without instantiating the event. * @param event */ hasFn: function (event) { if (typeof event.hasFn !== 'undefined') { return event.hasFn; } if (typeof event.fn !== 'undefined') { return event.fn; } var playableClsName, playableCls, has; try { playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type); playableCls = ST.clsFromString(playableClsName); has = playableCls && playableCls.prototype && playableCls.prototype.fn; } catch (e) { // this happens either because: // a) the event type is a directly injected type like pointerdown, mousemove, etc // b) a real error where the type is meant to be a Playable class but the definition is missing } event.hasFn = !!has; return has; }, hasLocatorFn: function (event) { var playableClsName, playableCls, has; try { playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type); playableCls = ST.clsFromString(playableClsName); has = playableCls.prototype.locatorFn; } catch (e) { has = false; } return has; }, isRemoteable: function (event) { if (typeof event.remoteable !== 'undefined') { return event.remoteable; } var playableClsName, playableCls, remoteable; try { playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type); playableCls = ST.clsFromString(playableClsName); remoteable = playableCls.prototype.remoteable; if (typeof remoteable === 'undefined') { remoteable = true; } } catch (e) { remoteable = true; // default is true } event.remoteable = remoteable; // cache the value for fun? return remoteable; } }, /** * Constructor for an instance. * @param {Function/Number/Object} config A function is used to set the `fn` config * while a number sets the `delay`. Otherwise, the properties of the `config` object * are copied onto this instance. */ constructor: function (config) { var me = this, t = typeof config; if (t === 'function') { me.fn = config; me.delay = 0; } else if (t === 'number') { me.delay = config; } else { ST.apply(me, config); if (me.delay === undefined && typeof me.fn === 'function') { me.delay = 0; } } }, inject: function (done) { return this.context.inject(this,done); }, callFn: function (done) { return this.context.callFn(this,done); }, /** * For injectable events this property holds the event type. For example, "mousedown". * This should not be specified for non-injectable events. * @cfg {String} type */ /** * @cfg {String/Function} target * A function that returns the target DOM node or {@link ST.Locator locator string}. */ /** * The located element for the `target` of this event. * @property {ST.Element} targetEl * @readonly * @protected */ /** * The browser Context in which to play this Playable. For example either Local, which * plays events directly into the current window/document or Webdriver, which uses * webdriverio calls to play events remotely via the webdriverio API. * @property {ST.context.Context} context * {@link #context} */ /** * @cfg {String} relatedTarget * A function that returns the relatedTarget DOM node or * {@link ST.Locator locator string}. */ /** * The located element for the `relatedTarget` of this event. * @property {ST.Element} relatedEl * @readonly * @protected */ /** * The number of milliseconds of delay to inject after playing the previous event * before playing this event. * @cfg {Number} delay */ /** * The function to call when playing this event. If this config is set, the `type` * property is ignored and nothing is injected. * * If this function returns a `Promise` that promise is resolved before the next * event is played. Otherwise, this function should complete before returning. If * this is not desired, the function must declare a single argument (typically named * "done" and call this function when processing is finished). * * [{ * fn: function () { * // either finish up now or return a Promise * } * }, { * fn: function (done) { * somethingAsync(function () { * // do stuff * * done(); // mark this event as complete * }); * } * }] * * @cfg {Function} fn */ /** * @cfg {Number} x */ /** * @cfg {Number} y */ /** * @cfg {Number} button */ /** * @cfg {Boolean} [animation=true] * Determines if animations must complete before this event is ready to be played. * Specify `null` to disable animation checks. */ /** * @cfg {Boolean} [visible=true] * Determines if the `target` and `relatedTarget` must be visible before this event is * ready to be played. Specify `false` to wait for elements to be non-visible. Specify * `null` to disable visibility checks. */ /** * @cfg {Boolean} [available=true] * Determines if the `target` and `relatedTarget` must be present in the dom (descendants * of document.body) before this event is ready to be played. Specify `false` to wait * for elements to be non-available. Specify `null` to disable availability checks. */ /** * @cfg {Function} ready * An optional function that returns true when this event can be played. This config * will replace the `ready` method. */ /** * @cfg {Number} timeout * The maximum time (in milliseconds) to wait for this event to be `ready`. If this * time is exceeded, playback will be aborted. */ /** * @property {"init"/"queued"/"pending"/"playing"/"done"} state * @private */ state: 'init', /** * Returns true when this event is ready to be played. This method checks for the * existence and visibility (based on the `visible` config) of the `target` and * `relatedTarget` elements. In addition, this method also waits for animations to * finish (based on the `animation` config). * @return {Boolean} */ ready: function () { var ret = this.animationsDone() && this.targetReady() && this.targetReady(true); return ret; }, /** * This function is needed since a Playable/Event could have a custom ready function. */ isReady: function () { return this.context.ready(this); }, /** * Returns `true` when there are no animations in progress. This method respects the * `animation` config to disable this check. * @return {Boolean} */ animationsDone: function () { var me = this, ext = window.Ext, anim = me.animation, fx, mgr; if (me.animation) { anim = (mgr = (fx = ext && ext.fx) && fx.Manager) && mgr.items; if (anim && anim.getCount()) { // TODO: sencha touch / modern toolkit flavor return me.setWaiting('animations', 'complete'); } } return me.setWaiting(false); }, /** * Returns `true` when the specified target is ready. The `ST.Element` instance is * cached on this object based on the specified `name` (e.g., "targetEl"). This method * respects the `visible` config as part of this check. * * @param {Boolean/String} [name="target"] The name of the target property. This is * the name of the property that holds the {@link ST.Locator locator string}. If * `true` or `false` are specified, these indicate the `relatedTarget` or `target`, * respectively. * @return {Boolean} */ targetReady: function (name) { name = (name === true) ? 'relatedTarget' : (name || 'target'); var me = this, elName = me._elNames, target = me[name], visibility = me.visible, availability = me.available, root = me.root || null, direction = me.direction, absent = availability === false, dom, el; ST.logger.debug('Playable.targetReady(), name='+name+', type='+me.type+', target='+target); if (target) { elName = elName[name] || (elName[name] = name + 'El'); if (!(el = me[elName])) { // new case where target function must return the locator... if (typeof target === 'function') { var t = me.target(); if (t && t.isPlayable) { target = t; } } if (target.isPlayable) { // When a sequence of events targets the same thing, we can queue it // like so: // // ST.play([ // { target: '@foo', ... }, // { target: -1, ... }, // { target: -2, ... } // ]); // // Which gets enqueued like this: // // Q[0] = new ST.playable.Playable({ target: '@foo', ... }); // Q[1] = new ST.playable.Playable({ target: Q[0], ... }); // Q[2] = new ST.playable.Playable({ target: Q[0], ... }); // me[elName] = el = target[elName]; } } ST.logger.debug('Playable.targetReady(), type='+me.type+', el='+el); if (el) { if (absent) { if (el.isDetached()) { return me.setWaiting(name, 'absent'); } return me.setWaiting(false); } if (typeof target === 'string') { target.$scope = me; // attach the Playable as the scope for any target functions. dom = ST.find(target, false, root, direction); ST.logger.debug('Playable.targetReady(), el, target is string, dom=',dom); if (dom && el.dom !== dom) { // We store the wrapped el as soon as we find it, but if we are // waiting for visibility it may be that the locator will not // match the same DOM node from the previous tick. Instead of // creating a new ST.Element we simply reset the dom property. // Because we have a target string (vs a target playable), the // ST.Element we stored is our own. el.dom = dom; } } } else { if (target.isPlayable) { el = target[elName]; dom = el && el.dom; } else { if (typeof target === 'function') { // if this is a function, poke on the correct scope target.$scope = me; } dom = ST.find(target, false, root, direction); } ST.logger.debug('Playable.targetReady(), no el, not playable, dom=',dom); if (dom) { me[elName] = el = new ST.Element(dom); if (absent) { return me.setWaiting(name, 'absent'); } } else if (absent) { return me.setWaiting(false); } else if (availability !== null) { // availability is seldom ever true... it is false if we want to // wait for the DOM node to be unavailable (which was handled above) // so that leaves availability === null which indicates we should // not wait. return me.setWaiting(name, 'available'); } } if (el && visibility !== null) { if (visibility === false) { if (el.isVisible()) { return me.setWaiting(name, 'not visible'); } } else if (!el.isVisible()) { return me.setWaiting(name, 'visible'); } } } ST.logger.debug('Playable.targetReady(), SET WAITING FALSE, name='+name+', type='+me.type+', target='+target+', el='+el); return me.setWaiting(false); }, /** * This string contains the name of the item preventing readiness of this event. For * example, "target" or "relatedTarget". This is used to formulate an appropriate * error message should the `timeout` be exceeded. See `setWaiting` for setting * this value. * @property {String} waitingFor * @readonly * @protected */ /** * This string describes the aspect of the item preventing readiness of this event. * For example, "available" or "visible". This is used to formulate an appropriate * error message should the `timeout` be exceeded. See `setWaiting` for setting * this value. * @property {String} waitingState * @readonly * @protected */ /** * Updates the `waitingFor` and `waitingState` properties given their provided values * and returns `true` if this call clears the `waitingFor` property. * * This method is not normally called by user code but should be called if a custom * `ready` method is provided to ensure timeouts have helpful state information. * * @param {Boolean/String} waitingFor The {@link #waitingFor} value or `false` to clear * the waiting state. * @param {String} waitingState The {@link #waitingState} value. * @return {Boolean} `true` if `waitingFor` is `false` and `false` otherwise. * @protected */ setWaiting: function (waitingFor, waitingState) { if (waitingFor === false) { waitingFor = waitingState = null; } this.waitingFor = waitingFor; this.waitingState = waitingState; return !waitingFor; }, /** * The timestamp recorded when the event is first checked for readiness and found * not `ready`. * @property {Number} waitStartTime * @private * @readonly */ waitStartTime: 0, _elNames: { relatedTarget: 'relatedEl' }, getDom: function () { var el = this.getElement(); return el && el.dom; }, getElement: function () { return this.future && this.future.el; }, getComponent: function () { return this.future && this.future.cmp; }, getInstanceData: function (key) { var data = this.instanceData; if (data && key) { return data[key]; } return data; }, getFutureData: function (key) { var data = this.future.data; if (data && key) { data = data[key]; } return data; }, setFutureData: function (config) { var data = this.future.data; ST.apply(data,config); }, initInstanceData: function () { this.instanceData = this.instanceData || {}; }, initFutureData: function () { this.future.data = this.future.data || {}; }, updateEl: function (dom) { var me = this, el = me.future.el; if (!dom) { me.future.el = null; } else if (!el || el.dom !== dom) { me.future.el = ST.get(dom); } me.future.locator.targetEl = me.future.el; if (me.future._attach) { me.future._attach(); } }}); ST.playable.define = function (playableName, body) { if (!body.extend) { body.extend = ST.playable.Playable; } var cls = ST.define(body), parts = playableName.split('.'), classScope = ST.playable, name; while (parts.length > 1) { name = parts.shift(); if (!classScope[name]) { classScope[name] = {}; } } name = parts[0]; return classScope[ST.capitalize(name)] = cls;};