]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/util/Event.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / util / Event.js
CommitLineData
6527f429
DM
1// @tag core\r
2/**\r
3 * Represents single event type that an Observable object listens to.\r
4 * All actual listeners are tracked inside here. When the event fires,\r
5 * it calls all the registered listener functions.\r
6 *\r
7 * @private\r
8 */\r
9Ext.define('Ext.util.Event', function() {\r
10 var arraySlice = Array.prototype.slice,\r
11 arrayInsert = Ext.Array.insert,\r
12 toArray = Ext.Array.toArray,\r
13 fireArgs = {};\r
14\r
15 return {\r
16 requires: 'Ext.util.DelayedTask',\r
17\r
18 /**\r
19 * @property {Boolean} isEvent\r
20 * `true` in this class to identify an object as an instantiated Event, or subclass thereof.\r
21 */\r
22 isEvent: true,\r
23 \r
24 // Private. Event suspend count\r
25 suspended: 0,\r
26\r
27 noOptions: {},\r
28\r
29 constructor: function(observable, name) {\r
30 this.name = name;\r
31 this.observable = observable;\r
32 this.listeners = [];\r
33 },\r
34\r
35 addListener: function(fn, scope, options, caller, manager) {\r
36 var me = this,\r
37 added = false,\r
38 observable = me.observable,\r
39 eventName = me.name,\r
40 listeners, listener, priority, isNegativePriority, highestNegativePriorityIndex,\r
41 hasNegativePriorityIndex, length, index, i, listenerPriority;\r
42\r
43 //<debug>\r
44 if (scope && !Ext._namedScopes[scope] && (typeof fn === 'string') && (typeof scope[fn] !== 'function')) {\r
45 Ext.raise("No method named '" + fn + "' found on scope object");\r
46 }\r
47 //</debug>\r
48\r
49 if (me.findListener(fn, scope) === -1) {\r
50 listener = me.createListener(fn, scope, options, caller, manager);\r
51 if (me.firing) {\r
52 // if we are currently firing this event, don't disturb the listener loop\r
53 me.listeners = me.listeners.slice(0);\r
54 }\r
55 listeners = me.listeners;\r
56 index = length = listeners.length;\r
57 priority = options && options.priority;\r
58 highestNegativePriorityIndex = me._highestNegativePriorityIndex;\r
59 hasNegativePriorityIndex = highestNegativePriorityIndex !== undefined;\r
60 if (priority) {\r
61 // Find the index at which to insert the listener into the listeners array,\r
62 // sorted by priority highest to lowest.\r
63 isNegativePriority = (priority < 0);\r
64 if (!isNegativePriority || hasNegativePriorityIndex) {\r
65 // If the priority is a positive number, or if it is a negative number\r
66 // and there are other existing negative priority listenrs, then we\r
67 // need to calcuate the listeners priority-order index.\r
68 // If the priority is a negative number, begin the search for priority\r
69 // order index at the index of the highest existing negative priority\r
70 // listener, otherwise begin at 0\r
71 for(i = (isNegativePriority ? highestNegativePriorityIndex : 0); i < length; i++) {\r
72 // Listeners created without options will have no "o" property\r
73 listenerPriority = listeners[i].o ? listeners[i].o.priority||0 : 0;\r
74 if (listenerPriority < priority) {\r
75 index = i;\r
76 break;\r
77 }\r
78 }\r
79 } else {\r
80 // if the priority is a negative number, and there are no other negative\r
81 // priority listeners, then no calculation is needed - the negative\r
82 // priority listener gets appended to the end of the listeners array.\r
83 me._highestNegativePriorityIndex = index;\r
84 }\r
85 } else if (hasNegativePriorityIndex) {\r
86 // listeners with a priority of 0 or undefined are appended to the end of\r
87 // the listeners array unless there are negative priority listeners in the\r
88 // listeners array, then they are inserted before the highest negative\r
89 // priority listener.\r
90 index = highestNegativePriorityIndex;\r
91 }\r
92\r
93 if (!isNegativePriority && index <= highestNegativePriorityIndex) {\r
94 me._highestNegativePriorityIndex ++;\r
95 }\r
96 if (index === length) {\r
97 listeners[length] = listener;\r
98 } else {\r
99 arrayInsert(listeners, index, [listener]);\r
100 }\r
101\r
102 if (observable.isElement) {\r
103 // It is the role of Ext.util.Event (vs Ext.Element) to handle subscribe/\r
104 // unsubscribe because it is the lowest level place to intercept the\r
105 // listener before it is added/removed. For addListener this could easily\r
106 // be done in Ext.Element's doAddListener override, but since there are\r
107 // multiple paths for listener removal (un, clearListeners), it is best\r
108 // to keep all subscribe/unsubscribe logic here.\r
109 observable._getPublisher(eventName).subscribe(\r
110 observable,\r
111 eventName,\r
112 options.delegated !== false,\r
113 options.capture\r
114 );\r
115 }\r
116\r
117 added = true;\r
118 }\r
119\r
120 return added;\r
121 },\r
122\r
123 createListener: function(fn, scope, o, caller, manager) {\r
124 var me = this,\r
125 namedScope = Ext._namedScopes[scope],\r
126 listener = {\r
127 fn: fn,\r
128 scope: scope,\r
129 ev: me,\r
130 caller: caller,\r
131 manager: manager,\r
132 namedScope: namedScope,\r
133 defaultScope: namedScope ? (scope || me.observable) : undefined,\r
134 lateBound: typeof fn === 'string'\r
135 },\r
136 handler = fn,\r
137 wrapped = false,\r
138 type;\r
139\r
140 // The order is important. The 'single' wrapper must be wrapped by the 'buffer' and 'delayed' wrapper\r
141 // because the event removal that the single listener does destroys the listener's DelayedTask(s)\r
142 if (o) {\r
143 listener.o = o;\r
144 if (o.single) {\r
145 handler = me.createSingle(handler, listener, o, scope);\r
146 wrapped = true;\r
147 }\r
148 if (o.target) {\r
149 handler = me.createTargeted(handler, listener, o, scope, wrapped);\r
150 wrapped = true;\r
151 }\r
152 if (o.delay) {\r
153 handler = me.createDelayed(handler, listener, o, scope, wrapped);\r
154 wrapped = true;\r
155 }\r
156 if (o.buffer) {\r
157 handler = me.createBuffered(handler, listener, o, scope, wrapped);\r
158 wrapped = true;\r
159 }\r
160\r
161 if (me.observable.isElement) {\r
162 // If the event type was translated, e.g. mousedown -> touchstart, we need to save\r
163 // the original type in the listener object so that the Ext.event.Event object can\r
164 // reflect the correct type at firing time\r
165 type = o.type;\r
166 if (type) {\r
167 listener.type = type;\r
168 }\r
169 }\r
170 }\r
171\r
172 listener.fireFn = handler;\r
173 listener.wrapped = wrapped;\r
174 return listener;\r
175 },\r
176\r
177 findListener: function(fn, scope) {\r
178 var listeners = this.listeners,\r
179 i = listeners.length,\r
180 listener;\r
181\r
182 while (i--) {\r
183 listener = listeners[i];\r
184 if (listener) {\r
185 // use ==, not === for scope comparison, so that undefined and null are equal\r
186 if (listener.fn === fn && listener.scope == scope) {\r
187 return i;\r
188 }\r
189 }\r
190 }\r
191\r
192 return - 1;\r
193 },\r
194\r
195 removeListener: function(fn, scope, index) {\r
196 var me = this,\r
197 removed = false,\r
198 observable = me.observable,\r
199 eventName = me.name,\r
200 listener, highestNegativePriorityIndex, options,\r
201 k, manager, managedListeners, managedListener, i;\r
202\r
203 index = index || me.findListener(fn, scope);\r
204\r
205 if (index != -1) {\r
206 listener = me.listeners[index];\r
207 options = listener.o;\r
208 highestNegativePriorityIndex = me._highestNegativePriorityIndex;\r
209\r
210 if (me.firing) {\r
211 me.listeners = me.listeners.slice(0);\r
212 }\r
213\r
214 // cancel and remove a buffered handler that hasn't fired yet\r
215 if (listener.task) {\r
216 listener.task.cancel();\r
217 delete listener.task;\r
218 }\r
219\r
220 // cancel and remove all delayed handlers that haven't fired yet\r
221 k = listener.tasks && listener.tasks.length;\r
222 if (k) {\r
223 while (k--) {\r
224 listener.tasks[k].cancel();\r
225 }\r
226 delete listener.tasks;\r
227 }\r
228\r
229 // Remove this listener from the listeners array\r
230 // We can use splice directly. The IE8 bug which Ext.Array works around only affects *insertion*\r
231 // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/\r
232 me.listeners.splice(index, 1);\r
233\r
234 manager = listener.manager;\r
235 if (manager) {\r
236 // If this is a managed listener we need to remove it from the manager's\r
237 // managedListeners array. This ensures that if we listen using mon\r
238 // and then remove without using mun, the managedListeners array is updated\r
239 // accordingly, for example\r
240 //\r
241 // manager.on(target, 'foo', fn);\r
242 //\r
243 // target.un('foo', fn);\r
244 managedListeners = manager.managedListeners;\r
245 if (managedListeners) {\r
246 for (i = managedListeners.length; i--;) {\r
247 managedListener = managedListeners[i];\r
248 if (managedListener.item === me.observable && managedListener.ename === eventName &&\r
249 managedListener.fn === fn && managedListener.scope === scope) {\r
250 managedListeners.splice(i, 1);\r
251 }\r
252 }\r
253 }\r
254 }\r
255\r
256 // if the listeners array contains negative priority listeners, adjust the\r
257 // internal index if needed.\r
258 if (highestNegativePriorityIndex) {\r
259 if (index < highestNegativePriorityIndex) {\r
260 me._highestNegativePriorityIndex --;\r
261 } else if (index === highestNegativePriorityIndex && index === me.listeners.length) {\r
262 delete me._highestNegativePriorityIndex;\r
263 }\r
264 }\r
265\r
266 if (observable.isElement) {\r
267 observable._getPublisher(eventName).unsubscribe(\r
268 observable,\r
269 eventName,\r
270 options.delegated !== false,\r
271 options.capture\r
272 );\r
273 }\r
274\r
275 removed = true;\r
276 }\r
277\r
278 return removed;\r
279 },\r
280\r
281 // Iterate to stop any buffered/delayed events\r
282 clearListeners: function() {\r
283 var listeners = this.listeners,\r
284 i = listeners.length,\r
285 listener;\r
286\r
287 while (i--) {\r
288 listener = listeners[i];\r
289 this.removeListener(listener.fn, listener.scope);\r
290 }\r
291 },\r
292\r
293 suspend: function() {\r
294 ++this.suspended;\r
295 },\r
296\r
297 resume: function() {\r
298 if (this.suspended) {\r
299 --this.suspended;\r
300 }\r
301 },\r
302 \r
303 isSuspended: function() {\r
304 return this.suspended > 0;\r
305 },\r
306\r
307 fireDelegated: function(firingObservable, args) {\r
308 this.firingObservable = firingObservable;\r
309 return this.fire.apply(this, args);\r
310 },\r
311\r
312 fire: function() {\r
313 var me = this,\r
314 listeners = me.listeners,\r
315 count = listeners.length,\r
316 observable = me.observable,\r
317 isElement = observable.isElement,\r
318 isComponent = observable.isComponent,\r
319 firingObservable = me.firingObservable,\r
320 options, delegate, fireInfo, i, args, listener, len, delegateEl, currentTarget,\r
321 type, chained, firingArgs, e, fireFn, fireScope;\r
322\r
323 if (!me.suspended && count > 0) {\r
324 me.firing = true;\r
325 args = arguments.length ? arraySlice.call(arguments, 0) : [];\r
326 len = args.length;\r
327 if (isElement) {\r
328 e = args[0];\r
329 }\r
330 for (i = 0; i < count; i++) {\r
331 listener = listeners[i];\r
332 options = listener.o;\r
333\r
334 if (isElement) {\r
335 if (currentTarget) {\r
336 // restore the previous currentTarget if we changed it last time\r
337 // around the loop while processing the delegate option.\r
338 e.setCurrentTarget(currentTarget);\r
339 }\r
340\r
341 // For events that have been translated to provide device compatibility,\r
342 // e.g. mousedown -> touchstart, we want the event object to reflect the\r
343 // type that was originally listened for, not the type of the actual event\r
344 // that fired. The listener's "type" property reflects the original type.\r
345 type = listener.type;\r
346\r
347 if (type) {\r
348 // chain a new object to the event object before changing the type.\r
349 // This is more efficient than creating a new event object, and we\r
350 // don't want to change the type of the original event because it may\r
351 // be used asynchronously by other handlers\r
352 chained = e;\r
353 e = args[0] = chained.chain({ type: type });\r
354 }\r
355\r
356 // In Ext4 Ext.EventObject was a singleton event object that was reused as events\r
357 // were fired. Set Ext.EventObject to the last fired event for compatibility.\r
358 Ext.EventObject = e;\r
359 }\r
360\r
361 firingArgs = args;\r
362\r
363 if (options) {\r
364 delegate = options.delegate;\r
365 if (delegate) {\r
366 if (isElement) {\r
367 // prepending the currentTarget.id to the delegate selector\r
368 // allows us to match selectors such as "> div"\r
369 delegateEl = e.getTarget('#' + e.currentTarget.id + ' ' + delegate);\r
370 if (delegateEl) {\r
371 args[1] = delegateEl;\r
372 // save the current target before changing it to the delegateEl\r
373 // so that we can restore it next time around\r
374 currentTarget = e.currentTarget;\r
375 e.setCurrentTarget(delegateEl);\r
376 } else {\r
377 continue;\r
378 }\r
379 } else if (isComponent &&\r
380 !firingObservable.is('#' + observable.id + ' ' + options.delegate)) {\r
381 continue;\r
382 }\r
383 }\r
384 \r
385 if (isElement) {\r
386 if (options.preventDefault) {\r
387 e.preventDefault();\r
388 }\r
389 \r
390 if (options.stopPropagation) {\r
391 e.stopPropagation();\r
392 }\r
393 \r
394 if (options.stopEvent) {\r
395 e.stopEvent();\r
396 }\r
397 }\r
398\r
399 args[len] = options;\r
400\r
401 if (options.args) {\r
402 firingArgs = options.args.concat(args);\r
403 }\r
404 }\r
405\r
406 fireInfo = me.getFireInfo(listener);\r
407 fireFn = fireInfo.fn;\r
408 fireScope = fireInfo.scope;\r
409 \r
410 // We don't want to keep closure and scope on the Event prototype!\r
411 fireInfo.fn = fireInfo.scope = null;\r
412 \r
413 if (fireFn.apply(fireScope, firingArgs) === false) {\r
414 Ext.EventObject = null;\r
415 \r
416 return (me.firing = false);\r
417 }\r
418\r
419 if (chained) {\r
420 // if we chained the event object for type translation we need to\r
421 // un-chain it before proceeding to process the next listener, which\r
422 // may not be a translated event.\r
423 e = args[0] = chained;\r
424 chained = null;\r
425 }\r
426 \r
427 // We don't guarantee Ext.EventObject existence outside of the immediate\r
428 // event propagation scope\r
429 Ext.EventObject = null;\r
430 }\r
431 }\r
432 \r
433 me.firing = false;\r
434 \r
435 return true;\r
436 },\r
437\r
438 getFireInfo: function(listener, fromWrapped) {\r
439 var observable = this.observable,\r
440 fireFn = listener.fireFn,\r
441 scope = listener.scope,\r
442 namedScope = listener.namedScope,\r
443 fn;\r
444\r
445 // If we are called with a wrapped listener, only attempt to do scope\r
446 // resolution if we are explicitly called by the last wrapped function\r
447 if (!fromWrapped && listener.wrapped) {\r
448 fireArgs.fn = fireFn;\r
449 return fireArgs;\r
450 }\r
451 \r
452 fn = fromWrapped ? listener.fn : fireFn;\r
453 //<debug>\r
454 var name = fn;\r
455\r
456 //</debug>\r
457 if (listener.lateBound) {\r
458 // handler is a function name - need to resolve it to a function reference\r
459 if (!scope || namedScope) {\r
460 // Only invoke resolveListenerScope if the user did not specify a scope,\r
461 // or if the user specified a named scope. Named function handlers that\r
462 // use an arbitrary object as the scope just skip this part, and just\r
463 // use the given scope object to resolve the method.\r
464 scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope);\r
465 }\r
466 //<debug>\r
467 if (!scope) {\r
468 Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);\r
469 }\r
470\r
471 if (!Ext.isFunction(scope[fn])) {\r
472 Ext.raise('No method named "' + fn + '" on ' +\r
473 (scope.$className || 'scope object.'));\r
474 }\r
475 //</debug>\r
476\r
477 fn = scope[fn];\r
478 } else if (namedScope && namedScope.isController) {\r
479 // If handler is a function reference and scope:'controller' was requested\r
480 // we'll do our best to look up a controller.\r
481 scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope);\r
482 //<debug>\r
483 if (!scope) {\r
484 Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);\r
485 }\r
486 //</debug>\r
487 } else if (!scope || namedScope) {\r
488 // If handler is a function reference we use the observable instance as\r
489 // the default scope\r
490 scope = observable;\r
491 }\r
492\r
493 // We can only ever be firing one event at a time, so just keep\r
494 // overwriting tghe object we've got in our closure, otherwise we'll be\r
495 // creating a whole bunch of garbage objects\r
496 fireArgs.fn = fn;\r
497 fireArgs.scope = scope;\r
498 //<debug>\r
499 if (!fn) {\r
500 Ext.raise('Unable to dynamically resolve method "' + name + '" on ' + this.observable.$className);\r
501 }\r
502 //</debug>\r
503 return fireArgs;\r
504 },\r
505\r
506 createTargeted: function (handler, listener, o, scope, wrapped) {\r
507 return function(){\r
508 if (o.target === arguments[0]) {\r
509 var fireInfo;\r
510\r
511 if (!wrapped) {\r
512 fireInfo = listener.ev.getFireInfo(listener, true);\r
513 handler = fireInfo.fn;\r
514 scope = fireInfo.scope;\r
515 \r
516 // We don't want to keep closure and scope references on the Event prototype!\r
517 fireInfo.fn = fireInfo.scope = null;\r
518 }\r
519\r
520 return handler.apply(scope, arguments);\r
521 }\r
522 };\r
523 },\r
524\r
525 createBuffered: function (handler, listener, o, scope, wrapped) {\r
526 listener.task = new Ext.util.DelayedTask();\r
527 return function() {\r
528 var fireInfo;\r
529\r
530 if (!wrapped) {\r
531 fireInfo = listener.ev.getFireInfo(listener, true);\r
532 handler = fireInfo.fn;\r
533 scope = fireInfo.scope;\r
534 \r
535 // We don't want to keep closure and scope references on the Event prototype!\r
536 fireInfo.fn = fireInfo.scope = null;\r
537 }\r
538\r
539 listener.task.delay(o.buffer, handler, scope, toArray(arguments));\r
540 };\r
541 },\r
542\r
543 createDelayed: function (handler, listener, o, scope, wrapped) {\r
544 return function() {\r
545 var task = new Ext.util.DelayedTask(),\r
546 fireInfo;\r
547\r
548 if (!wrapped) {\r
549 fireInfo = listener.ev.getFireInfo(listener, true);\r
550 handler = fireInfo.fn;\r
551 scope = fireInfo.scope;\r
552 \r
553 // We don't want to keep closure and scope references on the Event prototype!\r
554 fireInfo.fn = fireInfo.scope = null;\r
555 }\r
556 \r
557 if (!listener.tasks) {\r
558 listener.tasks = [];\r
559 }\r
560 listener.tasks.push(task);\r
561 task.delay(o.delay || 10, handler, scope, toArray(arguments));\r
562 };\r
563 },\r
564\r
565 createSingle: function (handler, listener, o, scope, wrapped) {\r
566 return function() {\r
567 var event = listener.ev,\r
568 fireInfo;\r
569\r
570\r
571 if (event.removeListener(listener.fn, scope) && event.observable) {\r
572 // Removing from a regular Observable-owned, named event (not an anonymous\r
573 // event such as Ext's readyEvent): Decrement the listeners count\r
574 event.observable.hasListeners[event.name]--;\r
575 }\r
576\r
577 if (!wrapped) {\r
578 fireInfo = event.getFireInfo(listener, true);\r
579 handler = fireInfo.fn;\r
580 scope = fireInfo.scope;\r
581 \r
582 // We don't want to keep closure and scope references on the Event prototype!\r
583 fireInfo.fn = fireInfo.scope = null;\r
584 }\r
585 \r
586 return handler.apply(scope, arguments);\r
587 };\r
588 }\r
589 };\r
590});\r