]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /** |
2 | * This is a base class for more advanced "simlets" (simulated servers). A simlet is asked | |
3 | * to provide a response given a {@link Ext.ux.ajax.SimXhr} instance. | |
4 | */\r | |
5 | Ext.define('Ext.ux.ajax.Simlet', function() {\r | |
6 | var urlRegex = /([^?#]*)(#.*)?$/,\r | |
7 | dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/,\r | |
8 | intRegex = /^[+-]?\d+$/,\r | |
9 | floatRegex = /^[+-]?\d+\.\d+$/;\r | |
10 | function parseParamValue(value) {\r | |
11 | var m;\r | |
12 | if (Ext.isDefined(value)) {\r | |
13 | value = decodeURIComponent(value);\r | |
14 | if (intRegex.test(value)) {\r | |
15 | value = parseInt(value, 10);\r | |
16 | } else if (floatRegex.test(value)) {\r | |
17 | value = parseFloat(value);\r | |
18 | } else if (!!(m = dateRegex.test(value))) {\r | |
19 | value = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]));\r | |
20 | }\r | |
21 | }\r | |
22 | return value;\r | |
23 | }\r | |
24 | return {\r | |
25 | alias: 'simlet.basic',\r | |
26 | isSimlet: true,\r | |
27 | responseProps: [\r | |
28 | 'responseText',\r | |
29 | 'responseXML',\r | |
30 | 'status',\r | |
31 | 'statusText'\r | |
32 | ],\r | |
33 | /** | |
34 | * @cfg {Number} responseText | |
35 | */\r | |
36 | /** | |
37 | * @cfg {Number} responseXML | |
38 | */\r | |
39 | /** | |
40 | * @cfg {Object} responseHeaders | |
41 | */\r | |
42 | /** | |
43 | * @cfg {Number} status | |
44 | */\r | |
45 | status: 200,\r | |
46 | /** | |
47 | * @cfg {String} statusText | |
48 | */\r | |
49 | statusText: 'OK',\r | |
50 | constructor: function(config) {\r | |
51 | Ext.apply(this, config);\r | |
52 | },\r | |
53 | doGet: function(ctx) {\r | |
54 | var me = this,\r | |
55 | ret = {};\r | |
56 | Ext.Array.forEach(me.responseProps, function(prop) {\r | |
57 | if (prop in me) {\r | |
58 | ret[prop] = me[prop];\r | |
59 | }\r | |
60 | });\r | |
61 | return ret;\r | |
62 | },\r | |
63 | doPost: function(ctx) {\r | |
64 | var me = this,\r | |
65 | ret = {};\r | |
66 | Ext.Array.forEach(me.responseProps, function(prop) {\r | |
67 | if (prop in me) {\r | |
68 | ret[prop] = me[prop];\r | |
69 | }\r | |
70 | });\r | |
71 | return ret;\r | |
72 | },\r | |
73 | doRedirect: function(ctx) {\r | |
74 | return false;\r | |
75 | },\r | |
76 | doDelete: function(ctx) {\r | |
77 | var me = this,\r | |
78 | xhr = ctx.xhr,\r | |
79 | records = xhr.options.records;\r | |
80 | me.removeFromData(ctx, records);\r | |
81 | },\r | |
82 | /** | |
83 | * Performs the action requested by the given XHR and returns an object to be applied | |
84 | * on to the XHR (containing `status`, `responseText`, etc.). For the most part, | |
85 | * this is delegated to `doMethod` methods on this class, such as `doGet`. | |
86 | * | |
87 | * @param {Ext.ux.ajax.SimXhr} xhr The simulated XMLHttpRequest instance. | |
88 | * @return {Object} The response properties to add to the XMLHttpRequest. | |
89 | */\r | |
90 | exec: function(xhr) {\r | |
91 | var me = this,\r | |
92 | ret = {},\r | |
93 | method = 'do' + Ext.String.capitalize(xhr.method.toLowerCase()),\r | |
94 | // doGet\r | |
95 | fn = me[method];\r | |
96 | if (fn) {\r | |
97 | ret = fn.call(me, me.getCtx(xhr.method, xhr.url, xhr));\r | |
98 | } else {\r | |
99 | ret = {\r | |
100 | status: 405,\r | |
101 | statusText: 'Method Not Allowed'\r | |
102 | };\r | |
103 | }\r | |
104 | return ret;\r | |
105 | },\r | |
106 | getCtx: function(method, url, xhr) {\r | |
107 | return {\r | |
108 | method: method,\r | |
109 | params: this.parseQueryString(url),\r | |
110 | url: url,\r | |
111 | xhr: xhr\r | |
112 | };\r | |
113 | },\r | |
114 | openRequest: function(method, url, options, async) {\r | |
115 | var ctx = this.getCtx(method, url),\r | |
116 | redirect = this.doRedirect(ctx),\r | |
117 | xhr;\r | |
118 | if (options.action === 'destroy') {\r | |
119 | method = 'delete';\r | |
120 | }\r | |
121 | if (redirect) {\r | |
122 | xhr = redirect;\r | |
123 | } else {\r | |
124 | xhr = new Ext.ux.ajax.SimXhr({\r | |
125 | mgr: this.manager,\r | |
126 | simlet: this,\r | |
127 | options: options\r | |
128 | });\r | |
129 | xhr.open(method, url, async);\r | |
130 | }\r | |
131 | return xhr;\r | |
132 | },\r | |
133 | parseQueryString: function(str) {\r | |
134 | var m = urlRegex.exec(str),\r | |
135 | ret = {},\r | |
136 | key, value, i, n;\r | |
137 | if (m && m[1]) {\r | |
138 | var pair,\r | |
139 | parts = m[1].split('&');\r | |
140 | for (i = 0 , n = parts.length; i < n; ++i) {\r | |
141 | if ((pair = parts[i].split('='))[0]) {\r | |
142 | key = decodeURIComponent(pair.shift());\r | |
143 | value = parseParamValue((pair.length > 1) ? pair.join('=') : pair[0]);\r | |
144 | if (!(key in ret)) {\r | |
145 | ret[key] = value;\r | |
146 | } else if (Ext.isArray(ret[key])) {\r | |
147 | ret[key].push(value);\r | |
148 | } else {\r | |
149 | ret[key] = [\r | |
150 | ret[key],\r | |
151 | value\r | |
152 | ];\r | |
153 | }\r | |
154 | }\r | |
155 | }\r | |
156 | }\r | |
157 | return ret;\r | |
158 | },\r | |
159 | redirect: function(method, url, params) {\r | |
160 | switch (arguments.length) {\r | |
161 | case 2:\r | |
162 | if (typeof url == 'string') {\r | |
163 | break;\r | |
164 | };\r | |
165 | params = url;\r | |
166 | // fall...\r | |
167 | case 1:\r | |
168 | url = method;\r | |
169 | method = 'GET';\r | |
170 | break;\r | |
171 | }\r | |
172 | if (params) {\r | |
173 | url = Ext.urlAppend(url, Ext.Object.toQueryString(params));\r | |
174 | }\r | |
175 | return this.manager.openRequest(method, url);\r | |
176 | },\r | |
177 | removeFromData: function(ctx, records) {\r | |
178 | var me = this,\r | |
179 | data = me.getData(ctx),\r | |
180 | model = (ctx.xhr.options.proxy && ctx.xhr.options.proxy.getModel()) || {},\r | |
181 | idProperty = model.idProperty || 'id';\r | |
182 | Ext.each(records, function(record) {\r | |
183 | var id = record.get(idProperty);\r | |
184 | for (var i = data.length; i-- > 0; ) {\r | |
185 | if (data[i][idProperty] === id) {\r | |
186 | me.deleteRecord(i);\r | |
187 | break;\r | |
188 | }\r | |
189 | }\r | |
190 | });\r | |
191 | }\r | |
192 | };\r | |
193 | }());\r | |
194 | \r | |
195 | /** | |
196 | * This base class is used to handle data preparation (e.g., sorting, filtering and | |
197 | * group summary). | |
198 | */\r | |
199 | Ext.define('Ext.ux.ajax.DataSimlet', function() {\r | |
200 | function makeSortFn(def, cmp) {\r | |
201 | var order = def.direction,\r | |
202 | sign = (order && order.toUpperCase() === 'DESC') ? -1 : 1;\r | |
203 | return function(leftRec, rightRec) {\r | |
204 | var lhs = leftRec[def.property],\r | |
205 | rhs = rightRec[def.property],\r | |
206 | c = (lhs < rhs) ? -1 : ((rhs < lhs) ? 1 : 0);\r | |
207 | if (c || !cmp) {\r | |
208 | return c * sign;\r | |
209 | }\r | |
210 | return cmp(leftRec, rightRec);\r | |
211 | };\r | |
212 | }\r | |
213 | function makeSortFns(defs, cmp) {\r | |
214 | for (var sortFn = cmp,\r | |
215 | i = defs && defs.length; i; ) {\r | |
216 | sortFn = makeSortFn(defs[--i], sortFn);\r | |
217 | }\r | |
218 | return sortFn;\r | |
219 | }\r | |
220 | return {\r | |
221 | extend: 'Ext.ux.ajax.Simlet',\r | |
222 | buildNodes: function(node, path) {\r | |
223 | var me = this,\r | |
224 | nodeData = {\r | |
225 | data: []\r | |
226 | },\r | |
227 | len = node.length,\r | |
228 | children, i, child, name;\r | |
229 | me.nodes[path] = nodeData;\r | |
230 | for (i = 0; i < len; ++i) {\r | |
231 | nodeData.data.push(child = node[i]);\r | |
232 | name = child.text || child.title;\r | |
233 | child.id = path ? path + '/' + name : name;\r | |
234 | children = child.children;\r | |
235 | if (!(child.leaf = !children)) {\r | |
236 | delete child.children;\r | |
237 | me.buildNodes(children, child.id);\r | |
238 | }\r | |
239 | }\r | |
240 | },\r | |
241 | deleteRecord: function(pos) {\r | |
242 | if (this.data && typeof this.data !== 'function') {\r | |
243 | Ext.Array.removeAt(this.data, pos);\r | |
244 | }\r | |
245 | },\r | |
246 | fixTree: function(ctx, tree) {\r | |
247 | var me = this,\r | |
248 | node = ctx.params.node,\r | |
249 | nodes;\r | |
250 | if (!(nodes = me.nodes)) {\r | |
251 | me.nodes = nodes = {};\r | |
252 | me.buildNodes(tree, '');\r | |
253 | }\r | |
254 | node = nodes[node];\r | |
255 | if (node) {\r | |
256 | if (me.node) {\r | |
257 | me.node.sortedData = me.sortedData;\r | |
258 | me.node.currentOrder = me.currentOrder;\r | |
259 | }\r | |
260 | me.node = node;\r | |
261 | me.data = node.data;\r | |
262 | me.sortedData = node.sortedData;\r | |
263 | me.currentOrder = node.currentOrder;\r | |
264 | } else {\r | |
265 | me.data = null;\r | |
266 | }\r | |
267 | },\r | |
268 | getData: function(ctx) {\r | |
269 | var me = this,\r | |
270 | params = ctx.params,\r | |
271 | order = (params.filter || '') + (params.group || '') + '-' + (params.sort || '') + '-' + (params.dir || ''),\r | |
272 | tree = me.tree,\r | |
273 | dynamicData, data, fields, sortFn;\r | |
274 | if (tree) {\r | |
275 | me.fixTree(ctx, tree);\r | |
276 | }\r | |
277 | data = me.data;\r | |
278 | if (typeof data === 'function') {\r | |
279 | dynamicData = true;\r | |
280 | data = data.call(this, ctx);\r | |
281 | }\r | |
282 | // If order is '--' then it means we had no order passed, due to the string concat above\r | |
283 | if (!data || order === '--') {\r | |
284 | return data || [];\r | |
285 | }\r | |
286 | if (!dynamicData && order == me.currentOrder) {\r | |
287 | return me.sortedData;\r | |
288 | }\r | |
289 | ctx.filterSpec = params.filter && Ext.decode(params.filter);\r | |
290 | ctx.groupSpec = params.group && Ext.decode(params.group);\r | |
291 | fields = params.sort;\r | |
292 | if (params.dir) {\r | |
293 | fields = [\r | |
294 | {\r | |
295 | direction: params.dir,\r | |
296 | property: fields\r | |
297 | }\r | |
298 | ];\r | |
299 | } else {\r | |
300 | fields = Ext.decode(params.sort);\r | |
301 | }\r | |
302 | if (ctx.filterSpec) {\r | |
303 | var filters = new Ext.util.FilterCollection();\r | |
304 | filters.add(this.processFilters(ctx.filterSpec));\r | |
305 | data = Ext.Array.filter(data, filters.getFilterFn());\r | |
306 | }\r | |
307 | sortFn = makeSortFns((ctx.sortSpec = fields));\r | |
308 | if (ctx.groupSpec) {\r | |
309 | sortFn = makeSortFns([\r | |
310 | ctx.groupSpec\r | |
311 | ], sortFn);\r | |
312 | }\r | |
313 | // If a straight Ajax request, data may not be an array.\r | |
314 | // If an Array, preserve 'physical' order of raw data...\r | |
315 | data = Ext.isArray(data) ? data.slice(0) : data;\r | |
316 | if (sortFn) {\r | |
317 | Ext.Array.sort(data, sortFn);\r | |
318 | }\r | |
319 | me.sortedData = data;\r | |
320 | me.currentOrder = order;\r | |
321 | return data;\r | |
322 | },\r | |
323 | processFilters: Ext.identityFn,\r | |
324 | getPage: function(ctx, data) {\r | |
325 | var ret = data,\r | |
326 | length = data.length,\r | |
327 | start = ctx.params.start || 0,\r | |
328 | end = ctx.params.limit ? Math.min(length, start + ctx.params.limit) : length;\r | |
329 | if (start || end < length) {\r | |
330 | ret = ret.slice(start, end);\r | |
331 | }\r | |
332 | return ret;\r | |
333 | },\r | |
334 | getGroupSummary: function(groupField, rows, ctx) {\r | |
335 | return rows[0];\r | |
336 | },\r | |
337 | getSummary: function(ctx, data, page) {\r | |
338 | var me = this,\r | |
339 | groupField = ctx.groupSpec.property,\r | |
340 | accum,\r | |
341 | todo = {},\r | |
342 | summary = [],\r | |
343 | fieldValue, lastFieldValue;\r | |
344 | Ext.each(page, function(rec) {\r | |
345 | fieldValue = rec[groupField];\r | |
346 | todo[fieldValue] = true;\r | |
347 | });\r | |
348 | function flush() {\r | |
349 | if (accum) {\r | |
350 | summary.push(me.getGroupSummary(groupField, accum, ctx));\r | |
351 | accum = null;\r | |
352 | }\r | |
353 | }\r | |
354 | // data is ordered primarily by the groupField, so one pass can pick up all\r | |
355 | // the summaries one at a time.\r | |
356 | Ext.each(data, function(rec) {\r | |
357 | fieldValue = rec[groupField];\r | |
358 | if (lastFieldValue !== fieldValue) {\r | |
359 | flush();\r | |
360 | lastFieldValue = fieldValue;\r | |
361 | }\r | |
362 | if (!todo[fieldValue]) {\r | |
363 | // if we have even 1 summary, we have summarized all that we need\r | |
364 | // (again because data and page are ordered by groupField)\r | |
365 | return !summary.length;\r | |
366 | }\r | |
367 | if (accum) {\r | |
368 | accum.push(rec);\r | |
369 | } else {\r | |
370 | accum = [\r | |
371 | rec\r | |
372 | ];\r | |
373 | }\r | |
374 | return true;\r | |
375 | });\r | |
376 | flush();\r | |
377 | // make sure that last pesky summary goes...\r | |
378 | return summary;\r | |
379 | }\r | |
380 | };\r | |
381 | }());\r | |
382 | \r | |
383 | /** | |
384 | * JSON Simlet. | |
385 | */\r | |
386 | Ext.define('Ext.ux.ajax.JsonSimlet', {\r | |
387 | extend: 'Ext.ux.ajax.DataSimlet',\r | |
388 | alias: 'simlet.json',\r | |
389 | doGet: function(ctx) {\r | |
390 | var me = this,\r | |
391 | data = me.getData(ctx),\r | |
392 | page = me.getPage(ctx, data),\r | |
393 | reader = ctx.xhr.options.proxy && ctx.xhr.options.proxy.getReader(),\r | |
394 | root = reader && reader.getRootProperty(),\r | |
395 | ret = me.callParent(arguments),\r | |
396 | // pick up status/statusText\r | |
397 | response = {};\r | |
398 | if (root && Ext.isArray(page)) {\r | |
399 | response[root] = page;\r | |
400 | response[reader.getTotalProperty()] = data.length;\r | |
401 | } else {\r | |
402 | response = page;\r | |
403 | }\r | |
404 | if (ctx.groupSpec) {\r | |
405 | response.summaryData = me.getSummary(ctx, data, page);\r | |
406 | }\r | |
407 | ret.responseText = Ext.encode(response);\r | |
408 | return ret;\r | |
409 | },\r | |
410 | doPost: function(ctx) {\r | |
411 | return this.doGet(ctx);\r | |
412 | }\r | |
413 | });\r | |
414 | \r | |
415 | /** | |
416 | * Simulates an XMLHttpRequest object's methods and properties but is backed by a | |
417 | * {@link Ext.ux.ajax.Simlet} instance that provides the data. | |
418 | */\r | |
419 | Ext.define('Ext.ux.ajax.SimXhr', {\r | |
420 | readyState: 0,\r | |
421 | mgr: null,\r | |
422 | simlet: null,\r | |
423 | constructor: function(config) {\r | |
424 | var me = this;\r | |
425 | Ext.apply(me, config);\r | |
426 | me.requestHeaders = {};\r | |
427 | },\r | |
428 | abort: function() {\r | |
429 | var me = this;\r | |
430 | if (me.timer) {\r | |
431 | clearTimeout(me.timer);\r | |
432 | me.timer = null;\r | |
433 | }\r | |
434 | me.aborted = true;\r | |
435 | },\r | |
436 | getAllResponseHeaders: function() {\r | |
437 | var headers = [];\r | |
438 | if (Ext.isObject(this.responseHeaders)) {\r | |
439 | Ext.Object.each(this.responseHeaders, function(name, value) {\r | |
440 | headers.push(name + ': ' + value);\r | |
441 | });\r | |
442 | }\r | |
443 | return headers.join('\r\n');\r | |
444 | },\r | |
445 | getResponseHeader: function(header) {\r | |
446 | var headers = this.responseHeaders;\r | |
447 | return (headers && headers[header]) || null;\r | |
448 | },\r | |
449 | open: function(method, url, async, user, password) {\r | |
450 | var me = this;\r | |
451 | me.method = method;\r | |
452 | me.url = url;\r | |
453 | me.async = async !== false;\r | |
454 | me.user = user;\r | |
455 | me.password = password;\r | |
456 | me.setReadyState(1);\r | |
457 | },\r | |
458 | overrideMimeType: function(mimeType) {\r | |
459 | this.mimeType = mimeType;\r | |
460 | },\r | |
461 | schedule: function() {\r | |
462 | var me = this,\r | |
463 | delay = me.mgr.delay;\r | |
464 | if (delay) {\r | |
465 | me.timer = setTimeout(function() {\r | |
466 | me.onTick();\r | |
467 | }, delay);\r | |
468 | } else {\r | |
469 | me.onTick();\r | |
470 | }\r | |
471 | },\r | |
472 | send: function(body) {\r | |
473 | var me = this;\r | |
474 | me.body = body;\r | |
475 | if (me.async) {\r | |
476 | me.schedule();\r | |
477 | } else {\r | |
478 | me.onComplete();\r | |
479 | }\r | |
480 | },\r | |
481 | setReadyState: function(state) {\r | |
482 | var me = this;\r | |
483 | if (me.readyState != state) {\r | |
484 | me.readyState = state;\r | |
485 | me.onreadystatechange();\r | |
486 | }\r | |
487 | },\r | |
488 | setRequestHeader: function(header, value) {\r | |
489 | this.requestHeaders[header] = value;\r | |
490 | },\r | |
491 | // handlers\r | |
492 | onreadystatechange: Ext.emptyFn,\r | |
493 | onComplete: function() {\r | |
494 | var me = this,\r | |
495 | callback;\r | |
496 | me.readyState = 4;\r | |
497 | Ext.apply(me, me.simlet.exec(me));\r | |
498 | callback = me.jsonpCallback;\r | |
499 | if (callback) {\r | |
500 | var text = callback + '(' + me.responseText + ')';\r | |
501 | eval(text);\r | |
502 | }\r | |
503 | },\r | |
504 | onTick: function() {\r | |
505 | var me = this;\r | |
506 | me.timer = null;\r | |
507 | me.onComplete();\r | |
508 | me.onreadystatechange && me.onreadystatechange();\r | |
509 | }\r | |
510 | });\r | |
511 | \r | |
512 | /** | |
513 | * This singleton manages simulated Ajax responses. This allows application logic to be | |
514 | * written unaware that its Ajax calls are being handled by simulations ("simlets"). This | |
515 | * is currently done by hooking {@link Ext.data.Connection} methods, so all users of that | |
516 | * class (and {@link Ext.Ajax} since it is a derived class) qualify for simulation. | |
517 | * | |
518 | * The requires hooks are inserted when either the {@link #init} method is called or the | |
519 | * first {@link Ext.ux.ajax.Simlet} is registered. For example: | |
520 | * | |
521 | * Ext.onReady(function () { | |
522 | * initAjaxSim(); | |
523 | * | |
524 | * // normal stuff | |
525 | * }); | |
526 | * | |
527 | * function initAjaxSim () { | |
528 | * Ext.ux.ajax.SimManager.init({ | |
529 | * delay: 300 | |
530 | * }).register({ | |
531 | * '/app/data/url': { | |
532 | * type: 'json', // use JsonSimlet (type is like xtype for components) | |
533 | * data: [ | |
534 | * { foo: 42, bar: 'abc' }, | |
535 | * ... | |
536 | * ] | |
537 | * } | |
538 | * }); | |
539 | * } | |
540 | * | |
541 | * As many URL's as desired can be registered and associated with a {@link Ext.ux.ajax.Simlet}. To make | |
542 | * non-simulated Ajax requests once this singleton is initialized, add a `nosim:true` option | |
543 | * to the Ajax options: | |
544 | * | |
545 | * Ext.Ajax.request({ | |
546 | * url: 'page.php', | |
547 | * nosim: true, // ignored by normal Ajax request | |
548 | * params: { | |
549 | * id: 1 | |
550 | * }, | |
551 | * success: function(response){ | |
552 | * var text = response.responseText; | |
553 | * // process server response here | |
554 | * } | |
555 | * }); | |
556 | */\r | |
557 | Ext.define('Ext.ux.ajax.SimManager', {\r | |
558 | singleton: true,\r | |
559 | requires: [\r | |
560 | 'Ext.data.Connection',\r | |
561 | 'Ext.ux.ajax.SimXhr',\r | |
562 | 'Ext.ux.ajax.Simlet',\r | |
563 | 'Ext.ux.ajax.JsonSimlet'\r | |
564 | ],\r | |
565 | /** | |
566 | * @cfg {Ext.ux.ajax.Simlet} defaultSimlet | |
567 | * The {@link Ext.ux.ajax.Simlet} instance to use for non-matching URL's. By default, this will | |
568 | * return 404. Set this to null to use real Ajax calls for non-matching URL's. | |
569 | */\r | |
570 | /** | |
571 | * @cfg {String} defaultType | |
572 | * The default `type` to apply to generic {@link Ext.ux.ajax.Simlet} configuration objects. The | |
573 | * default is 'basic'. | |
574 | */\r | |
575 | defaultType: 'basic',\r | |
576 | /** | |
577 | * @cfg {Number} delay | |
578 | * The number of milliseconds to delay before delivering a response to an async request. | |
579 | */\r | |
580 | delay: 150,\r | |
581 | /** | |
582 | * @property {Boolean} ready | |
583 | * True once this singleton has initialized and applied its Ajax hooks. | |
584 | * @private | |
585 | */\r | |
586 | ready: false,\r | |
587 | constructor: function() {\r | |
588 | this.simlets = [];\r | |
589 | },\r | |
590 | getSimlet: function(url) {\r | |
591 | // Strip down to base URL (no query parameters or hash):\r | |
592 | var me = this,\r | |
593 | index = url.indexOf('?'),\r | |
594 | simlets = me.simlets,\r | |
595 | len = simlets.length,\r | |
596 | i, simlet, simUrl, match;\r | |
597 | if (index < 0) {\r | |
598 | index = url.indexOf('#');\r | |
599 | }\r | |
600 | if (index > 0) {\r | |
601 | url = url.substring(0, index);\r | |
602 | }\r | |
603 | for (i = 0; i < len; ++i) {\r | |
604 | simlet = simlets[i];\r | |
605 | simUrl = simlet.url;\r | |
606 | if (simUrl instanceof RegExp) {\r | |
607 | match = simUrl.test(url);\r | |
608 | } else {\r | |
609 | match = simUrl === url;\r | |
610 | }\r | |
611 | if (match) {\r | |
612 | return simlet;\r | |
613 | }\r | |
614 | }\r | |
615 | return me.defaultSimlet;\r | |
616 | },\r | |
617 | getXhr: function(method, url, options, async) {\r | |
618 | var simlet = this.getSimlet(url);\r | |
619 | if (simlet) {\r | |
620 | return simlet.openRequest(method, url, options, async);\r | |
621 | }\r | |
622 | return null;\r | |
623 | },\r | |
624 | /** | |
625 | * Initializes this singleton and applies configuration options. | |
626 | * @param {Object} config An optional object with configuration properties to apply. | |
627 | * @return {Ext.ux.ajax.SimManager} this | |
628 | */\r | |
629 | init: function(config) {\r | |
630 | var me = this;\r | |
631 | Ext.apply(me, config);\r | |
632 | if (!me.ready) {\r | |
633 | me.ready = true;\r | |
634 | if (!('defaultSimlet' in me)) {\r | |
635 | me.defaultSimlet = new Ext.ux.ajax.Simlet({\r | |
636 | status: 404,\r | |
637 | statusText: 'Not Found'\r | |
638 | });\r | |
639 | }\r | |
640 | me._openRequest = Ext.data.Connection.prototype.openRequest;\r | |
641 | Ext.data.request.Ajax.override({\r | |
642 | openRequest: function(options, requestOptions, async) {\r | |
643 | var xhr = !options.nosim && me.getXhr(requestOptions.method, requestOptions.url, options, async);\r | |
644 | if (!xhr) {\r | |
645 | xhr = this.callParent(arguments);\r | |
646 | }\r | |
647 | return xhr;\r | |
648 | }\r | |
649 | });\r | |
650 | if (Ext.data.JsonP) {\r | |
651 | Ext.data.JsonP.self.override({\r | |
652 | createScript: function(url, params, options) {\r | |
653 | var fullUrl = Ext.urlAppend(url, Ext.Object.toQueryString(params)),\r | |
654 | script = !options.nosim && me.getXhr('GET', fullUrl, options, true);\r | |
655 | if (!script) {\r | |
656 | script = this.callParent(arguments);\r | |
657 | }\r | |
658 | return script;\r | |
659 | },\r | |
660 | loadScript: function(request) {\r | |
661 | var script = request.script;\r | |
662 | if (script.simlet) {\r | |
663 | script.jsonpCallback = request.params[request.callbackKey];\r | |
664 | script.send(null);\r | |
665 | // Ext.data.JsonP will attempt dom removal of a script tag, so emulate its presence\r | |
666 | request.script = document.createElement('script');\r | |
667 | } else {\r | |
668 | this.callParent(arguments);\r | |
669 | }\r | |
670 | }\r | |
671 | });\r | |
672 | }\r | |
673 | }\r | |
674 | return me;\r | |
675 | },\r | |
676 | openRequest: function(method, url, async) {\r | |
677 | var opt = {\r | |
678 | method: method,\r | |
679 | url: url\r | |
680 | };\r | |
681 | return this._openRequest.call(Ext.data.Connection.prototype, {}, opt, async);\r | |
682 | },\r | |
683 | /** | |
684 | * Registeres one or more {@link Ext.ux.ajax.Simlet} instances. | |
685 | * @param {Array/Object} simlet Either a {@link Ext.ux.ajax.Simlet} instance or config, an Array | |
686 | * of such elements or an Object keyed by URL with values that are {@link Ext.ux.ajax.Simlet} | |
687 | * instances or configs. | |
688 | */\r | |
689 | register: function(simlet) {\r | |
690 | var me = this;\r | |
691 | me.init();\r | |
692 | function reg(one) {\r | |
693 | var simlet = one;\r | |
694 | if (!simlet.isSimlet) {\r | |
695 | simlet = Ext.create('simlet.' + (simlet.type || simlet.stype || me.defaultType), one);\r | |
696 | }\r | |
697 | me.simlets.push(simlet);\r | |
698 | simlet.manager = me;\r | |
699 | }\r | |
700 | if (Ext.isArray(simlet)) {\r | |
701 | Ext.each(simlet, reg);\r | |
702 | } else if (simlet.isSimlet || simlet.url) {\r | |
703 | reg(simlet);\r | |
704 | } else {\r | |
705 | Ext.Object.each(simlet, function(url, s) {\r | |
706 | s.url = url;\r | |
707 | reg(s);\r | |
708 | });\r | |
709 | }\r | |
710 | return me;\r | |
711 | }\r | |
712 | });\r | |
713 | \r | |
714 | /** | |
715 | * This class simulates XML-based requests. | |
716 | */\r | |
717 | Ext.define('Ext.ux.ajax.XmlSimlet', {\r | |
718 | extend: 'Ext.ux.ajax.DataSimlet',\r | |
719 | alias: 'simlet.xml',\r | |
720 | /** | |
721 | * This template is used to populate the XML response. The configuration of the Reader | |
722 | * is available so that its `root` and `record` properties can be used as well as the | |
723 | * `fields` of the associated `model`. But beyond that, the way these pieces are put | |
724 | * together in the document requires the flexibility of a template. | |
725 | */\r | |
726 | xmlTpl: [\r | |
727 | '<{root}>\n',\r | |
728 | '<tpl for="data">',\r | |
729 | ' <{parent.record}>\n',\r | |
730 | '<tpl for="parent.fields">',\r | |
731 | ' <{name}>{[parent[values.name]]}</{name}>\n',\r | |
732 | '</tpl>',\r | |
733 | ' </{parent.record}>\n',\r | |
734 | '</tpl>',\r | |
735 | '</{root}>'\r | |
736 | ],\r | |
737 | doGet: function(ctx) {\r | |
738 | var me = this,\r | |
739 | data = me.getData(ctx),\r | |
740 | page = me.getPage(ctx, data),\r | |
741 | proxy = ctx.xhr.options.operation.getProxy(),\r | |
742 | reader = proxy && proxy.getReader(),\r | |
743 | model = reader && reader.getModel(),\r | |
744 | ret = me.callParent(arguments),\r | |
745 | // pick up status/statusText\r | |
746 | response = {\r | |
747 | data: page,\r | |
748 | reader: reader,\r | |
749 | fields: model && model.fields,\r | |
750 | root: reader && reader.getRootProperty(),\r | |
751 | record: reader && reader.record\r | |
752 | },\r | |
753 | tpl, xml, doc;\r | |
754 | if (ctx.groupSpec) {\r | |
755 | response.summaryData = me.getSummary(ctx, data, page);\r | |
756 | }\r | |
757 | // If a straight Ajax request there won't be an xmlTpl.\r | |
758 | if (me.xmlTpl) {\r | |
759 | tpl = Ext.XTemplate.getTpl(me, 'xmlTpl');\r | |
760 | xml = tpl.apply(response);\r | |
761 | } else {\r | |
762 | xml = data;\r | |
763 | }\r | |
764 | if (typeof DOMParser != 'undefined') {\r | |
765 | doc = (new DOMParser()).parseFromString(xml, "text/xml");\r | |
766 | } else {\r | |
767 | // IE doesn't have DOMParser, but fortunately, there is an ActiveX for XML\r | |
768 | doc = new ActiveXObject("Microsoft.XMLDOM");\r | |
769 | doc.async = false;\r | |
770 | doc.loadXML(xml);\r | |
771 | }\r | |
772 | ret.responseText = xml;\r | |
773 | ret.responseXML = doc;\r | |
774 | return ret;\r | |
775 | },\r | |
776 | fixTree: function() {\r | |
777 | this.callParent(arguments);\r | |
778 | var buffer = [];\r | |
779 | this.buildTreeXml(this.data, buffer);\r | |
780 | this.data = buffer.join('');\r | |
781 | },\r | |
782 | buildTreeXml: function(nodes, buffer) {\r | |
783 | var rootProperty = this.rootProperty,\r | |
784 | recordProperty = this.recordProperty;\r | |
785 | buffer.push('<', rootProperty, '>');\r | |
786 | Ext.Array.forEach(nodes, function(node) {\r | |
787 | buffer.push('<', recordProperty, '>');\r | |
788 | for (var key in node) {\r | |
789 | if (key == 'children') {\r | |
790 | this.buildTreeXml(node.children, buffer);\r | |
791 | } else {\r | |
792 | buffer.push('<', key, '>', node[key], '</', key, '>');\r | |
793 | }\r | |
794 | }\r | |
795 | buffer.push('</', recordProperty, '>');\r | |
796 | });\r | |
797 | buffer.push('</', rootProperty, '>');\r | |
798 | }\r | |
799 | });\r | |
800 | \r | |
801 | /** | |
802 | * This is the base class for {@link Ext.ux.event.Recorder} and {@link Ext.ux.event.Player}. | |
803 | */\r | |
804 | Ext.define('Ext.ux.event.Driver', {\r | |
805 | extend: 'Ext.util.Observable',\r | |
806 | active: null,\r | |
807 | specialKeysByName: {\r | |
808 | PGUP: 33,\r | |
809 | PGDN: 34,\r | |
810 | END: 35,\r | |
811 | HOME: 36,\r | |
812 | LEFT: 37,\r | |
813 | UP: 38,\r | |
814 | RIGHT: 39,\r | |
815 | DOWN: 40\r | |
816 | },\r | |
817 | specialKeysByCode: {},\r | |
818 | /** | |
819 | * @event start | |
820 | * Fires when this object is started. | |
821 | * @param {Ext.ux.event.Driver} this | |
822 | */\r | |
823 | /** | |
824 | * @event stop | |
825 | * Fires when this object is stopped. | |
826 | * @param {Ext.ux.event.Driver} this | |
827 | */\r | |
828 | getTextSelection: function(el) {\r | |
829 | // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js\r | |
830 | var doc = el.ownerDocument,\r | |
831 | range, range2, start, end;\r | |
832 | if (typeof el.selectionStart === "number") {\r | |
833 | start = el.selectionStart;\r | |
834 | end = el.selectionEnd;\r | |
835 | } else if (doc.selection) {\r | |
836 | range = doc.selection.createRange();\r | |
837 | range2 = el.createTextRange();\r | |
838 | range2.setEndPoint('EndToStart', range);\r | |
839 | start = range2.text.length;\r | |
840 | end = start + range.text.length;\r | |
841 | }\r | |
842 | return [\r | |
843 | start,\r | |
844 | end\r | |
845 | ];\r | |
846 | },\r | |
847 | getTime: function() {\r | |
848 | return new Date().getTime();\r | |
849 | },\r | |
850 | /** | |
851 | * Returns the number of milliseconds since start was called. | |
852 | */\r | |
853 | getTimestamp: function() {\r | |
854 | var d = this.getTime();\r | |
855 | return d - this.startTime;\r | |
856 | },\r | |
857 | onStart: function() {},\r | |
858 | onStop: function() {},\r | |
859 | /** | |
860 | * Starts this object. If this object is already started, nothing happens. | |
861 | */\r | |
862 | start: function() {\r | |
863 | var me = this;\r | |
864 | if (!me.active) {\r | |
865 | me.active = new Date();\r | |
866 | me.startTime = me.getTime();\r | |
867 | me.onStart();\r | |
868 | me.fireEvent('start', me);\r | |
869 | }\r | |
870 | },\r | |
871 | /** | |
872 | * Stops this object. If this object is not started, nothing happens. | |
873 | */\r | |
874 | stop: function() {\r | |
875 | var me = this;\r | |
876 | if (me.active) {\r | |
877 | me.active = null;\r | |
878 | me.onStop();\r | |
879 | me.fireEvent('stop', me);\r | |
880 | }\r | |
881 | }\r | |
882 | }, function() {\r | |
883 | var proto = this.prototype;\r | |
884 | Ext.Object.each(proto.specialKeysByName, function(name, value) {\r | |
885 | proto.specialKeysByCode[value] = name;\r | |
886 | });\r | |
887 | });\r | |
888 | \r | |
889 | /** | |
890 | * Event maker. | |
891 | */\r | |
892 | Ext.define('Ext.ux.event.Maker', {\r | |
893 | eventQueue: [],\r | |
894 | startAfter: 500,\r | |
895 | timerIncrement: 500,\r | |
896 | currentTiming: 0,\r | |
897 | constructor: function(config) {\r | |
898 | var me = this;\r | |
899 | me.currentTiming = me.startAfter;\r | |
900 | if (!Ext.isArray(config)) {\r | |
901 | config = [\r | |
902 | config\r | |
903 | ];\r | |
904 | }\r | |
905 | Ext.Array.each(config, function(item) {\r | |
906 | item.el = item.el || 'el';\r | |
907 | Ext.Array.each(Ext.ComponentQuery.query(item.cmpQuery), function(cmp) {\r | |
908 | var event = {},\r | |
909 | x, y, el;\r | |
910 | if (!item.domQuery) {\r | |
911 | el = cmp[item.el];\r | |
912 | } else {\r | |
913 | el = cmp.el.down(item.domQuery);\r | |
914 | }\r | |
915 | event.target = '#' + el.dom.id;\r | |
916 | event.type = item.type;\r | |
917 | event.button = config.button || 0;\r | |
918 | x = el.getX() + (el.getWidth() / 2);\r | |
919 | y = el.getY() + (el.getHeight() / 2);\r | |
920 | event.xy = [\r | |
921 | x,\r | |
922 | y\r | |
923 | ];\r | |
924 | event.ts = me.currentTiming;\r | |
925 | me.currentTiming += me.timerIncrement;\r | |
926 | me.eventQueue.push(event);\r | |
927 | });\r | |
928 | if (item.screenshot) {\r | |
929 | me.eventQueue[me.eventQueue.length - 1].screenshot = true;\r | |
930 | }\r | |
931 | });\r | |
932 | return me.eventQueue;\r | |
933 | }\r | |
934 | });\r | |
935 | \r | |
936 | /** | |
937 | * @extends Ext.ux.event.Driver | |
938 | * This class manages the playback of an array of "event descriptors". For details on the | |
939 | * contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the | |
940 | * {@link Ext.ux.event.Recorder} class are designed to serve as input for this class. | |
941 | * | |
942 | * The simplest use of this class is to instantiate it with an {@link #eventQueue} and call | |
943 | * {@link #method-start}. Like so: | |
944 | * | |
945 | * var player = Ext.create('Ext.ux.event.Player', { | |
946 | * eventQueue: [ ... ], | |
947 | * speed: 2, // play at 2x speed | |
948 | * listeners: { | |
949 | * stop: function () { | |
950 | * player = null; // all done | |
951 | * } | |
952 | * } | |
953 | * }); | |
954 | * | |
955 | * player.start(); | |
956 | * | |
957 | * A more complex use would be to incorporate keyframe generation after playing certain | |
958 | * events. | |
959 | * | |
960 | * var player = Ext.create('Ext.ux.event.Player', { | |
961 | * eventQueue: [ ... ], | |
962 | * keyFrameEvents: { | |
963 | * click: true | |
964 | * }, | |
965 | * listeners: { | |
966 | * stop: function () { | |
967 | * // play has completed... probably time for another keyframe... | |
968 | * player = null; | |
969 | * }, | |
970 | * keyframe: onKeyFrame | |
971 | * } | |
972 | * }); | |
973 | * | |
974 | * player.start(); | |
975 | * | |
976 | * If a keyframe can be handled immediately (synchronously), the listener would be: | |
977 | * | |
978 | * function onKeyFrame () { | |
979 | * handleKeyFrame(); | |
980 | * } | |
981 | * | |
982 | * If the keyframe event is always handled asynchronously, then the event listener is only | |
983 | * a bit more: | |
984 | * | |
985 | * function onKeyFrame (p, eventDescriptor) { | |
986 | * eventDescriptor.defer(); // pause event playback... | |
987 | * | |
988 | * handleKeyFrame(function () { | |
989 | * eventDescriptor.finish(); // ...resume event playback | |
990 | * }); | |
991 | * } | |
992 | * | |
993 | * Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps | |
994 | * differently by browser), a slightly more complex listener is required. | |
995 | * | |
996 | * function onKeyFrame (p, eventDescriptor) { | |
997 | * var async; | |
998 | * | |
999 | * handleKeyFrame(function () { | |
1000 | * // either this callback is being called immediately by handleKeyFrame (in | |
1001 | * // which case async is undefined) or it is being called later (in which case | |
1002 | * // async will be true). | |
1003 | * | |
1004 | * if (async) { | |
1005 | * eventDescriptor.finish(); | |
1006 | * } else { | |
1007 | * async = false; | |
1008 | * } | |
1009 | * }); | |
1010 | * | |
1011 | * // either the callback was called (and async is now false) or it was not | |
1012 | * // called (and async remains undefined). | |
1013 | * | |
1014 | * if (async !== false) { | |
1015 | * eventDescriptor.defer(); | |
1016 | * async = true; // let the callback know that we have gone async | |
1017 | * } | |
1018 | * } | |
1019 | */\r | |
1020 | Ext.define('Ext.ux.event.Player', function(Player) {\r | |
1021 | var defaults = {},\r | |
1022 | mouseEvents = {},\r | |
1023 | keyEvents = {},\r | |
1024 | doc,\r | |
1025 | //HTML events supported\r | |
1026 | uiEvents = {},\r | |
1027 | //events that bubble by default\r | |
1028 | bubbleEvents = {\r | |
1029 | //scroll: 1,\r | |
1030 | resize: 1,\r | |
1031 | reset: 1,\r | |
1032 | submit: 1,\r | |
1033 | change: 1,\r | |
1034 | select: 1,\r | |
1035 | error: 1,\r | |
1036 | abort: 1\r | |
1037 | };\r | |
1038 | Ext.each([\r | |
1039 | 'click',\r | |
1040 | 'dblclick',\r | |
1041 | 'mouseover',\r | |
1042 | 'mouseout',\r | |
1043 | 'mousedown',\r | |
1044 | 'mouseup',\r | |
1045 | 'mousemove'\r | |
1046 | ], function(type) {\r | |
1047 | bubbleEvents[type] = defaults[type] = mouseEvents[type] = {\r | |
1048 | bubbles: true,\r | |
1049 | cancelable: (type != "mousemove"),\r | |
1050 | // mousemove cannot be cancelled\r | |
1051 | detail: 1,\r | |
1052 | screenX: 0,\r | |
1053 | screenY: 0,\r | |
1054 | clientX: 0,\r | |
1055 | clientY: 0,\r | |
1056 | ctrlKey: false,\r | |
1057 | altKey: false,\r | |
1058 | shiftKey: false,\r | |
1059 | metaKey: false,\r | |
1060 | button: 0\r | |
1061 | };\r | |
1062 | });\r | |
1063 | Ext.each([\r | |
1064 | 'keydown',\r | |
1065 | 'keyup',\r | |
1066 | 'keypress'\r | |
1067 | ], function(type) {\r | |
1068 | bubbleEvents[type] = defaults[type] = keyEvents[type] = {\r | |
1069 | bubbles: true,\r | |
1070 | cancelable: true,\r | |
1071 | ctrlKey: false,\r | |
1072 | altKey: false,\r | |
1073 | shiftKey: false,\r | |
1074 | metaKey: false,\r | |
1075 | keyCode: 0,\r | |
1076 | charCode: 0\r | |
1077 | };\r | |
1078 | });\r | |
1079 | Ext.each([\r | |
1080 | 'blur',\r | |
1081 | 'change',\r | |
1082 | 'focus',\r | |
1083 | 'resize',\r | |
1084 | 'scroll',\r | |
1085 | 'select'\r | |
1086 | ], function(type) {\r | |
1087 | defaults[type] = uiEvents[type] = {\r | |
1088 | bubbles: (type in bubbleEvents),\r | |
1089 | cancelable: false,\r | |
1090 | detail: 1\r | |
1091 | };\r | |
1092 | });\r | |
1093 | var inputSpecialKeys = {\r | |
1094 | 8: function(target, start, end) {\r | |
1095 | // backspace: 8,\r | |
1096 | if (start < end) {\r | |
1097 | target.value = target.value.substring(0, start) + target.value.substring(end);\r | |
1098 | } else if (start > 0) {\r | |
1099 | target.value = target.value.substring(0, --start) + target.value.substring(end);\r | |
1100 | }\r | |
1101 | this.setTextSelection(target, start, start);\r | |
1102 | },\r | |
1103 | 46: function(target, start, end) {\r | |
1104 | // delete: 46\r | |
1105 | if (start < end) {\r | |
1106 | target.value = target.value.substring(0, start) + target.value.substring(end);\r | |
1107 | } else if (start < target.value.length - 1) {\r | |
1108 | target.value = target.value.substring(0, start) + target.value.substring(start + 1);\r | |
1109 | }\r | |
1110 | this.setTextSelection(target, start, start);\r | |
1111 | }\r | |
1112 | };\r | |
1113 | return {\r | |
1114 | extend: 'Ext.ux.event.Driver',\r | |
1115 | /** | |
1116 | * @cfg {Array} eventQueue The event queue to playback. This must be provided before | |
1117 | * the {@link #method-start} method is called. | |
1118 | */\r | |
1119 | /** | |
1120 | * @cfg {Object} keyFrameEvents An object that describes the events that should generate | |
1121 | * keyframe events. For example, `{ click: true }` would generate keyframe events after | |
1122 | * each `click` event. | |
1123 | */\r | |
1124 | keyFrameEvents: {\r | |
1125 | click: true\r | |
1126 | },\r | |
1127 | /** | |
1128 | * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false | |
1129 | * to ignore animations. Default is true. | |
1130 | */\r | |
1131 | pauseForAnimations: true,\r | |
1132 | /** | |
1133 | * @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the | |
1134 | * recorded speed). A value of 2 would playback at 2x speed. | |
1135 | */\r | |
1136 | speed: 1,\r | |
1137 | stallTime: 0,\r | |
1138 | _inputSpecialKeys: {\r | |
1139 | INPUT: inputSpecialKeys,\r | |
1140 | TEXTAREA: Ext.apply({}, //13: function (target, start, end) { // enter: 8,\r | |
1141 | //TODO ?\r | |
1142 | //}\r | |
1143 | inputSpecialKeys)\r | |
1144 | },\r | |
1145 | tagPathRegEx: /(\w+)(?:\[(\d+)\])?/,\r | |
1146 | /** | |
1147 | * @event beforeplay | |
1148 | * Fires before an event is played. | |
1149 | * @param {Ext.ux.event.Player} this | |
1150 | * @param {Object} eventDescriptor The event descriptor about to be played. | |
1151 | */\r | |
1152 | /** | |
1153 | * @event keyframe | |
1154 | * Fires when this player reaches a keyframe. Typically, this is after events | |
1155 | * like `click` are injected and any resulting animations have been completed. | |
1156 | * @param {Ext.ux.event.Player} this | |
1157 | * @param {Object} eventDescriptor The keyframe event descriptor. | |
1158 | */\r | |
1159 | constructor: function(config) {\r | |
1160 | var me = this;\r | |
1161 | me.callParent(arguments);\r | |
1162 | me.timerFn = function() {\r | |
1163 | me.onTick();\r | |
1164 | };\r | |
1165 | me.attachTo = me.attachTo || window;\r | |
1166 | doc = me.attachTo.document;\r | |
1167 | },\r | |
1168 | /** | |
1169 | * Returns the element given is XPath-like description. | |
1170 | * @param {String} xpath The XPath-like description of the element. | |
1171 | * @return {HTMLElement} | |
1172 | */\r | |
1173 | getElementFromXPath: function(xpath) {\r | |
1174 | var me = this,\r | |
1175 | parts = xpath.split('/'),\r | |
1176 | regex = me.tagPathRegEx,\r | |
1177 | i, n, m, count, tag, child,\r | |
1178 | el = me.attachTo.document;\r | |
1179 | el = (parts[0] == '~') ? el.body : el.getElementById(parts[0].substring(1));\r | |
1180 | // remove '#'\r | |
1181 | for (i = 1 , n = parts.length; el && i < n; ++i) {\r | |
1182 | m = regex.exec(parts[i]);\r | |
1183 | count = m[2] ? parseInt(m[2], 10) : 1;\r | |
1184 | tag = m[1].toUpperCase();\r | |
1185 | for (child = el.firstChild; child; child = child.nextSibling) {\r | |
1186 | if (child.tagName == tag) {\r | |
1187 | if (count == 1) {\r | |
1188 | break;\r | |
1189 | }\r | |
1190 | --count;\r | |
1191 | }\r | |
1192 | }\r | |
1193 | el = child;\r | |
1194 | }\r | |
1195 | return el;\r | |
1196 | },\r | |
1197 | // Moving across a line break only counts as moving one character in a TextRange, whereas a line break in\r | |
1198 | // the textarea value is two characters. This function corrects for that by converting a text offset into a\r | |
1199 | // range character offset by subtracting one character for every line break in the textarea prior to the\r | |
1200 | // offset\r | |
1201 | offsetToRangeCharacterMove: function(el, offset) {\r | |
1202 | return offset - (el.value.slice(0, offset).split("\r\n").length - 1);\r | |
1203 | },\r | |
1204 | setTextSelection: function(el, startOffset, endOffset) {\r | |
1205 | // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js\r | |
1206 | if (startOffset < 0) {\r | |
1207 | startOffset += el.value.length;\r | |
1208 | }\r | |
1209 | if (endOffset == null) {\r | |
1210 | endOffset = startOffset;\r | |
1211 | }\r | |
1212 | if (endOffset < 0) {\r | |
1213 | endOffset += el.value.length;\r | |
1214 | }\r | |
1215 | if (typeof el.selectionStart === "number") {\r | |
1216 | el.selectionStart = startOffset;\r | |
1217 | el.selectionEnd = endOffset;\r | |
1218 | } else {\r | |
1219 | var range = el.createTextRange();\r | |
1220 | var startCharMove = this.offsetToRangeCharacterMove(el, startOffset);\r | |
1221 | range.collapse(true);\r | |
1222 | if (startOffset == endOffset) {\r | |
1223 | range.move("character", startCharMove);\r | |
1224 | } else {\r | |
1225 | range.moveEnd("character", this.offsetToRangeCharacterMove(el, endOffset));\r | |
1226 | range.moveStart("character", startCharMove);\r | |
1227 | }\r | |
1228 | range.select();\r | |
1229 | }\r | |
1230 | },\r | |
1231 | getTimeIndex: function() {\r | |
1232 | var t = this.getTimestamp() - this.stallTime;\r | |
1233 | return t * this.speed;\r | |
1234 | },\r | |
1235 | makeToken: function(eventDescriptor, signal) {\r | |
1236 | var me = this,\r | |
1237 | t0;\r | |
1238 | eventDescriptor[signal] = true;\r | |
1239 | eventDescriptor.defer = function() {\r | |
1240 | eventDescriptor[signal] = false;\r | |
1241 | t0 = me.getTime();\r | |
1242 | };\r | |
1243 | eventDescriptor.finish = function() {\r | |
1244 | eventDescriptor[signal] = true;\r | |
1245 | me.stallTime += me.getTime() - t0;\r | |
1246 | me.schedule();\r | |
1247 | };\r | |
1248 | },\r | |
1249 | /** | |
1250 | * This method is called after an event has been played to prepare for the next event. | |
1251 | * @param {Object} eventDescriptor The descriptor of the event just played. | |
1252 | */\r | |
1253 | nextEvent: function(eventDescriptor) {\r | |
1254 | var me = this,\r | |
1255 | index = ++me.queueIndex;\r | |
1256 | // keyframe events are inserted after a keyFrameEvent is played.\r | |
1257 | if (me.keyFrameEvents[eventDescriptor.type]) {\r | |
1258 | Ext.Array.insert(me.eventQueue, index, [\r | |
1259 | {\r | |
1260 | keyframe: true,\r | |
1261 | ts: eventDescriptor.ts\r | |
1262 | }\r | |
1263 | ]);\r | |
1264 | }\r | |
1265 | },\r | |
1266 | /** | |
1267 | * This method returns the event descriptor at the front of the queue. This does not | |
1268 | * dequeue the event. Repeated calls return the same object (until {@link #nextEvent} | |
1269 | * is called). | |
1270 | */\r | |
1271 | peekEvent: function() {\r | |
1272 | return this.eventQueue[this.queueIndex] || null;\r | |
1273 | },\r | |
1274 | /** | |
1275 | * Replaces an event in the queue with an array of events. This is often used to roll | |
1276 | * up a multi-step pseudo-event and expand it just-in-time to be played. The process | |
1277 | * for doing this in a derived class would be this: | |
1278 | * | |
1279 | * Ext.define('My.Player', { | |
1280 | * extend: 'Ext.ux.event.Player', | |
1281 | * | |
1282 | * peekEvent: function () { | |
1283 | * var event = this.callParent(); | |
1284 | * | |
1285 | * if (event.multiStepSpecial) { | |
1286 | * this.replaceEvent(null, [ | |
1287 | * ... expand to actual events | |
1288 | * ]); | |
1289 | * | |
1290 | * event = this.callParent(); // get the new next event | |
1291 | * } | |
1292 | * | |
1293 | * return event; | |
1294 | * } | |
1295 | * }); | |
1296 | * | |
1297 | * This method ensures that the `beforeplay` hook (if any) from the replaced event is | |
1298 | * placed on the first new event and the `afterplay` hook (if any) is placed on the | |
1299 | * last new event. | |
1300 | * | |
1301 | * @param {Number} index The queue index to replace. Pass `null` to replace the event | |
1302 | * at the current `queueIndex`. | |
1303 | * @param {Event[]} events The array of events with which to replace the specified | |
1304 | * event. | |
1305 | */\r | |
1306 | replaceEvent: function(index, events) {\r | |
1307 | for (var t,\r | |
1308 | i = 0,\r | |
1309 | n = events.length; i < n; ++i) {\r | |
1310 | if (i) {\r | |
1311 | t = events[i - 1];\r | |
1312 | delete t.afterplay;\r | |
1313 | delete t.screenshot;\r | |
1314 | delete events[i].beforeplay;\r | |
1315 | }\r | |
1316 | }\r | |
1317 | Ext.Array.replace(this.eventQueue, (index == null) ? this.queueIndex : index, 1, events);\r | |
1318 | },\r | |
1319 | /** | |
1320 | * This method dequeues and injects events until it has arrived at the time index. If | |
1321 | * no events are ready (based on the time index), this method does nothing. | |
1322 | * @return {Boolean} True if there is more to do; false if not (at least for now). | |
1323 | */\r | |
1324 | processEvents: function() {\r | |
1325 | var me = this,\r | |
1326 | animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items,\r | |
1327 | eventDescriptor;\r | |
1328 | while ((eventDescriptor = me.peekEvent()) !== null) {\r | |
1329 | if (animations && animations.getCount()) {\r | |
1330 | return true;\r | |
1331 | }\r | |
1332 | if (eventDescriptor.keyframe) {\r | |
1333 | if (!me.processKeyFrame(eventDescriptor)) {\r | |
1334 | return false;\r | |
1335 | }\r | |
1336 | me.nextEvent(eventDescriptor);\r | |
1337 | } else if (eventDescriptor.ts <= me.getTimeIndex() && me.fireEvent('beforeplay', me, eventDescriptor) !== false && me.playEvent(eventDescriptor)) {\r | |
1338 | me.nextEvent(eventDescriptor);\r | |
1339 | } else {\r | |
1340 | return true;\r | |
1341 | }\r | |
1342 | }\r | |
1343 | me.stop();\r | |
1344 | return false;\r | |
1345 | },\r | |
1346 | /** | |
1347 | * This method is called when a keyframe is reached. This will fire the keyframe event. | |
1348 | * If the keyframe has been handled, true is returned. Otherwise, false is returned. | |
1349 | * @param {Object} eventDescriptor The event descriptor of the keyframe. | |
1350 | * @return {Boolean} True if the keyframe was handled, false if not. | |
1351 | */\r | |
1352 | processKeyFrame: function(eventDescriptor) {\r | |
1353 | var me = this;\r | |
1354 | // only fire keyframe event (and setup the eventDescriptor) once...\r | |
1355 | if (!eventDescriptor.defer) {\r | |
1356 | me.makeToken(eventDescriptor, 'done');\r | |
1357 | me.fireEvent('keyframe', me, eventDescriptor);\r | |
1358 | }\r | |
1359 | return eventDescriptor.done;\r | |
1360 | },\r | |
1361 | /** | |
1362 | * Called to inject the given event on the specified target. | |
1363 | * @param {HTMLElement} target The target of the event. | |
1364 | * @param {Object} event The event to inject. The properties of this object should be | |
1365 | * those of standard DOM events but vary based on the `type` property. For details on | |
1366 | * event types and their properties, see the class documentation. | |
1367 | */\r | |
1368 | injectEvent: function(target, event) {\r | |
1369 | var me = this,\r | |
1370 | type = event.type,\r | |
1371 | options = Ext.apply({}, event, defaults[type]),\r | |
1372 | handler;\r | |
1373 | if (type === 'type') {\r | |
1374 | handler = me._inputSpecialKeys[target.tagName];\r | |
1375 | if (handler) {\r | |
1376 | return me.injectTypeInputEvent(target, event, handler);\r | |
1377 | }\r | |
1378 | return me.injectTypeEvent(target, event);\r | |
1379 | }\r | |
1380 | if (type === 'focus' && target.focus) {\r | |
1381 | target.focus();\r | |
1382 | return true;\r | |
1383 | }\r | |
1384 | if (type === 'blur' && target.blur) {\r | |
1385 | target.blur();\r | |
1386 | return true;\r | |
1387 | }\r | |
1388 | if (type === 'scroll') {\r | |
1389 | target.scrollLeft = event.pos[0];\r | |
1390 | target.scrollTop = event.pos[1];\r | |
1391 | return true;\r | |
1392 | }\r | |
1393 | if (type === 'mduclick') {\r | |
1394 | return me.injectEvent(target, Ext.applyIf({\r | |
1395 | type: 'mousedown'\r | |
1396 | }, event)) && me.injectEvent(target, Ext.applyIf({\r | |
1397 | type: 'mouseup'\r | |
1398 | }, event)) && me.injectEvent(target, Ext.applyIf({\r | |
1399 | type: 'click'\r | |
1400 | }, event));\r | |
1401 | }\r | |
1402 | if (mouseEvents[type]) {\r | |
1403 | return Player.injectMouseEvent(target, options, me.attachTo);\r | |
1404 | }\r | |
1405 | if (keyEvents[type]) {\r | |
1406 | return Player.injectKeyEvent(target, options, me.attachTo);\r | |
1407 | }\r | |
1408 | if (uiEvents[type]) {\r | |
1409 | return Player.injectUIEvent(target, type, options.bubbles, options.cancelable, options.view || me.attachTo, options.detail);\r | |
1410 | }\r | |
1411 | return false;\r | |
1412 | },\r | |
1413 | injectTypeEvent: function(target, event) {\r | |
1414 | var me = this,\r | |
1415 | text = event.text,\r | |
1416 | xlat = [],\r | |
1417 | ch, chUp, i, n, sel, upper, isInput;\r | |
1418 | if (text) {\r | |
1419 | delete event.text;\r | |
1420 | upper = text.toUpperCase();\r | |
1421 | for (i = 0 , n = text.length; i < n; ++i) {\r | |
1422 | ch = text.charCodeAt(i);\r | |
1423 | chUp = upper.charCodeAt(i);\r | |
1424 | xlat.push(Ext.applyIf({\r | |
1425 | type: 'keydown',\r | |
1426 | charCode: chUp,\r | |
1427 | keyCode: chUp\r | |
1428 | }, event), Ext.applyIf({\r | |
1429 | type: 'keypress',\r | |
1430 | charCode: ch,\r | |
1431 | keyCode: ch\r | |
1432 | }, event), Ext.applyIf({\r | |
1433 | type: 'keyup',\r | |
1434 | charCode: chUp,\r | |
1435 | keyCode: chUp\r | |
1436 | }, event));\r | |
1437 | }\r | |
1438 | } else {\r | |
1439 | xlat.push(Ext.applyIf({\r | |
1440 | type: 'keydown',\r | |
1441 | charCode: event.keyCode\r | |
1442 | }, event), Ext.applyIf({\r | |
1443 | type: 'keyup',\r | |
1444 | charCode: event.keyCode\r | |
1445 | }, event));\r | |
1446 | }\r | |
1447 | for (i = 0 , n = xlat.length; i < n; ++i) {\r | |
1448 | me.injectEvent(target, xlat[i]);\r | |
1449 | }\r | |
1450 | return true;\r | |
1451 | },\r | |
1452 | injectTypeInputEvent: function(target, event, handler) {\r | |
1453 | var me = this,\r | |
1454 | text = event.text,\r | |
1455 | sel, n;\r | |
1456 | if (handler) {\r | |
1457 | sel = me.getTextSelection(target);\r | |
1458 | if (text) {\r | |
1459 | n = sel[0];\r | |
1460 | target.value = target.value.substring(0, n) + text + target.value.substring(sel[1]);\r | |
1461 | n += text.length;\r | |
1462 | me.setTextSelection(target, n, n);\r | |
1463 | } else {\r | |
1464 | if (!(handler = handler[event.keyCode])) {\r | |
1465 | // no handler for the special key for this element\r | |
1466 | if ('caret' in event) {\r | |
1467 | me.setTextSelection(target, event.caret, event.caret);\r | |
1468 | } else if (event.selection) {\r | |
1469 | me.setTextSelection(target, event.selection[0], event.selection[1]);\r | |
1470 | }\r | |
1471 | return me.injectTypeEvent(target, event);\r | |
1472 | }\r | |
1473 | handler.call(this, target, sel[0], sel[1]);\r | |
1474 | return true;\r | |
1475 | }\r | |
1476 | }\r | |
1477 | return true;\r | |
1478 | },\r | |
1479 | playEvent: function(eventDescriptor) {\r | |
1480 | var me = this,\r | |
1481 | target = me.getElementFromXPath(eventDescriptor.target),\r | |
1482 | event;\r | |
1483 | if (!target) {\r | |
1484 | // not present (yet)... wait for element present...\r | |
1485 | // TODO - need a timeout here\r | |
1486 | return false;\r | |
1487 | }\r | |
1488 | if (!me.playEventHook(eventDescriptor, 'beforeplay')) {\r | |
1489 | return false;\r | |
1490 | }\r | |
1491 | if (!eventDescriptor.injected) {\r | |
1492 | eventDescriptor.injected = true;\r | |
1493 | event = me.translateEvent(eventDescriptor, target);\r | |
1494 | me.injectEvent(target, event);\r | |
1495 | }\r | |
1496 | return me.playEventHook(eventDescriptor, 'afterplay');\r | |
1497 | },\r | |
1498 | playEventHook: function(eventDescriptor, hookName) {\r | |
1499 | var me = this,\r | |
1500 | doneName = hookName + '.done',\r | |
1501 | firedName = hookName + '.fired',\r | |
1502 | hook = eventDescriptor[hookName];\r | |
1503 | if (hook && !eventDescriptor[doneName]) {\r | |
1504 | if (!eventDescriptor[firedName]) {\r | |
1505 | eventDescriptor[firedName] = true;\r | |
1506 | me.makeToken(eventDescriptor, doneName);\r | |
1507 | if (me.eventScope && Ext.isString(hook)) {\r | |
1508 | hook = me.eventScope[hook];\r | |
1509 | }\r | |
1510 | if (hook) {\r | |
1511 | hook.call(me.eventScope || me, eventDescriptor);\r | |
1512 | }\r | |
1513 | }\r | |
1514 | return false;\r | |
1515 | }\r | |
1516 | return true;\r | |
1517 | },\r | |
1518 | schedule: function() {\r | |
1519 | var me = this;\r | |
1520 | if (!me.timer) {\r | |
1521 | me.timer = setTimeout(me.timerFn, 10);\r | |
1522 | }\r | |
1523 | },\r | |
1524 | _translateAcross: [\r | |
1525 | 'type',\r | |
1526 | 'button',\r | |
1527 | 'charCode',\r | |
1528 | 'keyCode',\r | |
1529 | 'caret',\r | |
1530 | 'pos',\r | |
1531 | 'text',\r | |
1532 | 'selection'\r | |
1533 | ],\r | |
1534 | translateEvent: function(eventDescriptor, target) {\r | |
1535 | var me = this,\r | |
1536 | event = {},\r | |
1537 | modKeys = eventDescriptor.modKeys || '',\r | |
1538 | names = me._translateAcross,\r | |
1539 | i = names.length,\r | |
1540 | name, xy;\r | |
1541 | while (i--) {\r | |
1542 | name = names[i];\r | |
1543 | if (name in eventDescriptor) {\r | |
1544 | event[name] = eventDescriptor[name];\r | |
1545 | }\r | |
1546 | }\r | |
1547 | event.altKey = modKeys.indexOf('A') > 0;\r | |
1548 | event.ctrlKey = modKeys.indexOf('C') > 0;\r | |
1549 | event.metaKey = modKeys.indexOf('M') > 0;\r | |
1550 | event.shiftKey = modKeys.indexOf('S') > 0;\r | |
1551 | if (target && 'x' in eventDescriptor) {\r | |
1552 | xy = Ext.fly(target).getXY();\r | |
1553 | xy[0] += eventDescriptor.x;\r | |
1554 | xy[1] += eventDescriptor.y;\r | |
1555 | } else if ('x' in eventDescriptor) {\r | |
1556 | xy = [\r | |
1557 | eventDescriptor.x,\r | |
1558 | eventDescriptor.y\r | |
1559 | ];\r | |
1560 | } else if ('px' in eventDescriptor) {\r | |
1561 | xy = [\r | |
1562 | eventDescriptor.px,\r | |
1563 | eventDescriptor.py\r | |
1564 | ];\r | |
1565 | }\r | |
1566 | if (xy) {\r | |
1567 | event.clientX = event.screenX = xy[0];\r | |
1568 | event.clientY = event.screenY = xy[1];\r | |
1569 | }\r | |
1570 | if (eventDescriptor.key) {\r | |
1571 | event.keyCode = me.specialKeysByName[eventDescriptor.key];\r | |
1572 | }\r | |
1573 | if (eventDescriptor.type === 'wheel') {\r | |
1574 | if ('onwheel' in me.attachTo.document) {\r | |
1575 | event.wheelX = eventDescriptor.dx;\r | |
1576 | event.wheelY = eventDescriptor.dy;\r | |
1577 | } else {\r | |
1578 | event.type = 'mousewheel';\r | |
1579 | event.wheelDeltaX = -40 * eventDescriptor.dx;\r | |
1580 | event.wheelDeltaY = event.wheelDelta = -40 * eventDescriptor.dy;\r | |
1581 | }\r | |
1582 | }\r | |
1583 | return event;\r | |
1584 | },\r | |
1585 | //---------------------------------\r | |
1586 | // Driver overrides\r | |
1587 | onStart: function() {\r | |
1588 | var me = this;\r | |
1589 | me.queueIndex = 0;\r | |
1590 | me.schedule();\r | |
1591 | },\r | |
1592 | onStop: function() {\r | |
1593 | var me = this;\r | |
1594 | if (me.timer) {\r | |
1595 | clearTimeout(me.timer);\r | |
1596 | me.timer = null;\r | |
1597 | }\r | |
1598 | },\r | |
1599 | //---------------------------------\r | |
1600 | onTick: function() {\r | |
1601 | var me = this;\r | |
1602 | me.timer = null;\r | |
1603 | if (me.processEvents()) {\r | |
1604 | me.schedule();\r | |
1605 | }\r | |
1606 | },\r | |
1607 | statics: {\r | |
1608 | ieButtonCodeMap: {\r | |
1609 | 0: 1,\r | |
1610 | 1: 4,\r | |
1611 | 2: 2\r | |
1612 | },\r | |
1613 | /** | |
1614 | * Injects a key event using the given event information to populate the event | |
1615 | * object. | |
1616 | * | |
1617 | * **Note:** `keydown` causes Safari 2.x to crash. | |
1618 | * | |
1619 | * @param {HTMLElement} target The target of the given event. | |
1620 | * @param {Object} options Object object containing all of the event injection | |
1621 | * options. | |
1622 | * @param {String} options.type The type of event to fire. This can be any one of | |
1623 | * the following: `keyup`, `keydown` and `keypress`. | |
1624 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. | |
1625 | * DOM Level 3 specifies that all key events bubble by default. | |
1626 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled | |
1627 | * using `preventDefault`. DOM Level 3 specifies that all key events can be | |
1628 | * cancelled. | |
1629 | * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is | |
1630 | * pressed while the event is firing. | |
1631 | * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is | |
1632 | * pressed while the event is firing. | |
1633 | * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is | |
1634 | * pressed while the event is firing. | |
1635 | * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is | |
1636 | * pressed while the event is firing. | |
1637 | * @param {Number} [options.keyCode=0] The code for the key that is in use. | |
1638 | * @param {Number} [options.charCode=0] The Unicode code for the character | |
1639 | * associated with the key being used. | |
1640 | * @param {Window} [view=window] The view containing the target. This is typically | |
1641 | * the window object. | |
1642 | * @private | |
1643 | */\r | |
1644 | injectKeyEvent: function(target, options, view) {\r | |
1645 | var type = options.type,\r | |
1646 | customEvent = null;\r | |
1647 | if (type === 'textevent') {\r | |
1648 | type = 'keypress';\r | |
1649 | }\r | |
1650 | view = view || window;\r | |
1651 | //check for DOM-compliant browsers first\r | |
1652 | if (doc.createEvent) {\r | |
1653 | try {\r | |
1654 | customEvent = doc.createEvent("KeyEvents");\r | |
1655 | // Interesting problem: Firefox implemented a non-standard\r | |
1656 | // version of initKeyEvent() based on DOM Level 2 specs.\r | |
1657 | // Key event was removed from DOM Level 2 and re-introduced\r | |
1658 | // in DOM Level 3 with a different interface. Firefox is the\r | |
1659 | // only browser with any implementation of Key Events, so for\r | |
1660 | // now, assume it's Firefox if the above line doesn't error.\r | |
1661 | // @TODO: Decipher between Firefox's implementation and a correct one.\r | |
1662 | customEvent.initKeyEvent(type, options.bubbles, options.cancelable, view, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode);\r | |
1663 | } catch (ex) {\r | |
1664 | // If it got here, that means key events aren't officially supported. \r | |
1665 | // Safari/WebKit is a real problem now. WebKit 522 won't let you\r | |
1666 | // set keyCode, charCode, or other properties if you use a\r | |
1667 | // UIEvent, so we first must try to create a generic event. The\r | |
1668 | // fun part is that this will throw an error on Safari 2.x. The\r | |
1669 | // end result is that we need another try...catch statement just to\r | |
1670 | // deal with this mess.\r | |
1671 | try {\r | |
1672 | //try to create generic event - will fail in Safari 2.x\r | |
1673 | customEvent = doc.createEvent("Events");\r | |
1674 | } catch (uierror) {\r | |
1675 | //the above failed, so create a UIEvent for Safari 2.x\r | |
1676 | customEvent = doc.createEvent("UIEvents");\r | |
1677 | } finally {\r | |
1678 | customEvent.initEvent(type, options.bubbles, options.cancelable);\r | |
1679 | customEvent.view = view;\r | |
1680 | customEvent.altKey = options.altKey;\r | |
1681 | customEvent.ctrlKey = options.ctrlKey;\r | |
1682 | customEvent.shiftKey = options.shiftKey;\r | |
1683 | customEvent.metaKey = options.metaKey;\r | |
1684 | customEvent.keyCode = options.keyCode;\r | |
1685 | customEvent.charCode = options.charCode;\r | |
1686 | }\r | |
1687 | }\r | |
1688 | target.dispatchEvent(customEvent);\r | |
1689 | } else if (doc.createEventObject) {\r | |
1690 | //IE\r | |
1691 | customEvent = doc.createEventObject();\r | |
1692 | customEvent.bubbles = options.bubbles;\r | |
1693 | customEvent.cancelable = options.cancelable;\r | |
1694 | customEvent.view = view;\r | |
1695 | customEvent.ctrlKey = options.ctrlKey;\r | |
1696 | customEvent.altKey = options.altKey;\r | |
1697 | customEvent.shiftKey = options.shiftKey;\r | |
1698 | customEvent.metaKey = options.metaKey;\r | |
1699 | // IE doesn't support charCode explicitly. CharCode should\r | |
1700 | // take precedence over any keyCode value for accurate\r | |
1701 | // representation.\r | |
1702 | customEvent.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;\r | |
1703 | target.fireEvent("on" + type, customEvent);\r | |
1704 | } else {\r | |
1705 | return false;\r | |
1706 | }\r | |
1707 | return true;\r | |
1708 | },\r | |
1709 | /** | |
1710 | * Injects a mouse event using the given event information to populate the event | |
1711 | * object. | |
1712 | * | |
1713 | * @param {HTMLElement} target The target of the given event. | |
1714 | * @param {Object} options Object object containing all of the event injection | |
1715 | * options. | |
1716 | * @param {String} options.type The type of event to fire. This can be any one of | |
1717 | * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`, | |
1718 | * `mouseover` and `mousemove`. | |
1719 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. | |
1720 | * DOM Level 2 specifies that all mouse events bubble by default. | |
1721 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled | |
1722 | * using `preventDefault`. DOM Level 2 specifies that all mouse events except | |
1723 | * `mousemove` can be cancelled. This defaults to `false` for `mousemove`. | |
1724 | * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is | |
1725 | * pressed while the event is firing. | |
1726 | * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is | |
1727 | * pressed while the event is firing. | |
1728 | * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is | |
1729 | * pressed while the event is firing. | |
1730 | * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is | |
1731 | * pressed while the event is firing. | |
1732 | * @param {Number} [options.detail=1] The number of times the mouse button has | |
1733 | * been used. | |
1734 | * @param {Number} [options.screenX=0] The x-coordinate on the screen at which point | |
1735 | * the event occurred. | |
1736 | * @param {Number} [options.screenY=0] The y-coordinate on the screen at which point | |
1737 | * the event occurred. | |
1738 | * @param {Number} [options.clientX=0] The x-coordinate on the client at which point | |
1739 | * the event occurred. | |
1740 | * @param {Number} [options.clientY=0] The y-coordinate on the client at which point | |
1741 | * the event occurred. | |
1742 | * @param {Number} [options.button=0] The button being pressed while the event is | |
1743 | * executing. The value should be 0 for the primary mouse button (typically the | |
1744 | * left button), 1 for the tertiary mouse button (typically the middle button), | |
1745 | * and 2 for the secondary mouse button (typically the right button). | |
1746 | * @param {HTMLElement} [options.relatedTarget=null] For `mouseout` events, this | |
1747 | * is the element that the mouse has moved to. For `mouseover` events, this is | |
1748 | * the element that the mouse has moved from. This argument is ignored for all | |
1749 | * other events. | |
1750 | * @param {Window} [view=window] The view containing the target. This is typically | |
1751 | * the window object. | |
1752 | * @private | |
1753 | */\r | |
1754 | injectMouseEvent: function(target, options, view) {\r | |
1755 | var type = options.type,\r | |
1756 | customEvent = null;\r | |
1757 | view = view || window;\r | |
1758 | //check for DOM-compliant browsers first\r | |
1759 | if (doc.createEvent) {\r | |
1760 | customEvent = doc.createEvent("MouseEvents");\r | |
1761 | //Safari 2.x (WebKit 418) still doesn't implement initMouseEvent()\r | |
1762 | if (customEvent.initMouseEvent) {\r | |
1763 | customEvent.initMouseEvent(type, options.bubbles, options.cancelable, view, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, options.relatedTarget);\r | |
1764 | } else {\r | |
1765 | //Safari\r | |
1766 | //the closest thing available in Safari 2.x is UIEvents\r | |
1767 | customEvent = doc.createEvent("UIEvents");\r | |
1768 | customEvent.initEvent(type, options.bubbles, options.cancelable);\r | |
1769 | customEvent.view = view;\r | |
1770 | customEvent.detail = options.detail;\r | |
1771 | customEvent.screenX = options.screenX;\r | |
1772 | customEvent.screenY = options.screenY;\r | |
1773 | customEvent.clientX = options.clientX;\r | |
1774 | customEvent.clientY = options.clientY;\r | |
1775 | customEvent.ctrlKey = options.ctrlKey;\r | |
1776 | customEvent.altKey = options.altKey;\r | |
1777 | customEvent.metaKey = options.metaKey;\r | |
1778 | customEvent.shiftKey = options.shiftKey;\r | |
1779 | customEvent.button = options.button;\r | |
1780 | customEvent.relatedTarget = options.relatedTarget;\r | |
1781 | }\r | |
1782 | /* | |
1783 | * Check to see if relatedTarget has been assigned. Firefox | |
1784 | * versions less than 2.0 don't allow it to be assigned via | |
1785 | * initMouseEvent() and the property is readonly after event | |
1786 | * creation, so in order to keep YAHOO.util.getRelatedTarget() | |
1787 | * working, assign to the IE proprietary toElement property | |
1788 | * for mouseout event and fromElement property for mouseover | |
1789 | * event. | |
1790 | */\r | |
1791 | if (options.relatedTarget && !customEvent.relatedTarget) {\r | |
1792 | if (type == "mouseout") {\r | |
1793 | customEvent.toElement = options.relatedTarget;\r | |
1794 | } else if (type == "mouseover") {\r | |
1795 | customEvent.fromElement = options.relatedTarget;\r | |
1796 | }\r | |
1797 | }\r | |
1798 | target.dispatchEvent(customEvent);\r | |
1799 | } else if (doc.createEventObject) {\r | |
1800 | //IE\r | |
1801 | customEvent = doc.createEventObject();\r | |
1802 | customEvent.bubbles = options.bubbles;\r | |
1803 | customEvent.cancelable = options.cancelable;\r | |
1804 | customEvent.view = view;\r | |
1805 | customEvent.detail = options.detail;\r | |
1806 | customEvent.screenX = options.screenX;\r | |
1807 | customEvent.screenY = options.screenY;\r | |
1808 | customEvent.clientX = options.clientX;\r | |
1809 | customEvent.clientY = options.clientY;\r | |
1810 | customEvent.ctrlKey = options.ctrlKey;\r | |
1811 | customEvent.altKey = options.altKey;\r | |
1812 | customEvent.metaKey = options.metaKey;\r | |
1813 | customEvent.shiftKey = options.shiftKey;\r | |
1814 | customEvent.button = Player.ieButtonCodeMap[options.button] || 0;\r | |
1815 | /* | |
1816 | * Have to use relatedTarget because IE won't allow assignment | |
1817 | * to toElement or fromElement on generic events. This keeps | |
1818 | * YAHOO.util.customEvent.getRelatedTarget() functional. | |
1819 | */\r | |
1820 | customEvent.relatedTarget = options.relatedTarget;\r | |
1821 | target.fireEvent('on' + type, customEvent);\r | |
1822 | } else {\r | |
1823 | return false;\r | |
1824 | }\r | |
1825 | return true;\r | |
1826 | },\r | |
1827 | /** | |
1828 | * Injects a UI event using the given event information to populate the event | |
1829 | * object. | |
1830 | * | |
1831 | * @param {HTMLElement} target The target of the given event. | |
1832 | * @param {Object} options | |
1833 | * @param {String} options.type The type of event to fire. This can be any one of | |
1834 | * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`, | |
1835 | * `mouseover` and `mousemove`. | |
1836 | * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. | |
1837 | * DOM Level 2 specifies that all mouse events bubble by default. | |
1838 | * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled | |
1839 | * using `preventDefault`. DOM Level 2 specifies that all mouse events except | |
1840 | * `mousemove` can be canceled. This defaults to `false` for `mousemove`. | |
1841 | * @param {Number} [options.detail=1] The number of times the mouse button has been | |
1842 | * used. | |
1843 | * @param {Window} [view=window] The view containing the target. This is typically | |
1844 | * the window object. | |
1845 | * @private | |
1846 | */\r | |
1847 | injectUIEvent: function(target, options, view) {\r | |
1848 | var customEvent = null;\r | |
1849 | view = view || window;\r | |
1850 | //check for DOM-compliant browsers first\r | |
1851 | if (doc.createEvent) {\r | |
1852 | //just a generic UI Event object is needed\r | |
1853 | customEvent = doc.createEvent("UIEvents");\r | |
1854 | customEvent.initUIEvent(options.type, options.bubbles, options.cancelable, view, options.detail);\r | |
1855 | target.dispatchEvent(customEvent);\r | |
1856 | } else if (doc.createEventObject) {\r | |
1857 | //IE\r | |
1858 | customEvent = doc.createEventObject();\r | |
1859 | customEvent.bubbles = options.bubbles;\r | |
1860 | customEvent.cancelable = options.cancelable;\r | |
1861 | customEvent.view = view;\r | |
1862 | customEvent.detail = options.detail;\r | |
1863 | target.fireEvent("on" + options.type, customEvent);\r | |
1864 | } else {\r | |
1865 | return false;\r | |
1866 | }\r | |
1867 | return true;\r | |
1868 | }\r | |
1869 | }\r | |
1870 | };\r | |
1871 | });\r | |
1872 | // statics\r | |
1873 | \r | |
1874 | /** | |
1875 | * @extends Ext.ux.event.Driver | |
1876 | * Event recorder. | |
1877 | */\r | |
1878 | Ext.define('Ext.ux.event.Recorder', function(Recorder) {\r | |
1879 | function apply() {\r | |
1880 | var a = arguments,\r | |
1881 | n = a.length,\r | |
1882 | obj = {\r | |
1883 | kind: 'other'\r | |
1884 | },\r | |
1885 | i;\r | |
1886 | for (i = 0; i < n; ++i) {\r | |
1887 | Ext.apply(obj, arguments[i]);\r | |
1888 | }\r | |
1889 | if (obj.alt && !obj.event) {\r | |
1890 | obj.event = obj.alt;\r | |
1891 | }\r | |
1892 | return obj;\r | |
1893 | }\r | |
1894 | function key(extra) {\r | |
1895 | return apply({\r | |
1896 | kind: 'keyboard',\r | |
1897 | modKeys: true,\r | |
1898 | key: true\r | |
1899 | }, extra);\r | |
1900 | }\r | |
1901 | function mouse(extra) {\r | |
1902 | return apply({\r | |
1903 | kind: 'mouse',\r | |
1904 | button: true,\r | |
1905 | modKeys: true,\r | |
1906 | xy: true\r | |
1907 | }, extra);\r | |
1908 | }\r | |
1909 | var eventsToRecord = {\r | |
1910 | keydown: key(),\r | |
1911 | keypress: key(),\r | |
1912 | keyup: key(),\r | |
1913 | dragmove: mouse({\r | |
1914 | alt: 'mousemove',\r | |
1915 | pageCoords: true,\r | |
1916 | whileDrag: true\r | |
1917 | }),\r | |
1918 | mousemove: mouse({\r | |
1919 | pageCoords: true\r | |
1920 | }),\r | |
1921 | mouseover: mouse(),\r | |
1922 | mouseout: mouse(),\r | |
1923 | click: mouse(),\r | |
1924 | wheel: mouse({\r | |
1925 | wheel: true\r | |
1926 | }),\r | |
1927 | mousedown: mouse({\r | |
1928 | press: true\r | |
1929 | }),\r | |
1930 | mouseup: mouse({\r | |
1931 | release: true\r | |
1932 | }),\r | |
1933 | scroll: apply({\r | |
1934 | listen: false\r | |
1935 | }),\r | |
1936 | focus: apply(),\r | |
1937 | blur: apply()\r | |
1938 | };\r | |
1939 | for (var key in eventsToRecord) {\r | |
1940 | if (!eventsToRecord[key].event) {\r | |
1941 | eventsToRecord[key].event = key;\r | |
1942 | }\r | |
1943 | }\r | |
1944 | eventsToRecord.wheel.event = null;\r | |
1945 | // must detect later\r | |
1946 | return {\r | |
1947 | extend: 'Ext.ux.event.Driver',\r | |
1948 | /** | |
1949 | * @event add | |
1950 | * Fires when an event is added to the recording. | |
1951 | * @param {Ext.ux.event.Recorder} this | |
1952 | * @param {Object} eventDescriptor The event descriptor. | |
1953 | */\r | |
1954 | /** | |
1955 | * @event coalesce | |
1956 | * Fires when an event is coalesced. This edits the tail of the recorded | |
1957 | * event list. | |
1958 | * @param {Ext.ux.event.Recorder} this | |
1959 | * @param {Object} eventDescriptor The event descriptor that was coalesced. | |
1960 | */\r | |
1961 | eventsToRecord: eventsToRecord,\r | |
1962 | ignoreIdRegEx: /ext-gen(?:\d+)/,\r | |
1963 | inputRe: /^(input|textarea)$/i,\r | |
1964 | constructor: function(config) {\r | |
1965 | var me = this,\r | |
1966 | events = config && config.eventsToRecord;\r | |
1967 | if (events) {\r | |
1968 | me.eventsToRecord = Ext.apply(Ext.apply({}, me.eventsToRecord), // duplicate\r | |
1969 | events);\r | |
1970 | // and merge\r | |
1971 | delete config.eventsToRecord;\r | |
1972 | }\r | |
1973 | // don't smash\r | |
1974 | me.callParent(arguments);\r | |
1975 | me.clear();\r | |
1976 | me.modKeys = [];\r | |
1977 | me.attachTo = me.attachTo || window;\r | |
1978 | },\r | |
1979 | clear: function() {\r | |
1980 | this.eventsRecorded = [];\r | |
1981 | },\r | |
1982 | listenToEvent: function(event) {\r | |
1983 | var me = this,\r | |
1984 | el = me.attachTo.document.body,\r | |
1985 | fn = function() {\r | |
1986 | return me.onEvent.apply(me, arguments);\r | |
1987 | },\r | |
1988 | cleaner = {};\r | |
1989 | if (el.attachEvent && el.ownerDocument.documentMode < 10) {\r | |
1990 | event = 'on' + event;\r | |
1991 | el.attachEvent(event, fn);\r | |
1992 | cleaner.destroy = function() {\r | |
1993 | if (fn) {\r | |
1994 | el.detachEvent(event, fn);\r | |
1995 | fn = null;\r | |
1996 | }\r | |
1997 | };\r | |
1998 | } else {\r | |
1999 | el.addEventListener(event, fn, true);\r | |
2000 | cleaner.destroy = function() {\r | |
2001 | if (fn) {\r | |
2002 | el.removeEventListener(event, fn, true);\r | |
2003 | fn = null;\r | |
2004 | }\r | |
2005 | };\r | |
2006 | }\r | |
2007 | return cleaner;\r | |
2008 | },\r | |
2009 | coalesce: function(rec, ev) {\r | |
2010 | var me = this,\r | |
2011 | events = me.eventsRecorded,\r | |
2012 | length = events.length,\r | |
2013 | tail = length && events[length - 1],\r | |
2014 | tail2 = (length > 1) && events[length - 2],\r | |
2015 | tail3 = (length > 2) && events[length - 3];\r | |
2016 | if (!tail) {\r | |
2017 | return false;\r | |
2018 | }\r | |
2019 | if (rec.type === 'mousemove') {\r | |
2020 | if (tail.type === 'mousemove' && rec.ts - tail.ts < 200) {\r | |
2021 | rec.ts = tail.ts;\r | |
2022 | events[length - 1] = rec;\r | |
2023 | return true;\r | |
2024 | }\r | |
2025 | } else if (rec.type === 'click') {\r | |
2026 | if (tail2 && tail.type === 'mouseup' && tail2.type === 'mousedown') {\r | |
2027 | if (rec.button == tail.button && rec.button == tail2.button && rec.target == tail.target && rec.target == tail2.target && me.samePt(rec, tail) && me.samePt(rec, tail2)) {\r | |
2028 | events.pop();\r | |
2029 | // remove mouseup\r | |
2030 | tail2.type = 'mduclick';\r | |
2031 | return true;\r | |
2032 | }\r | |
2033 | }\r | |
2034 | } else if (rec.type === 'keyup') {\r | |
2035 | // tail3 = { type: "type", text: "..." },\r | |
2036 | // tail2 = { type: "keydown", charCode: 65, keyCode: 65 },\r | |
2037 | // tail = { type: "keypress", charCode: 97, keyCode: 97 },\r | |
2038 | // rec = { type: "keyup", charCode: 65, keyCode: 65 },\r | |
2039 | if (tail2 && tail.type === 'keypress' && tail2.type === 'keydown') {\r | |
2040 | if (rec.target === tail.target && rec.target === tail2.target) {\r | |
2041 | events.pop();\r | |
2042 | // remove keypress\r | |
2043 | tail2.type = 'type';\r | |
2044 | tail2.text = String.fromCharCode(tail.charCode);\r | |
2045 | delete tail2.charCode;\r | |
2046 | delete tail2.keyCode;\r | |
2047 | if (tail3 && tail3.type === 'type') {\r | |
2048 | if (tail3.text && tail3.target === tail2.target) {\r | |
2049 | tail3.text += tail2.text;\r | |
2050 | events.pop();\r | |
2051 | }\r | |
2052 | }\r | |
2053 | return true;\r | |
2054 | }\r | |
2055 | }\r | |
2056 | // tail = { type: "keydown", charCode: 40, keyCode: 40 },\r | |
2057 | // rec = { type: "keyup", charCode: 40, keyCode: 40 },\r | |
2058 | else if (me.completeKeyStroke(tail, rec)) {\r | |
2059 | tail.type = 'type';\r | |
2060 | me.completeSpecialKeyStroke(ev.target, tail, rec);\r | |
2061 | return true;\r | |
2062 | }\r | |
2063 | // tail2 = { type: "keydown", charCode: 40, keyCode: 40 },\r | |
2064 | // tail = { type: "scroll", ... },\r | |
2065 | // rec = { type: "keyup", charCode: 40, keyCode: 40 },\r | |
2066 | else if (tail.type === 'scroll' && me.completeKeyStroke(tail2, rec)) {\r | |
2067 | tail2.type = 'type';\r | |
2068 | me.completeSpecialKeyStroke(ev.target, tail2, rec);\r | |
2069 | // swap the order of type and scroll events\r | |
2070 | events.pop();\r | |
2071 | events.pop();\r | |
2072 | events.push(tail, tail2);\r | |
2073 | return true;\r | |
2074 | }\r | |
2075 | }\r | |
2076 | return false;\r | |
2077 | },\r | |
2078 | completeKeyStroke: function(down, up) {\r | |
2079 | if (down && down.type === 'keydown' && down.keyCode === up.keyCode) {\r | |
2080 | delete down.charCode;\r | |
2081 | return true;\r | |
2082 | }\r | |
2083 | return false;\r | |
2084 | },\r | |
2085 | completeSpecialKeyStroke: function(target, down, up) {\r | |
2086 | var key = this.specialKeysByCode[up.keyCode];\r | |
2087 | if (key && this.inputRe.test(target.tagName)) {\r | |
2088 | // home,end,arrow keys + shift get crazy, so encode selection/caret\r | |
2089 | delete down.keyCode;\r | |
2090 | down.key = key;\r | |
2091 | down.selection = this.getTextSelection(target);\r | |
2092 | if (down.selection[0] === down.selection[1]) {\r | |
2093 | down.caret = down.selection[0];\r | |
2094 | delete down.selection;\r | |
2095 | }\r | |
2096 | return true;\r | |
2097 | }\r | |
2098 | return false;\r | |
2099 | },\r | |
2100 | getElementXPath: function(el) {\r | |
2101 | var me = this,\r | |
2102 | good = false,\r | |
2103 | xpath = [],\r | |
2104 | count, sibling, t, tag;\r | |
2105 | for (t = el; t; t = t.parentNode) {\r | |
2106 | if (t == me.attachTo.document.body) {\r | |
2107 | xpath.unshift('~');\r | |
2108 | good = true;\r | |
2109 | break;\r | |
2110 | }\r | |
2111 | if (t.id && !me.ignoreIdRegEx.test(t.id)) {\r | |
2112 | xpath.unshift('#' + t.id);\r | |
2113 | good = true;\r | |
2114 | break;\r | |
2115 | }\r | |
2116 | for (count = 1 , sibling = t; !!(sibling = sibling.previousSibling); ) {\r | |
2117 | if (sibling.tagName == t.tagName) {\r | |
2118 | ++count;\r | |
2119 | }\r | |
2120 | }\r | |
2121 | tag = t.tagName.toLowerCase();\r | |
2122 | if (count < 2) {\r | |
2123 | xpath.unshift(tag);\r | |
2124 | } else {\r | |
2125 | xpath.unshift(tag + '[' + count + ']');\r | |
2126 | }\r | |
2127 | }\r | |
2128 | return good ? xpath.join('/') : null;\r | |
2129 | },\r | |
2130 | getRecordedEvents: function() {\r | |
2131 | return this.eventsRecorded;\r | |
2132 | },\r | |
2133 | onEvent: function(ev) {\r | |
2134 | var me = this,\r | |
2135 | e = new Ext.event.Event(ev),\r | |
2136 | info = me.eventsToRecord[e.type],\r | |
2137 | root, modKeys, elXY,\r | |
2138 | rec = {\r | |
2139 | type: e.type,\r | |
2140 | ts: me.getTimestamp(),\r | |
2141 | target: me.getElementXPath(e.target)\r | |
2142 | },\r | |
2143 | xy;\r | |
2144 | if (!info || !rec.target) {\r | |
2145 | return;\r | |
2146 | }\r | |
2147 | root = e.target.ownerDocument;\r | |
2148 | root = root.defaultView || root.parentWindow;\r | |
2149 | // Standards || IE\r | |
2150 | if (root !== me.attachTo) {\r | |
2151 | return;\r | |
2152 | }\r | |
2153 | if (me.eventsToRecord.scroll) {\r | |
2154 | me.syncScroll(e.target);\r | |
2155 | }\r | |
2156 | if (info.xy) {\r | |
2157 | xy = e.getXY();\r | |
2158 | if (info.pageCoords || !rec.target) {\r | |
2159 | rec.px = xy[0];\r | |
2160 | rec.py = xy[1];\r | |
2161 | } else {\r | |
2162 | elXY = Ext.fly(e.getTarget()).getXY();\r | |
2163 | xy[0] -= elXY[0];\r | |
2164 | xy[1] -= elXY[1];\r | |
2165 | rec.x = xy[0];\r | |
2166 | rec.y = xy[1];\r | |
2167 | }\r | |
2168 | }\r | |
2169 | if (info.button) {\r | |
2170 | if ('buttons' in ev) {\r | |
2171 | rec.button = ev.buttons;\r | |
2172 | } else // LEFT=1, RIGHT=2, MIDDLE=4, etc.\r | |
2173 | {\r | |
2174 | rec.button = ev.button;\r | |
2175 | }\r | |
2176 | if (!rec.button && info.whileDrag) {\r | |
2177 | return;\r | |
2178 | }\r | |
2179 | }\r | |
2180 | if (info.wheel) {\r | |
2181 | rec.type = 'wheel';\r | |
2182 | if (info.event === 'wheel') {\r | |
2183 | // Current FireFox (technically IE9+ if we use addEventListener but\r | |
2184 | // checking document.onwheel does not detect this)\r | |
2185 | rec.dx = ev.deltaX;\r | |
2186 | rec.dy = ev.deltaY;\r | |
2187 | } else if (typeof ev.wheelDeltaX === 'number') {\r | |
2188 | // new WebKit has both X & Y\r | |
2189 | rec.dx = -1 / 40 * ev.wheelDeltaX;\r | |
2190 | rec.dy = -1 / 40 * ev.wheelDeltaY;\r | |
2191 | } else if (ev.wheelDelta) {\r | |
2192 | // old WebKit and IE\r | |
2193 | rec.dy = -1 / 40 * ev.wheelDelta;\r | |
2194 | } else if (ev.detail) {\r | |
2195 | // Old Gecko\r | |
2196 | rec.dy = ev.detail;\r | |
2197 | }\r | |
2198 | }\r | |
2199 | if (info.modKeys) {\r | |
2200 | me.modKeys[0] = e.altKey ? 'A' : '';\r | |
2201 | me.modKeys[1] = e.ctrlKey ? 'C' : '';\r | |
2202 | me.modKeys[2] = e.metaKey ? 'M' : '';\r | |
2203 | me.modKeys[3] = e.shiftKey ? 'S' : '';\r | |
2204 | modKeys = me.modKeys.join('');\r | |
2205 | if (modKeys) {\r | |
2206 | rec.modKeys = modKeys;\r | |
2207 | }\r | |
2208 | }\r | |
2209 | if (info.key) {\r | |
2210 | rec.charCode = e.getCharCode();\r | |
2211 | rec.keyCode = e.getKey();\r | |
2212 | }\r | |
2213 | if (me.coalesce(rec, e)) {\r | |
2214 | me.fireEvent('coalesce', me, rec);\r | |
2215 | } else {\r | |
2216 | me.eventsRecorded.push(rec);\r | |
2217 | me.fireEvent('add', me, rec);\r | |
2218 | }\r | |
2219 | },\r | |
2220 | onStart: function() {\r | |
2221 | var me = this,\r | |
2222 | ddm = me.attachTo.Ext.dd.DragDropManager,\r | |
2223 | evproto = me.attachTo.Ext.EventObjectImpl.prototype,\r | |
2224 | special = [];\r | |
2225 | // FireFox does not support the 'mousewheel' event but does support the\r | |
2226 | // 'wheel' event instead.\r | |
2227 | Recorder.prototype.eventsToRecord.wheel.event = ('onwheel' in me.attachTo.document) ? 'wheel' : 'mousewheel';\r | |
2228 | me.listeners = [];\r | |
2229 | Ext.Object.each(me.eventsToRecord, function(name, value) {\r | |
2230 | if (value && value.listen !== false) {\r | |
2231 | if (!value.event) {\r | |
2232 | value.event = name;\r | |
2233 | }\r | |
2234 | if (value.alt && value.alt !== name) {\r | |
2235 | // The 'drag' event is just mousemove while buttons are pressed,\r | |
2236 | // so if there is a mousemove entry as well, ignore the drag\r | |
2237 | if (!me.eventsToRecord[value.alt]) {\r | |
2238 | special.push(value);\r | |
2239 | }\r | |
2240 | } else {\r | |
2241 | me.listeners.push(me.listenToEvent(value.event));\r | |
2242 | }\r | |
2243 | }\r | |
2244 | });\r | |
2245 | Ext.each(special, function(info) {\r | |
2246 | me.eventsToRecord[info.alt] = info;\r | |
2247 | me.listeners.push(me.listenToEvent(info.alt));\r | |
2248 | });\r | |
2249 | me.ddmStopEvent = ddm.stopEvent;\r | |
2250 | ddm.stopEvent = Ext.Function.createSequence(ddm.stopEvent, function(e) {\r | |
2251 | me.onEvent(e);\r | |
2252 | });\r | |
2253 | me.evStopEvent = evproto.stopEvent;\r | |
2254 | evproto.stopEvent = Ext.Function.createSequence(evproto.stopEvent, function() {\r | |
2255 | me.onEvent(this);\r | |
2256 | });\r | |
2257 | },\r | |
2258 | onStop: function() {\r | |
2259 | var me = this;\r | |
2260 | Ext.destroy(me.listeners);\r | |
2261 | me.listeners = null;\r | |
2262 | me.attachTo.Ext.dd.DragDropManager.stopEvent = me.ddmStopEvent;\r | |
2263 | me.attachTo.Ext.EventObjectImpl.prototype.stopEvent = me.evStopEvent;\r | |
2264 | },\r | |
2265 | samePt: function(pt1, pt2) {\r | |
2266 | return pt1.x == pt2.x && pt1.y == pt2.y;\r | |
2267 | },\r | |
2268 | syncScroll: function(el) {\r | |
2269 | var me = this,\r | |
2270 | ts = me.getTimestamp(),\r | |
2271 | oldX, oldY, x, y, scrolled, rec;\r | |
2272 | for (var p = el; p; p = p.parentNode) {\r | |
2273 | oldX = p.$lastScrollLeft;\r | |
2274 | oldY = p.$lastScrollTop;\r | |
2275 | x = p.scrollLeft;\r | |
2276 | y = p.scrollTop;\r | |
2277 | scrolled = false;\r | |
2278 | if (oldX !== x) {\r | |
2279 | if (x) {\r | |
2280 | scrolled = true;\r | |
2281 | }\r | |
2282 | p.$lastScrollLeft = x;\r | |
2283 | }\r | |
2284 | if (oldY !== y) {\r | |
2285 | if (y) {\r | |
2286 | scrolled = true;\r | |
2287 | }\r | |
2288 | p.$lastScrollTop = y;\r | |
2289 | }\r | |
2290 | if (scrolled) {\r | |
2291 | //console.log('scroll x:' + x + ' y:' + y, p);\r | |
2292 | me.eventsRecorded.push(rec = {\r | |
2293 | type: 'scroll',\r | |
2294 | target: me.getElementXPath(p),\r | |
2295 | ts: ts,\r | |
2296 | pos: [\r | |
2297 | x,\r | |
2298 | y\r | |
2299 | ]\r | |
2300 | });\r | |
2301 | me.fireEvent('add', me, rec);\r | |
2302 | }\r | |
2303 | if (p.tagName === 'BODY') {\r | |
2304 | break;\r | |
2305 | }\r | |
2306 | }\r | |
2307 | }\r | |
2308 | };\r | |
2309 | });\r | |
2310 | \r | |
2311 | /** | |
2312 | * This base class can be used by derived classes to dynamically require Google API's. | |
2313 | */\r | |
2314 | Ext.define('Ext.ux.google.Api', {\r | |
2315 | mixins: [\r | |
2316 | 'Ext.mixin.Mashup'\r | |
2317 | ],\r | |
2318 | requiredScripts: [\r | |
2319 | '//www.google.com/jsapi'\r | |
2320 | ],\r | |
2321 | statics: {\r | |
2322 | loadedModules: {}\r | |
2323 | },\r | |
2324 | /* | |
2325 | * feeds: [ callback1, callback2, .... ] transitions to -> feeds : true (when complete) | |
2326 | */\r | |
2327 | onClassExtended: function(cls, data, hooks) {\r | |
2328 | var onBeforeClassCreated = hooks.onBeforeCreated,\r | |
2329 | Api = this;\r | |
2330 | // the Ext.ux.google.Api class\r | |
2331 | hooks.onBeforeCreated = function(cls, data) {\r | |
2332 | var me = this,\r | |
2333 | apis = [],\r | |
2334 | requiresGoogle = Ext.Array.from(data.requiresGoogle),\r | |
2335 | loadedModules = Api.loadedModules,\r | |
2336 | remaining = 0,\r | |
2337 | callback = function() {\r | |
2338 | if (!--remaining) {\r | |
2339 | onBeforeClassCreated.call(me, cls, data, hooks);\r | |
2340 | }\r | |
2341 | Ext.env.Ready.unblock();\r | |
2342 | },\r | |
2343 | api, i, length;\r | |
2344 | /* | |
2345 | * requiresGoogle: [ | |
2346 | * 'feeds', | |
2347 | * { api: 'feeds', version: '1.x', | |
2348 | * callback : fn, nocss : true } //optionals | |
2349 | * ] | |
2350 | */\r | |
2351 | length = requiresGoogle.length;\r | |
2352 | for (i = 0; i < length; ++i) {\r | |
2353 | if (Ext.isString(api = requiresGoogle[i])) {\r | |
2354 | apis.push({\r | |
2355 | api: api\r | |
2356 | });\r | |
2357 | } else if (Ext.isObject(api)) {\r | |
2358 | apis.push(Ext.apply({}, api));\r | |
2359 | }\r | |
2360 | }\r | |
2361 | Ext.each(apis, function(api) {\r | |
2362 | var name = api.api,\r | |
2363 | version = String(api.version || '1.x'),\r | |
2364 | module = loadedModules[name];\r | |
2365 | if (!module) {\r | |
2366 | ++remaining;\r | |
2367 | Ext.env.Ready.block();\r | |
2368 | loadedModules[name] = module = [\r | |
2369 | callback\r | |
2370 | ].concat(api.callback || []);\r | |
2371 | delete api.api;\r | |
2372 | delete api.version;\r | |
2373 | //TODO: window.google assertion?\r | |
2374 | google.load(name, version, Ext.applyIf({\r | |
2375 | callback: function() {\r | |
2376 | loadedModules[name] = true;\r | |
2377 | for (var n = module.length; n-- > 0; ) {\r | |
2378 | module[n]();\r | |
2379 | }\r | |
2380 | }\r | |
2381 | }, //iterate callbacks in reverse\r | |
2382 | api));\r | |
2383 | } else if (module !== true) {\r | |
2384 | module.push(callback);\r | |
2385 | }\r | |
2386 | });\r | |
2387 | if (!remaining) {\r | |
2388 | onBeforeClassCreated.call(me, cls, data, hooks);\r | |
2389 | }\r | |
2390 | };\r | |
2391 | }\r | |
2392 | });\r | |
2393 | \r | |
2394 | /** | |
2395 | * This class, when required, ensures that the Google RSS Feeds API is available. | |
2396 | */\r | |
2397 | Ext.define('Ext.ux.google.Feeds', {\r | |
2398 | extend: 'Ext.ux.google.Api',\r | |
2399 | requiresGoogle: {\r | |
2400 | api: 'feeds',\r | |
2401 | nocss: true\r | |
2402 | }\r | |
2403 | });\r | |
2404 | \r |