]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * @extends Ext.ux.event.Driver\r | |
3 | * This class manages the playback of an array of "event descriptors". For details on the\r | |
4 | * contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the\r | |
5 | * {@link Ext.ux.event.Recorder} class are designed to serve as input for this class.\r | |
6 | * \r | |
7 | * The simplest use of this class is to instantiate it with an {@link #eventQueue} and call\r | |
8 | * {@link #method-start}. Like so:\r | |
9 | *\r | |
10 | * var player = Ext.create('Ext.ux.event.Player', {\r | |
11 | * eventQueue: [ ... ],\r | |
12 | * speed: 2, // play at 2x speed\r | |
13 | * listeners: {\r | |
14 | * stop: function () {\r | |
15 | * player = null; // all done\r | |
16 | * }\r | |
17 | * }\r | |
18 | * });\r | |
19 | *\r | |
20 | * player.start();\r | |
21 | *\r | |
22 | * A more complex use would be to incorporate keyframe generation after playing certain\r | |
23 | * events.\r | |
24 | *\r | |
25 | * var player = Ext.create('Ext.ux.event.Player', {\r | |
26 | * eventQueue: [ ... ],\r | |
27 | * keyFrameEvents: {\r | |
28 | * click: true\r | |
29 | * },\r | |
30 | * listeners: {\r | |
31 | * stop: function () {\r | |
32 | * // play has completed... probably time for another keyframe...\r | |
33 | * player = null;\r | |
34 | * },\r | |
35 | * keyframe: onKeyFrame\r | |
36 | * }\r | |
37 | * });\r | |
38 | *\r | |
39 | * player.start();\r | |
40 | *\r | |
41 | * If a keyframe can be handled immediately (synchronously), the listener would be:\r | |
42 | *\r | |
43 | * function onKeyFrame () {\r | |
44 | * handleKeyFrame();\r | |
45 | * }\r | |
46 | *\r | |
47 | * If the keyframe event is always handled asynchronously, then the event listener is only\r | |
48 | * a bit more:\r | |
49 | *\r | |
50 | * function onKeyFrame (p, eventDescriptor) {\r | |
51 | * eventDescriptor.defer(); // pause event playback...\r | |
52 | *\r | |
53 | * handleKeyFrame(function () {\r | |
54 | * eventDescriptor.finish(); // ...resume event playback\r | |
55 | * });\r | |
56 | * }\r | |
57 | *\r | |
58 | * Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps\r | |
59 | * differently by browser), a slightly more complex listener is required.\r | |
60 | *\r | |
61 | * function onKeyFrame (p, eventDescriptor) {\r | |
62 | * var async;\r | |
63 | *\r | |
64 | * handleKeyFrame(function () {\r | |
65 | * // either this callback is being called immediately by handleKeyFrame (in\r | |
66 | * // which case async is undefined) or it is being called later (in which case\r | |
67 | * // async will be true).\r | |
68 | *\r | |
69 | * if (async) {\r | |
70 | * eventDescriptor.finish();\r | |
71 | * } else {\r | |
72 | * async = false;\r | |
73 | * }\r | |
74 | * });\r | |
75 | *\r | |
76 | * // either the callback was called (and async is now false) or it was not\r | |
77 | * // called (and async remains undefined).\r | |
78 | *\r | |
79 | * if (async !== false) {\r | |
80 | * eventDescriptor.defer();\r | |
81 | * async = true; // let the callback know that we have gone async\r | |
82 | * }\r | |
83 | * }\r | |
84 | */\r | |
85 | Ext.define('Ext.ux.event.Player', function (Player) {\r | |
86 | var defaults = {},\r | |
87 | mouseEvents = {},\r | |
88 | keyEvents = {},\r | |
89 | doc,\r | |
90 | \r | |
91 | //HTML events supported\r | |
92 | uiEvents = {},\r | |
93 | \r | |
94 | //events that bubble by default\r | |
95 | bubbleEvents = {\r | |
96 | //scroll: 1,\r | |
97 | resize: 1,\r | |
98 | reset: 1,\r | |
99 | submit: 1,\r | |
100 | change: 1,\r | |
101 | select: 1,\r | |
102 | error: 1,\r | |
103 | abort: 1\r | |
104 | };\r | |
105 | \r | |
106 | Ext.each([ 'click', 'dblclick', 'mouseover', 'mouseout', 'mousedown', 'mouseup', 'mousemove' ],\r | |
107 | function (type) {\r | |
108 | bubbleEvents[type] = defaults[type] = mouseEvents[type] = {\r | |
109 | bubbles: true,\r | |
110 | cancelable: (type != "mousemove"), // mousemove cannot be cancelled\r | |
111 | detail: 1,\r | |
112 | screenX: 0,\r | |
113 | screenY: 0,\r | |
114 | clientX: 0,\r | |
115 | clientY: 0,\r | |
116 | ctrlKey: false,\r | |
117 | altKey: false,\r | |
118 | shiftKey: false,\r | |
119 | metaKey: false,\r | |
120 | button: 0\r | |
121 | };\r | |
122 | });\r | |
123 | \r | |
124 | Ext.each([ 'keydown', 'keyup', 'keypress' ],\r | |
125 | function (type) {\r | |
126 | bubbleEvents[type] = defaults[type] = keyEvents[type] = {\r | |
127 | bubbles: true,\r | |
128 | cancelable: true,\r | |
129 | ctrlKey: false,\r | |
130 | altKey: false,\r | |
131 | shiftKey: false,\r | |
132 | metaKey: false,\r | |
133 | keyCode: 0,\r | |
134 | charCode: 0\r | |
135 | };\r | |
136 | });\r | |
137 | \r | |
138 | Ext.each([ 'blur', 'change', 'focus', 'resize', 'scroll', 'select' ],\r | |
139 | function (type) {\r | |
140 | defaults[type] = uiEvents[type] = {\r | |
141 | bubbles: (type in bubbleEvents),\r | |
142 | cancelable: false,\r | |
143 | detail: 1\r | |
144 | };\r | |
145 | });\r | |
146 | \r | |
147 | var inputSpecialKeys = {\r | |
148 | 8: function (target, start, end) { // backspace: 8,\r | |
149 | if (start < end) {\r | |
150 | target.value = target.value.substring(0, start) +\r | |
151 | target.value.substring(end);\r | |
152 | } else if (start > 0) {\r | |
153 | target.value = target.value.substring(0, --start) +\r | |
154 | target.value.substring(end);\r | |
155 | }\r | |
156 | this.setTextSelection(target, start, start);\r | |
157 | },\r | |
158 | 46: function (target, start, end) { // delete: 46\r | |
159 | if (start < end) {\r | |
160 | target.value = target.value.substring(0, start) +\r | |
161 | target.value.substring(end);\r | |
162 | } else if (start < target.value.length - 1) {\r | |
163 | target.value = target.value.substring(0, start) +\r | |
164 | target.value.substring(start+1);\r | |
165 | }\r | |
166 | this.setTextSelection(target, start, start);\r | |
167 | }\r | |
168 | };\r | |
169 | \r | |
170 | return {\r | |
171 | extend: 'Ext.ux.event.Driver',\r | |
172 | \r | |
173 | /**\r | |
174 | * @cfg {Array} eventQueue The event queue to playback. This must be provided before\r | |
175 | * the {@link #method-start} method is called.\r | |
176 | */\r | |
177 | \r | |
178 | /**\r | |
179 | * @cfg {Object} keyFrameEvents An object that describes the events that should generate\r | |
180 | * keyframe events. For example, `{ click: true }` would generate keyframe events after\r | |
181 | * each `click` event.\r | |
182 | */\r | |
183 | keyFrameEvents: {\r | |
184 | click: true\r | |
185 | },\r | |
186 | \r | |
187 | /**\r | |
188 | * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false\r | |
189 | * to ignore animations. Default is true.\r | |
190 | */\r | |
191 | pauseForAnimations: true,\r | |
192 | \r | |
193 | /**\r | |
194 | * @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the\r | |
195 | * recorded speed). A value of 2 would playback at 2x speed.\r | |
196 | */\r | |
197 | speed: 1.0,\r | |
198 | \r | |
199 | stallTime: 0,\r | |
200 | \r | |
201 | _inputSpecialKeys: {\r | |
202 | INPUT: inputSpecialKeys,\r | |
203 | \r | |
204 | TEXTAREA: Ext.apply({\r | |
205 | //13: function (target, start, end) { // enter: 8,\r | |
206 | //TODO ?\r | |
207 | //}\r | |
208 | }, inputSpecialKeys)\r | |
209 | },\r | |
210 | \r | |
211 | tagPathRegEx: /(\w+)(?:\[(\d+)\])?/,\r | |
212 | \r | |
213 | /**\r | |
214 | * @event beforeplay\r | |
215 | * Fires before an event is played.\r | |
216 | * @param {Ext.ux.event.Player} this\r | |
217 | * @param {Object} eventDescriptor The event descriptor about to be played.\r | |
218 | */\r | |
219 | \r | |
220 | /**\r | |
221 | * @event keyframe\r | |
222 | * Fires when this player reaches a keyframe. Typically, this is after events\r | |
223 | * like `click` are injected and any resulting animations have been completed.\r | |
224 | * @param {Ext.ux.event.Player} this\r | |
225 | * @param {Object} eventDescriptor The keyframe event descriptor.\r | |
226 | */\r | |
227 | \r | |
228 | constructor: function (config) {\r | |
229 | var me = this;\r | |
230 | \r | |
231 | me.callParent(arguments);\r | |
232 | \r | |
233 | me.timerFn = function () {\r | |
234 | me.onTick();\r | |
235 | };\r | |
236 | me.attachTo = me.attachTo || window;\r | |
237 | \r | |
238 | doc = me.attachTo.document;\r | |
239 | },\r | |
240 | \r | |
241 | /**\r | |
242 | * Returns the element given is XPath-like description.\r | |
243 | * @param {String} xpath The XPath-like description of the element.\r | |
244 | * @return {HTMLElement}\r | |
245 | */\r | |
246 | getElementFromXPath: function (xpath) {\r | |
247 | var me = this,\r | |
248 | parts = xpath.split('/'),\r | |
249 | regex = me.tagPathRegEx,\r | |
250 | i, n, m, count, tag, child,\r | |
251 | el = me.attachTo.document;\r | |
252 | \r | |
253 | el = (parts[0] == '~') ? el.body\r | |
254 | : el.getElementById(parts[0].substring(1)); // remove '#'\r | |
255 | \r | |
256 | for (i = 1, n = parts.length; el && i < n; ++i) {\r | |
257 | m = regex.exec(parts[i]);\r | |
258 | count = m[2] ? parseInt(m[2], 10) : 1;\r | |
259 | tag = m[1].toUpperCase();\r | |
260 | \r | |
261 | for (child = el.firstChild; child; child = child.nextSibling) {\r | |
262 | if (child.tagName == tag) {\r | |
263 | if (count == 1) {\r | |
264 | break;\r | |
265 | }\r | |
266 | --count;\r | |
267 | }\r | |
268 | }\r | |
269 | \r | |
270 | el = child;\r | |
271 | }\r | |
272 | \r | |
273 | return el;\r | |
274 | },\r | |
275 | \r | |
276 | // Moving across a line break only counts as moving one character in a TextRange, whereas a line break in\r | |
277 | // the textarea value is two characters. This function corrects for that by converting a text offset into a\r | |
278 | // range character offset by subtracting one character for every line break in the textarea prior to the\r | |
279 | // offset\r | |
280 | offsetToRangeCharacterMove: function(el, offset) {\r | |
281 | return offset - (el.value.slice(0, offset).split("\r\n").length - 1);\r | |
282 | },\r | |
283 | \r | |
284 | setTextSelection: function (el, startOffset, endOffset) {\r | |
285 | // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js\r | |
286 | if (startOffset < 0) {\r | |
287 | startOffset += el.value.length;\r | |
288 | }\r | |
289 | if (endOffset == null) {\r | |
290 | endOffset = startOffset;\r | |
291 | }\r | |
292 | if (endOffset < 0) {\r | |
293 | endOffset += el.value.length;\r | |
294 | }\r | |
295 | \r | |
296 | if (typeof el.selectionStart === "number") {\r | |
297 | el.selectionStart = startOffset;\r | |
298 | el.selectionEnd = endOffset;\r | |
299 | } else {\r | |
300 | var range = el.createTextRange();\r | |
301 | var startCharMove = this.offsetToRangeCharacterMove(el, startOffset);\r | |
302 | \r | |
303 | range.collapse(true);\r | |
304 | \r | |
305 | if (startOffset == endOffset) {\r | |
306 | range.move("character", startCharMove);\r | |
307 | } else {\r | |
308 | range.moveEnd("character", this.offsetToRangeCharacterMove(el, endOffset));\r | |
309 | range.moveStart("character", startCharMove);\r | |
310 | }\r | |
311 | \r | |
312 | range.select();\r | |
313 | }\r | |
314 | },\r | |
315 | \r | |
316 | getTimeIndex: function () {\r | |
317 | var t = this.getTimestamp() - this.stallTime;\r | |
318 | return t * this.speed;\r | |
319 | },\r | |
320 | \r | |
321 | makeToken: function (eventDescriptor, signal) {\r | |
322 | var me = this,\r | |
323 | t0;\r | |
324 | \r | |
325 | eventDescriptor[signal] = true;\r | |
326 | \r | |
327 | eventDescriptor.defer = function () {\r | |
328 | eventDescriptor[signal] = false;\r | |
329 | t0 = me.getTime();\r | |
330 | };\r | |
331 | \r | |
332 | eventDescriptor.finish = function () {\r | |
333 | eventDescriptor[signal] = true;\r | |
334 | me.stallTime += me.getTime() - t0;\r | |
335 | \r | |
336 | me.schedule();\r | |
337 | };\r | |
338 | },\r | |
339 | \r | |
340 | /**\r | |
341 | * This method is called after an event has been played to prepare for the next event.\r | |
342 | * @param {Object} eventDescriptor The descriptor of the event just played.\r | |
343 | */\r | |
344 | nextEvent: function (eventDescriptor) {\r | |
345 | var me = this,\r | |
346 | index = ++me.queueIndex;\r | |
347 | \r | |
348 | // keyframe events are inserted after a keyFrameEvent is played.\r | |
349 | if (me.keyFrameEvents[eventDescriptor.type]) {\r | |
350 | Ext.Array.insert(me.eventQueue, index, [\r | |
351 | {keyframe: true, ts: eventDescriptor.ts}\r | |
352 | ]);\r | |
353 | }\r | |
354 | },\r | |
355 | \r | |
356 | /**\r | |
357 | * This method returns the event descriptor at the front of the queue. This does not\r | |
358 | * dequeue the event. Repeated calls return the same object (until {@link #nextEvent}\r | |
359 | * is called).\r | |
360 | */\r | |
361 | peekEvent: function () {\r | |
362 | return this.eventQueue[this.queueIndex] || null;\r | |
363 | },\r | |
364 | \r | |
365 | /**\r | |
366 | * Replaces an event in the queue with an array of events. This is often used to roll\r | |
367 | * up a multi-step pseudo-event and expand it just-in-time to be played. The process\r | |
368 | * for doing this in a derived class would be this:\r | |
369 | * \r | |
370 | * Ext.define('My.Player', {\r | |
371 | * extend: 'Ext.ux.event.Player',\r | |
372 | *\r | |
373 | * peekEvent: function () {\r | |
374 | * var event = this.callParent();\r | |
375 | *\r | |
376 | * if (event.multiStepSpecial) {\r | |
377 | * this.replaceEvent(null, [\r | |
378 | * ... expand to actual events\r | |
379 | * ]);\r | |
380 | *\r | |
381 | * event = this.callParent(); // get the new next event\r | |
382 | * }\r | |
383 | *\r | |
384 | * return event;\r | |
385 | * }\r | |
386 | * });\r | |
387 | * \r | |
388 | * This method ensures that the `beforeplay` hook (if any) from the replaced event is\r | |
389 | * placed on the first new event and the `afterplay` hook (if any) is placed on the\r | |
390 | * last new event.\r | |
391 | * \r | |
392 | * @param {Number} index The queue index to replace. Pass `null` to replace the event\r | |
393 | * at the current `queueIndex`.\r | |
394 | * @param {Event[]} events The array of events with which to replace the specified\r | |
395 | * event.\r | |
396 | */\r | |
397 | replaceEvent: function (index, events) {\r | |
398 | for (var t, i = 0, n = events.length; i < n; ++i) {\r | |
399 | if (i) {\r | |
400 | t = events[i-1];\r | |
401 | delete t.afterplay;\r | |
402 | delete t.screenshot;\r | |
403 | \r | |
404 | delete events[i].beforeplay;\r | |
405 | }\r | |
406 | }\r | |
407 | \r | |
408 | Ext.Array.replace(this.eventQueue, (index == null) ? this.queueIndex : index,\r | |
409 | 1, events);\r | |
410 | },\r | |
411 | \r | |
412 | /**\r | |
413 | * This method dequeues and injects events until it has arrived at the time index. If\r | |
414 | * no events are ready (based on the time index), this method does nothing.\r | |
415 | * @return {Boolean} True if there is more to do; false if not (at least for now).\r | |
416 | */\r | |
417 | processEvents: function () {\r | |
418 | var me = this,\r | |
419 | animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items,\r | |
420 | eventDescriptor;\r | |
421 | \r | |
422 | while ((eventDescriptor = me.peekEvent()) !== null) {\r | |
423 | if (animations && animations.getCount()) {\r | |
424 | return true;\r | |
425 | }\r | |
426 | \r | |
427 | if (eventDescriptor.keyframe) {\r | |
428 | if (!me.processKeyFrame(eventDescriptor)) {\r | |
429 | return false;\r | |
430 | }\r | |
431 | me.nextEvent(eventDescriptor);\r | |
432 | } else if (eventDescriptor.ts <= me.getTimeIndex() &&\r | |
433 | me.fireEvent('beforeplay', me, eventDescriptor) !== false &&\r | |
434 | me.playEvent(eventDescriptor)) {\r | |
435 | me.nextEvent(eventDescriptor);\r | |
436 | } else {\r | |
437 | return true;\r | |
438 | }\r | |
439 | }\r | |
440 | \r | |
441 | me.stop();\r | |
442 | return false;\r | |
443 | },\r | |
444 | \r | |
445 | /**\r | |
446 | * This method is called when a keyframe is reached. This will fire the keyframe event.\r | |
447 | * If the keyframe has been handled, true is returned. Otherwise, false is returned.\r | |
448 | * @param {Object} eventDescriptor The event descriptor of the keyframe.\r | |
449 | * @return {Boolean} True if the keyframe was handled, false if not.\r | |
450 | */\r | |
451 | processKeyFrame: function (eventDescriptor) {\r | |
452 | var me = this;\r | |
453 | \r | |
454 | // only fire keyframe event (and setup the eventDescriptor) once...\r | |
455 | if (!eventDescriptor.defer) {\r | |
456 | me.makeToken(eventDescriptor, 'done');\r | |
457 | me.fireEvent('keyframe', me, eventDescriptor);\r | |
458 | }\r | |
459 | \r | |
460 | return eventDescriptor.done;\r | |
461 | },\r | |
462 | \r | |
463 | /**\r | |
464 | * Called to inject the given event on the specified target.\r | |
465 | * @param {HTMLElement} target The target of the event.\r | |
466 | * @param {Object} event The event to inject. The properties of this object should be\r | |
467 | * those of standard DOM events but vary based on the `type` property. For details on\r | |
468 | * event types and their properties, see the class documentation.\r | |
469 | */\r | |
470 | injectEvent: function (target, event) {\r | |
471 | var me = this,\r | |
472 | type = event.type,\r | |
473 | options = Ext.apply({}, event, defaults[type]),\r | |
474 | handler;\r | |
475 | \r | |
476 | if (type === 'type') {\r | |
477 | handler = me._inputSpecialKeys[target.tagName];\r | |
478 | if (handler) {\r | |
479 | return me.injectTypeInputEvent(target, event, handler);\r | |
480 | }\r | |
481 | return me.injectTypeEvent(target, event);\r | |
482 | }\r | |
483 | if (type === 'focus' && target.focus) {\r | |
484 | target.focus();\r | |
485 | return true;\r | |
486 | }\r | |
487 | if (type === 'blur' && target.blur) {\r | |
488 | target.blur();\r | |
489 | return true;\r | |
490 | }\r | |
491 | if (type === 'scroll') {\r | |
492 | target.scrollLeft = event.pos[0];\r | |
493 | target.scrollTop = event.pos[1];\r | |
494 | return true;\r | |
495 | }\r | |
496 | if (type === 'mduclick') {\r | |
497 | return me.injectEvent(target, Ext.applyIf({ type: 'mousedown' }, event)) &&\r | |
498 | me.injectEvent(target, Ext.applyIf({ type: 'mouseup' }, event)) &&\r | |
499 | me.injectEvent(target, Ext.applyIf({ type: 'click' }, event));\r | |
500 | }\r | |
501 | \r | |
502 | if (mouseEvents[type]){\r | |
503 | return Player.injectMouseEvent(target, options, me.attachTo);\r | |
504 | }\r | |
505 | \r | |
506 | if (keyEvents[type]){\r | |
507 | return Player.injectKeyEvent(target, options, me.attachTo);\r | |
508 | }\r | |
509 | \r | |
510 | if (uiEvents[type]){\r | |
511 | return Player.injectUIEvent(target, type,\r | |
512 | options.bubbles,\r | |
513 | options.cancelable,\r | |
514 | options.view || me.attachTo,\r | |
515 | options.detail);\r | |
516 | }\r | |
517 | \r | |
518 | return false;\r | |
519 | },\r | |
520 | \r | |
521 | injectTypeEvent: function (target, event) {\r | |
522 | var me = this,\r | |
523 | text = event.text,\r | |
524 | xlat = [],\r | |
525 | ch, chUp, i, n, sel, upper, isInput;\r | |
526 | \r | |
527 | if (text) {\r | |
528 | delete event.text;\r | |
529 | upper = text.toUpperCase();\r | |
530 | \r | |
531 | for (i = 0, n = text.length; i < n; ++i) {\r | |
532 | ch = text.charCodeAt(i);\r | |
533 | chUp = upper.charCodeAt(i);\r | |
534 | \r | |
535 | xlat.push(\r | |
536 | Ext.applyIf({type: 'keydown', charCode: chUp, keyCode: chUp}, event),\r | |
537 | Ext.applyIf({type: 'keypress', charCode: ch, keyCode: ch}, event),\r | |
538 | Ext.applyIf({type: 'keyup', charCode: chUp, keyCode: chUp}, event)\r | |
539 | );\r | |
540 | }\r | |
541 | } else {\r | |
542 | xlat.push(\r | |
543 | Ext.applyIf({type: 'keydown', charCode: event.keyCode}, event),\r | |
544 | Ext.applyIf({type: 'keyup', charCode: event.keyCode}, event)\r | |
545 | );\r | |
546 | }\r | |
547 | \r | |
548 | for (i = 0, n = xlat.length; i < n; ++i) {\r | |
549 | me.injectEvent(target, xlat[i]);\r | |
550 | }\r | |
551 | \r | |
552 | return true;\r | |
553 | },\r | |
554 | \r | |
555 | injectTypeInputEvent: function (target, event, handler) {\r | |
556 | var me = this,\r | |
557 | text = event.text,\r | |
558 | sel, n;\r | |
559 | \r | |
560 | if (handler) {\r | |
561 | sel = me.getTextSelection(target);\r | |
562 | \r | |
563 | if (text) {\r | |
564 | n = sel[0];\r | |
565 | target.value = target.value.substring(0, n) + text +\r | |
566 | target.value.substring(sel[1]);\r | |
567 | n += text.length;\r | |
568 | me.setTextSelection(target, n, n);\r | |
569 | } else {\r | |
570 | if (!(handler = handler[event.keyCode])) {\r | |
571 | // no handler for the special key for this element\r | |
572 | if ('caret' in event) {\r | |
573 | me.setTextSelection(target, event.caret, event.caret);\r | |
574 | } else if (event.selection) {\r | |
575 | me.setTextSelection(target, event.selection[0], event.selection[1]);\r | |
576 | }\r | |
577 | return me.injectTypeEvent(target, event);\r | |
578 | }\r | |
579 | handler.call(this, target, sel[0], sel[1]);\r | |
580 | return true;\r | |
581 | }\r | |
582 | }\r | |
583 | \r | |
584 | return true;\r | |
585 | },\r | |
586 | \r | |
587 | playEvent: function (eventDescriptor) {\r | |
588 | var me = this,\r | |
589 | target = me.getElementFromXPath(eventDescriptor.target),\r | |
590 | event;\r | |
591 | \r | |
592 | if (!target) {\r | |
593 | // not present (yet)... wait for element present...\r | |
594 | // TODO - need a timeout here\r | |
595 | return false;\r | |
596 | }\r | |
597 | \r | |
598 | if (!me.playEventHook(eventDescriptor, 'beforeplay')) {\r | |
599 | return false;\r | |
600 | }\r | |
601 | \r | |
602 | if (!eventDescriptor.injected) {\r | |
603 | eventDescriptor.injected = true;\r | |
604 | event = me.translateEvent(eventDescriptor, target);\r | |
605 | me.injectEvent(target, event);\r | |
606 | }\r | |
607 | \r | |
608 | return me.playEventHook(eventDescriptor, 'afterplay');\r | |
609 | },\r | |
610 | \r | |
611 | playEventHook: function (eventDescriptor, hookName) {\r | |
612 | var me = this,\r | |
613 | doneName = hookName + '.done',\r | |
614 | firedName = hookName + '.fired',\r | |
615 | hook = eventDescriptor[hookName];\r | |
616 | \r | |
617 | if (hook && !eventDescriptor[doneName]) {\r | |
618 | if (!eventDescriptor[firedName]) {\r | |
619 | eventDescriptor[firedName] = true;\r | |
620 | me.makeToken(eventDescriptor, doneName);\r | |
621 | \r | |
622 | if (me.eventScope && Ext.isString(hook)) {\r | |
623 | hook = me.eventScope[hook];\r | |
624 | }\r | |
625 | \r | |
626 | if (hook) {\r | |
627 | hook.call(me.eventScope || me, eventDescriptor);\r | |
628 | }\r | |
629 | }\r | |
630 | return false;\r | |
631 | }\r | |
632 | \r | |
633 | return true;\r | |
634 | },\r | |
635 | \r | |
636 | schedule: function () {\r | |
637 | var me = this;\r | |
638 | if (!me.timer) {\r | |
639 | me.timer = setTimeout(me.timerFn, 10);\r | |
640 | }\r | |
641 | },\r | |
642 | \r | |
643 | _translateAcross: [\r | |
644 | 'type',\r | |
645 | 'button',\r | |
646 | 'charCode',\r | |
647 | 'keyCode',\r | |
648 | 'caret',\r | |
649 | 'pos',\r | |
650 | 'text',\r | |
651 | 'selection'\r | |
652 | ],\r | |
653 | \r | |
654 | translateEvent: function (eventDescriptor, target) {\r | |
655 | var me = this,\r | |
656 | event = {},\r | |
657 | modKeys = eventDescriptor.modKeys || '',\r | |
658 | names = me._translateAcross,\r | |
659 | i = names.length,\r | |
660 | name, xy;\r | |
661 | \r | |
662 | while (i--) {\r | |
663 | name = names[i];\r | |
664 | if (name in eventDescriptor) {\r | |
665 | event[name] = eventDescriptor[name];\r | |
666 | }\r | |
667 | }\r | |
668 | \r | |
669 | event.altKey = modKeys.indexOf('A') > 0;\r | |
670 | event.ctrlKey = modKeys.indexOf('C') > 0;\r | |
671 | event.metaKey = modKeys.indexOf('M') > 0;\r | |
672 | event.shiftKey = modKeys.indexOf('S') > 0;\r | |
673 | \r | |
674 | if (target && 'x' in eventDescriptor) {\r | |
675 | xy = Ext.fly(target).getXY();\r | |
676 | xy[0] += eventDescriptor.x;\r | |
677 | xy[1] += eventDescriptor.y;\r | |
678 | } else if ('x' in eventDescriptor) {\r | |
679 | xy = [ eventDescriptor.x, eventDescriptor.y ];\r | |
680 | } else if ('px' in eventDescriptor) {\r | |
681 | xy = [ eventDescriptor.px, eventDescriptor.py ];\r | |
682 | }\r | |
683 | \r | |
684 | if (xy) {\r | |
685 | event.clientX = event.screenX = xy[0];\r | |
686 | event.clientY = event.screenY = xy[1];\r | |
687 | }\r | |
688 | \r | |
689 | if (eventDescriptor.key) {\r | |
690 | event.keyCode = me.specialKeysByName[eventDescriptor.key];\r | |
691 | }\r | |
692 | \r | |
693 | if (eventDescriptor.type === 'wheel') {\r | |
694 | if ('onwheel' in me.attachTo.document) {\r | |
695 | event.wheelX = eventDescriptor.dx;\r | |
696 | event.wheelY = eventDescriptor.dy;\r | |
697 | } else {\r | |
698 | event.type = 'mousewheel';\r | |
699 | event.wheelDeltaX = -40 * eventDescriptor.dx;\r | |
700 | event.wheelDeltaY = event.wheelDelta = -40 * eventDescriptor.dy;\r | |
701 | }\r | |
702 | }\r | |
703 | \r | |
704 | return event;\r | |
705 | },\r | |
706 | \r | |
707 | //---------------------------------\r | |
708 | // Driver overrides\r | |
709 | \r | |
710 | onStart: function () {\r | |
711 | var me = this;\r | |
712 | \r | |
713 | me.queueIndex = 0;\r | |
714 | me.schedule();\r | |
715 | },\r | |
716 | \r | |
717 | onStop: function () {\r | |
718 | var me = this;\r | |
719 | \r | |
720 | if (me.timer) {\r | |
721 | clearTimeout(me.timer);\r | |
722 | me.timer = null;\r | |
723 | }\r | |
724 | },\r | |
725 | \r | |
726 | //---------------------------------\r | |
727 | \r | |
728 | onTick: function () {\r | |
729 | var me = this;\r | |
730 | \r | |
731 | me.timer = null;\r | |
732 | \r | |
733 | if (me.processEvents()) {\r | |
734 | me.schedule();\r | |
735 | }\r | |
736 | },\r | |
737 | \r | |
738 | statics: {\r | |
739 | ieButtonCodeMap: {\r | |
740 | 0: 1,\r | |
741 | 1: 4,\r | |
742 | 2: 2\r | |
743 | },\r | |
744 | \r | |
745 | /**\r | |
746 | * Injects a key event using the given event information to populate the event\r | |
747 | * object.\r | |
748 | * \r | |
749 | * **Note:** `keydown` causes Safari 2.x to crash.\r | |
750 | * \r | |
751 | * @param {HTMLElement} target The target of the given event.\r | |
752 | * @param {Object} options Object object containing all of the event injection\r | |
753 | * options.\r | |
754 | * @param {String} options.type The type of event to fire. This can be any one of\r | |
755 | * the following: `keyup`, `keydown` and `keypress`.\r | |
756 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.\r | |
757 | * DOM Level 3 specifies that all key events bubble by default.\r | |
758 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled\r | |
759 | * using `preventDefault`. DOM Level 3 specifies that all key events can be\r | |
760 | * cancelled.\r | |
761 | * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is\r | |
762 | * pressed while the event is firing.\r | |
763 | * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is\r | |
764 | * pressed while the event is firing.\r | |
765 | * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is\r | |
766 | * pressed while the event is firing.\r | |
767 | * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is\r | |
768 | * pressed while the event is firing.\r | |
769 | * @param {Number} [options.keyCode=0] The code for the key that is in use.\r | |
770 | * @param {Number} [options.charCode=0] The Unicode code for the character \r | |
771 | * associated with the key being used.\r | |
772 | * @param {Window} [view=window] The view containing the target. This is typically\r | |
773 | * the window object.\r | |
774 | * @private\r | |
775 | */\r | |
776 | injectKeyEvent: function (target, options, view) {\r | |
777 | var type = options.type,\r | |
778 | customEvent = null;\r | |
779 | \r | |
780 | if (type === 'textevent') {\r | |
781 | type = 'keypress';\r | |
782 | }\r | |
783 | view = view || window;\r | |
784 | \r | |
785 | //check for DOM-compliant browsers first\r | |
786 | if (doc.createEvent){\r | |
787 | try {\r | |
788 | customEvent = doc.createEvent("KeyEvents");\r | |
789 | \r | |
790 | // Interesting problem: Firefox implemented a non-standard\r | |
791 | // version of initKeyEvent() based on DOM Level 2 specs.\r | |
792 | // Key event was removed from DOM Level 2 and re-introduced\r | |
793 | // in DOM Level 3 with a different interface. Firefox is the\r | |
794 | // only browser with any implementation of Key Events, so for\r | |
795 | // now, assume it's Firefox if the above line doesn't error.\r | |
796 | \r | |
797 | // @TODO: Decipher between Firefox's implementation and a correct one.\r | |
798 | customEvent.initKeyEvent(type, options.bubbles, options.cancelable,\r | |
799 | view, options.ctrlKey, options.altKey, options.shiftKey,\r | |
800 | options.metaKey, options.keyCode, options.charCode);\r | |
801 | \r | |
802 | } catch (ex) {\r | |
803 | // If it got here, that means key events aren't officially supported. \r | |
804 | // Safari/WebKit is a real problem now. WebKit 522 won't let you\r | |
805 | // set keyCode, charCode, or other properties if you use a\r | |
806 | // UIEvent, so we first must try to create a generic event. The\r | |
807 | // fun part is that this will throw an error on Safari 2.x. The\r | |
808 | // end result is that we need another try...catch statement just to\r | |
809 | // deal with this mess.\r | |
810 | \r | |
811 | try {\r | |
812 | //try to create generic event - will fail in Safari 2.x\r | |
813 | customEvent = doc.createEvent("Events");\r | |
814 | \r | |
815 | } catch (uierror) {\r | |
816 | //the above failed, so create a UIEvent for Safari 2.x\r | |
817 | customEvent = doc.createEvent("UIEvents");\r | |
818 | \r | |
819 | } finally {\r | |
820 | customEvent.initEvent(type, options.bubbles, options.cancelable);\r | |
821 | \r | |
822 | customEvent.view = view;\r | |
823 | customEvent.altKey = options.altKey;\r | |
824 | customEvent.ctrlKey = options.ctrlKey;\r | |
825 | customEvent.shiftKey = options.shiftKey;\r | |
826 | customEvent.metaKey = options.metaKey;\r | |
827 | customEvent.keyCode = options.keyCode;\r | |
828 | customEvent.charCode = options.charCode;\r | |
829 | } \r | |
830 | }\r | |
831 | \r | |
832 | target.dispatchEvent(customEvent);\r | |
833 | \r | |
834 | } else if (doc.createEventObject) { //IE\r | |
835 | customEvent = doc.createEventObject();\r | |
836 | \r | |
837 | customEvent.bubbles = options.bubbles;\r | |
838 | customEvent.cancelable = options.cancelable;\r | |
839 | customEvent.view = view;\r | |
840 | customEvent.ctrlKey = options.ctrlKey;\r | |
841 | customEvent.altKey = options.altKey;\r | |
842 | customEvent.shiftKey = options.shiftKey;\r | |
843 | customEvent.metaKey = options.metaKey;\r | |
844 | \r | |
845 | // IE doesn't support charCode explicitly. CharCode should\r | |
846 | // take precedence over any keyCode value for accurate\r | |
847 | // representation.\r | |
848 | \r | |
849 | customEvent.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;\r | |
850 | \r | |
851 | target.fireEvent("on" + type, customEvent); \r | |
852 | \r | |
853 | } else {\r | |
854 | return false;\r | |
855 | }\r | |
856 | \r | |
857 | return true;\r | |
858 | },\r | |
859 | \r | |
860 | /**\r | |
861 | * Injects a mouse event using the given event information to populate the event\r | |
862 | * object.\r | |
863 | *\r | |
864 | * @param {HTMLElement} target The target of the given event.\r | |
865 | * @param {Object} options Object object containing all of the event injection\r | |
866 | * options.\r | |
867 | * @param {String} options.type The type of event to fire. This can be any one of\r | |
868 | * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,\r | |
869 | * `mouseover` and `mousemove`.\r | |
870 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.\r | |
871 | * DOM Level 2 specifies that all mouse events bubble by default.\r | |
872 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled\r | |
873 | * using `preventDefault`. DOM Level 2 specifies that all mouse events except\r | |
874 | * `mousemove` can be cancelled. This defaults to `false` for `mousemove`.\r | |
875 | * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is\r | |
876 | * pressed while the event is firing.\r | |
877 | * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is\r | |
878 | * pressed while the event is firing.\r | |
879 | * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is\r | |
880 | * pressed while the event is firing.\r | |
881 | * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is\r | |
882 | * pressed while the event is firing.\r | |
883 | * @param {Number} [options.detail=1] The number of times the mouse button has \r | |
884 | * been used.\r | |
885 | * @param {Number} [options.screenX=0] The x-coordinate on the screen at which point\r | |
886 | * the event occurred.\r | |
887 | * @param {Number} [options.screenY=0] The y-coordinate on the screen at which point\r | |
888 | * the event occurred.\r | |
889 | * @param {Number} [options.clientX=0] The x-coordinate on the client at which point\r | |
890 | * the event occurred.\r | |
891 | * @param {Number} [options.clientY=0] The y-coordinate on the client at which point\r | |
892 | * the event occurred.\r | |
893 | * @param {Number} [options.button=0] The button being pressed while the event is\r | |
894 | * executing. The value should be 0 for the primary mouse button (typically the\r | |
895 | * left button), 1 for the tertiary mouse button (typically the middle button),\r | |
896 | * and 2 for the secondary mouse button (typically the right button).\r | |
897 | * @param {HTMLElement} [options.relatedTarget=null] For `mouseout` events, this\r | |
898 | * is the element that the mouse has moved to. For `mouseover` events, this is\r | |
899 | * the element that the mouse has moved from. This argument is ignored for all\r | |
900 | * other events.\r | |
901 | * @param {Window} [view=window] The view containing the target. This is typically\r | |
902 | * the window object.\r | |
903 | * @private\r | |
904 | */\r | |
905 | injectMouseEvent: function (target, options, view) {\r | |
906 | var type = options.type,\r | |
907 | customEvent = null;\r | |
908 | \r | |
909 | view = view || window;\r | |
910 | \r | |
911 | //check for DOM-compliant browsers first\r | |
912 | if (doc.createEvent){\r | |
913 | customEvent = doc.createEvent("MouseEvents");\r | |
914 | \r | |
915 | //Safari 2.x (WebKit 418) still doesn't implement initMouseEvent()\r | |
916 | if (customEvent.initMouseEvent){\r | |
917 | customEvent.initMouseEvent(type, options.bubbles, options.cancelable,\r | |
918 | view, options.detail, options.screenX, options.screenY,\r | |
919 | options.clientX, options.clientY, options.ctrlKey,\r | |
920 | options.altKey, options.shiftKey, options.metaKey,\r | |
921 | options.button, options.relatedTarget);\r | |
922 | } else { //Safari\r | |
923 | //the closest thing available in Safari 2.x is UIEvents\r | |
924 | customEvent = doc.createEvent("UIEvents");\r | |
925 | \r | |
926 | customEvent.initEvent(type, options.bubbles, options.cancelable);\r | |
927 | \r | |
928 | customEvent.view = view;\r | |
929 | customEvent.detail = options.detail;\r | |
930 | customEvent.screenX = options.screenX;\r | |
931 | customEvent.screenY = options.screenY;\r | |
932 | customEvent.clientX = options.clientX;\r | |
933 | customEvent.clientY = options.clientY;\r | |
934 | customEvent.ctrlKey = options.ctrlKey;\r | |
935 | customEvent.altKey = options.altKey;\r | |
936 | customEvent.metaKey = options.metaKey;\r | |
937 | customEvent.shiftKey = options.shiftKey;\r | |
938 | customEvent.button = options.button;\r | |
939 | customEvent.relatedTarget = options.relatedTarget;\r | |
940 | }\r | |
941 | \r | |
942 | /*\r | |
943 | * Check to see if relatedTarget has been assigned. Firefox\r | |
944 | * versions less than 2.0 don't allow it to be assigned via\r | |
945 | * initMouseEvent() and the property is readonly after event\r | |
946 | * creation, so in order to keep YAHOO.util.getRelatedTarget()\r | |
947 | * working, assign to the IE proprietary toElement property\r | |
948 | * for mouseout event and fromElement property for mouseover\r | |
949 | * event.\r | |
950 | */\r | |
951 | if (options.relatedTarget && !customEvent.relatedTarget){\r | |
952 | if (type == "mouseout"){\r | |
953 | customEvent.toElement = options.relatedTarget;\r | |
954 | } else if (type == "mouseover"){\r | |
955 | customEvent.fromElement = options.relatedTarget;\r | |
956 | }\r | |
957 | }\r | |
958 | \r | |
959 | target.dispatchEvent(customEvent);\r | |
960 | \r | |
961 | } else if (doc.createEventObject) { //IE\r | |
962 | customEvent = doc.createEventObject();\r | |
963 | \r | |
964 | customEvent.bubbles = options.bubbles;\r | |
965 | customEvent.cancelable = options.cancelable;\r | |
966 | customEvent.view = view;\r | |
967 | customEvent.detail = options.detail;\r | |
968 | customEvent.screenX = options.screenX;\r | |
969 | customEvent.screenY = options.screenY;\r | |
970 | customEvent.clientX = options.clientX;\r | |
971 | customEvent.clientY = options.clientY;\r | |
972 | customEvent.ctrlKey = options.ctrlKey;\r | |
973 | customEvent.altKey = options.altKey;\r | |
974 | customEvent.metaKey = options.metaKey;\r | |
975 | customEvent.shiftKey = options.shiftKey;\r | |
976 | customEvent.button = Player.ieButtonCodeMap[options.button] || 0;\r | |
977 | \r | |
978 | /*\r | |
979 | * Have to use relatedTarget because IE won't allow assignment\r | |
980 | * to toElement or fromElement on generic events. This keeps\r | |
981 | * YAHOO.util.customEvent.getRelatedTarget() functional.\r | |
982 | */\r | |
983 | customEvent.relatedTarget = options.relatedTarget;\r | |
984 | \r | |
985 | target.fireEvent('on' + type, customEvent);\r | |
986 | } else {\r | |
987 | return false;\r | |
988 | }\r | |
989 | \r | |
990 | return true;\r | |
991 | },\r | |
992 | \r | |
993 | /**\r | |
994 | * Injects a UI event using the given event information to populate the event\r | |
995 | * object.\r | |
996 | * \r | |
997 | * @param {HTMLElement} target The target of the given event.\r | |
998 | * @param {Object} options\r | |
999 | * @param {String} options.type The type of event to fire. This can be any one of\r | |
1000 | * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,\r | |
1001 | * `mouseover` and `mousemove`.\r | |
1002 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.\r | |
1003 | * DOM Level 2 specifies that all mouse events bubble by default.\r | |
1004 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled\r | |
1005 | * using `preventDefault`. DOM Level 2 specifies that all mouse events except\r | |
1006 | * `mousemove` can be canceled. This defaults to `false` for `mousemove`.\r | |
1007 | * @param {Number} [options.detail=1] The number of times the mouse button has been\r | |
1008 | * used.\r | |
1009 | * @param {Window} [view=window] The view containing the target. This is typically\r | |
1010 | * the window object.\r | |
1011 | * @private\r | |
1012 | */\r | |
1013 | injectUIEvent: function (target, options, view) {\r | |
1014 | var customEvent = null; \r | |
1015 | \r | |
1016 | view = view || window;\r | |
1017 | \r | |
1018 | //check for DOM-compliant browsers first\r | |
1019 | if (doc.createEvent){\r | |
1020 | //just a generic UI Event object is needed\r | |
1021 | customEvent = doc.createEvent("UIEvents");\r | |
1022 | \r | |
1023 | customEvent.initUIEvent(options.type, options.bubbles, options.cancelable,\r | |
1024 | view, options.detail);\r | |
1025 | \r | |
1026 | target.dispatchEvent(customEvent);\r | |
1027 | \r | |
1028 | } else if (doc.createEventObject){ //IE\r | |
1029 | customEvent = doc.createEventObject();\r | |
1030 | \r | |
1031 | customEvent.bubbles = options.bubbles;\r | |
1032 | customEvent.cancelable = options.cancelable;\r | |
1033 | customEvent.view = view;\r | |
1034 | customEvent.detail = options.detail;\r | |
1035 | \r | |
1036 | target.fireEvent("on" + options.type, customEvent);\r | |
1037 | \r | |
1038 | } else {\r | |
1039 | return false;\r | |
1040 | }\r | |
1041 | \r | |
1042 | return true;\r | |
1043 | }\r | |
1044 | } // statics\r | |
1045 | };\r | |
1046 | });\r |