-
Notifications
You must be signed in to change notification settings - Fork 88
/
on.js
531 lines (524 loc) · 21.1 KB
/
on.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
define(["./has!dom-addeventlistener?:./aspect", "./_base/kernel", "./sniff"], function(aspect, dojo, has){
"use strict";
if(has("dom")){ // check to make sure we are in a browser, this module should work anywhere
var major = window.ScriptEngineMajorVersion;
has.add("jscript", major && (major() + ScriptEngineMinorVersion() / 10));
has.add("event-orientationchange", has("touch") && !has("android")); // TODO: how do we detect this?
has.add("event-stopimmediatepropagation", window.Event && !!window.Event.prototype && !!window.Event.prototype.stopImmediatePropagation);
has.add("event-focusin", function(global, doc, element){
// All browsers except firefox support focusin, but too hard to feature test webkit since element.onfocusin
// is undefined. Just return true for IE and use fallback path for other browsers.
return !!element.attachEvent;
});
}
var on = function(target, type, listener, dontFix){
// summary:
// A function that provides core event listening functionality. With this function
// you can provide a target, event type, and listener to be notified of
// future matching events that are fired.
// target: Element|Object
// This is the target object or DOM element that to receive events from
// type: String|Function
// This is the name of the event to listen for or an extension event type.
// listener: Function
// This is the function that should be called when the event fires.
// returns: Object
// An object with a remove() method that can be used to stop listening for this
// event.
// description:
// To listen for "click" events on a button node, we can do:
// | define(["dojo/on"], function(listen){
// | on(button, "click", clickHandler);
// | ...
// Evented JavaScript objects can also have their own events.
// | var obj = new Evented;
// | on(obj, "foo", fooHandler);
// And then we could publish a "foo" event:
// | on.emit(obj, "foo", {key: "value"});
// We can use extension events as well. For example, you could listen for a tap gesture:
// | define(["dojo/on", "dojo/gesture/tap", function(listen, tap){
// | on(button, tap, tapHandler);
// | ...
// which would trigger fooHandler. Note that for a simple object this is equivalent to calling:
// | obj.onfoo({key:"value"});
// If you use on.emit on a DOM node, it will use native event dispatching when possible.
if(typeof target.on == "function" && typeof type != "function" && !target.nodeType){
// delegate to the target's on() method, so it can handle it's own listening if it wants (unless it
// is DOM node and we may be dealing with jQuery or Prototype's incompatible addition to the
// Element prototype
return target.on(type, listener);
}
// delegate to main listener code
return on.parse(target, type, listener, addListener, dontFix, this);
};
on.pausable = function(target, type, listener, dontFix){
// summary:
// This function acts the same as on(), but with pausable functionality. The
// returned signal object has pause() and resume() functions. Calling the
// pause() method will cause the listener to not be called for future events. Calling the
// resume() method will cause the listener to again be called for future events.
var paused;
var signal = on(target, type, function(){
if(!paused){
return listener.apply(this, arguments);
}
}, dontFix);
signal.pause = function(){
paused = true;
};
signal.resume = function(){
paused = false;
};
return signal;
};
on.once = function(target, type, listener, dontFix){
// summary:
// This function acts the same as on(), but will only call the listener once. The
// listener will be called for the first
// event that takes place and then listener will automatically be removed.
var signal = on(target, type, function(){
// remove this listener
signal.remove();
// proceed to call the listener
return listener.apply(this, arguments);
});
return signal;
};
on.parse = function(target, type, listener, addListener, dontFix, matchesTarget){
if(type.call){
// event handler function
// on(node, touch.press, touchListener);
return type.call(matchesTarget, target, listener);
}
if(type.indexOf(",") > -1){
// we allow comma delimited event names, so you can register for multiple events at once
var events = type.split(/\s*,\s*/);
var handles = [];
var i = 0;
var eventName;
while(eventName = events[i++]){
handles.push(addListener(target, eventName, listener, dontFix, matchesTarget));
}
handles.remove = function(){
for(var i = 0; i < handles.length; i++){
handles[i].remove();
}
};
return handles;
}
return addListener(target, type, listener, dontFix, matchesTarget);
};
var touchEvents = /^touch/;
function addListener(target, type, listener, dontFix, matchesTarget){
// event delegation:
var selector = type.match(/(.*):(.*)/);
// if we have a selector:event, the last one is interpreted as an event, and we use event delegation
if(selector){
type = selector[2];
selector = selector[1];
// create the extension event for selectors and directly call it
return on.selector(selector, type).call(matchesTarget, target, listener);
}
// test to see if it a touch event right now, so we don't have to do it every time it fires
if(has("touch")){
if(touchEvents.test(type)){
// touch event, fix it
listener = fixTouchListener(listener);
}
if(!has("event-orientationchange") && (type == "orientationchange")){
//"orientationchange" not supported <= Android 2.1,
//but works through "resize" on window
type = "resize";
target = window;
listener = fixTouchListener(listener);
}
}
if(addStopImmediate){
// add stopImmediatePropagation if it doesn't exist
listener = addStopImmediate(listener);
}
// normal path, the target is |this|
if(target.addEventListener){
// the target has addEventListener, which should be used if available (might or might not be a node, non-nodes can implement this method as well)
// check for capture conversions
var capture = type in captures,
adjustedType = capture ? captures[type] : type;
target.addEventListener(adjustedType, listener, capture);
// create and return the signal
return {
remove: function(){
target.removeEventListener(adjustedType, listener, capture);
}
};
}
type = "on" + type;
if(fixAttach && target.attachEvent){
return fixAttach(target, type, listener);
}
throw new Error("Target must be an event emitter");
}
on.selector = function(selector, eventType, children){
// summary:
// Creates a new extension event with event delegation. This is based on
// the provided event type (can be extension event) that
// only calls the listener when the CSS selector matches the target of the event.
//
// The application must require() an appropriate level of dojo/query to handle the selector.
// selector:
// The CSS selector to use for filter events and determine the |this| of the event listener.
// eventType:
// The event to listen for
// children:
// Indicates if children elements of the selector should be allowed. This defaults to
// true
// example:
// | require(["dojo/on", "dojo/mouse", "dojo/query!css2"], function(listen, mouse){
// | on(node, on.selector(".my-class", mouse.enter), handlerForMyHover);
return function(target, listener){
// if the selector is function, use it to select the node, otherwise use the matches method
var matchesTarget = typeof selector == "function" ? {matches: selector} : this,
bubble = eventType.bubble;
function select(eventTarget){
// see if we have a valid matchesTarget or default to dojo.query
matchesTarget = matchesTarget && matchesTarget.matches ? matchesTarget : dojo.query;
// there is a selector, so make sure it matches
while(!matchesTarget.matches(eventTarget, selector, target)){
if(eventTarget == target || children === false || !(eventTarget = eventTarget.parentNode) || eventTarget.nodeType != 1){ // intentional assignment
return;
}
}
return eventTarget;
}
if(bubble){
// the event type doesn't naturally bubble, but has a bubbling form, use that, and give it the selector so it can perform the select itself
return on(target, bubble(select), listener);
}
// standard event delegation
return on(target, eventType, function(event){
// call select to see if we match
var eventTarget = select(event.target);
// if it matches we call the listener
return eventTarget && listener.call(eventTarget, event);
});
};
};
function syntheticPreventDefault(){
this.cancelable = false;
this.defaultPrevented = true;
}
function syntheticStopPropagation(){
this.bubbles = false;
}
var slice = [].slice,
syntheticDispatch = on.emit = function(target, type, event){
// summary:
// Fires an event on the target object.
// target:
// The target object to fire the event on. This can be a DOM element or a plain
// JS object. If the target is a DOM element, native event emitting mechanisms
// are used when possible.
// type:
// The event type name. You can emulate standard native events like "click" and
// "mouseover" or create custom events like "open" or "finish".
// event:
// An object that provides the properties for the event. See https://developer.mozilla.org/en/DOM/event.initEvent
// for some of the properties. These properties are copied to the event object.
// Of particular importance are the cancelable and bubbles properties. The
// cancelable property indicates whether or not the event has a default action
// that can be cancelled. The event is cancelled by calling preventDefault() on
// the event object. The bubbles property indicates whether or not the
// event will bubble up the DOM tree. If bubbles is true, the event will be called
// on the target and then each parent successively until the top of the tree
// is reached or stopPropagation() is called. Both bubbles and cancelable
// default to false.
// returns:
// If the event is cancelable and the event is not cancelled,
// emit will return true. If the event is cancelable and the event is cancelled,
// emit will return false.
// details:
// Note that this is designed to emit events for listeners registered through
// dojo/on. It should actually work with any event listener except those
// added through IE's attachEvent (IE8 and below's non-W3C event emitting
// doesn't support custom event types). It should work with all events registered
// through dojo/on. Also note that the emit method does do any default
// action, it only returns a value to indicate if the default action should take
// place. For example, emitting a keypress event would not cause a character
// to appear in a textbox.
// example:
// To fire our own click event
// | on.emit(dojo.byId("button"), "click", {
// | cancelable: true,
// | bubbles: true,
// | screenX: 33,
// | screenY: 44
// | });
// We can also fire our own custom events:
// | on.emit(dojo.byId("slider"), "slide", {
// | cancelable: true,
// | bubbles: true,
// | direction: "left-to-right"
// | });
var args = slice.call(arguments, 2);
var method = "on" + type;
if("parentNode" in target){
// node (or node-like), create event controller methods
var newEvent = args[0] = {};
for(var i in event){
newEvent[i] = event[i];
}
newEvent.preventDefault = syntheticPreventDefault;
newEvent.stopPropagation = syntheticStopPropagation;
newEvent.target = target;
newEvent.type = type;
event = newEvent;
}
do{
// call any node which has a handler (note that ideally we would try/catch to simulate normal event propagation but that causes too much pain for debugging)
target[method] && target[method].apply(target, args);
// and then continue up the parent node chain if it is still bubbling (if started as bubbles and stopPropagation hasn't been called)
}while(event && event.bubbles && (target = target.parentNode));
return event && event.cancelable && event; // if it is still true (was cancelable and was cancelled), return the event to indicate default action should happen
};
var captures = has("event-focusin") ? {} : {focusin: "focus", focusout: "blur"};
if(!has("event-stopimmediatepropagation")){
var stopImmediatePropagation =function(){
this.immediatelyStopped = true;
this.modified = true; // mark it as modified so the event will be cached in IE
};
var addStopImmediate = function(listener){
return function(event){
if(!event.immediatelyStopped){// check to make sure it hasn't been stopped immediately
event.stopImmediatePropagation = stopImmediatePropagation;
return listener.apply(this, arguments);
}
};
}
}
if(has("dom-addeventlistener")){
// emitter that works with native event handling
on.emit = function(target, type, event){
if(target.dispatchEvent && document.createEvent){
// use the native event emitting mechanism if it is available on the target object
// create a generic event
// we could create branch into the different types of event constructors, but
// that would be a lot of extra code, with little benefit that I can see, seems
// best to use the generic constructor and copy properties over, making it
// easy to have events look like the ones created with specific initializers
var nativeEvent = target.ownerDocument.createEvent("HTMLEvents");
nativeEvent.initEvent(type, !!event.bubbles, !!event.cancelable);
// and copy all our properties over
for(var i in event){
if(!(i in nativeEvent)){
nativeEvent[i] = event[i];
}
}
return target.dispatchEvent(nativeEvent) && nativeEvent;
}
return syntheticDispatch.apply(on, arguments); // emit for a non-node
};
}else{
// no addEventListener, basically old IE event normalization
on._fixEvent = function(evt, sender){
// summary:
// normalizes properties on the event object including event
// bubbling methods, keystroke normalization, and x/y positions
// evt:
// native event object
// sender:
// node to treat as "currentTarget"
if(!evt){
var w = sender && (sender.ownerDocument || sender.document || sender).parentWindow || window;
evt = w.event;
}
if(!evt){return evt;}
try{
if(lastEvent && evt.type == lastEvent.type && evt.srcElement == lastEvent.target){
// should be same event, reuse event object (so it can be augmented);
// accessing evt.srcElement rather than evt.target since evt.target not set on IE until fixup below
evt = lastEvent;
}
}catch(e){
// will occur on IE on lastEvent.type reference if lastEvent points to a previous event that already
// finished bubbling, but the setTimeout() to clear lastEvent hasn't fired yet
}
if(!evt.target){ // check to see if it has been fixed yet
evt.target = evt.srcElement;
evt.currentTarget = (sender || evt.srcElement);
if(evt.type == "mouseover"){
evt.relatedTarget = evt.fromElement;
}
if(evt.type == "mouseout"){
evt.relatedTarget = evt.toElement;
}
if(!evt.stopPropagation){
evt.stopPropagation = stopPropagation;
evt.preventDefault = preventDefault;
}
switch(evt.type){
case "keypress":
var c = ("charCode" in evt ? evt.charCode : evt.keyCode);
if (c==10){
// CTRL-ENTER is CTRL-ASCII(10) on IE, but CTRL-ENTER on Mozilla
c=0;
evt.keyCode = 13;
}else if(c==13||c==27){
c=0; // Mozilla considers ENTER and ESC non-printable
}else if(c==3){
c=99; // Mozilla maps CTRL-BREAK to CTRL-c
}
// Mozilla sets keyCode to 0 when there is a charCode
// but that stops the event on IE.
evt.charCode = c;
_setKeyChar(evt);
break;
}
}
return evt;
};
var lastEvent, IESignal = function(handle){
this.handle = handle;
};
IESignal.prototype.remove = function(){
delete _dojoIEListeners_[this.handle];
};
var fixListener = function(listener){
// this is a minimal function for closing on the previous listener with as few as variables as possible
return function(evt){
evt = on._fixEvent(evt, this);
var result = listener.call(this, evt);
if(evt.modified){
// cache the last event and reuse it if we can
if(!lastEvent){
setTimeout(function(){
lastEvent = null;
});
}
lastEvent = evt;
}
return result;
};
};
var fixAttach = function(target, type, listener){
listener = fixListener(listener);
if(((target.ownerDocument ? target.ownerDocument.parentWindow : target.parentWindow || target.window || window) != top ||
has("jscript") < 5.8) &&
!has("config-_allow_leaks")){
// IE will leak memory on certain handlers in frames (IE8 and earlier) and in unattached DOM nodes for JScript 5.7 and below.
// Here we use global redirection to solve the memory leaks
if(typeof _dojoIEListeners_ == "undefined"){
_dojoIEListeners_ = [];
}
var emitter = target[type];
if(!emitter || !emitter.listeners){
var oldListener = emitter;
emitter = Function('event', 'var callee = arguments.callee; for(var i = 0; i<callee.listeners.length; i++){var listener = _dojoIEListeners_[callee.listeners[i]]; if(listener){listener.call(this,event);}}');
emitter.listeners = [];
target[type] = emitter;
emitter.global = this;
if(oldListener){
emitter.listeners.push(_dojoIEListeners_.push(oldListener) - 1);
}
}
var handle;
emitter.listeners.push(handle = (emitter.global._dojoIEListeners_.push(listener) - 1));
return new IESignal(handle);
}
return aspect.after(target, type, listener, true);
};
var _setKeyChar = function(evt){
evt.keyChar = evt.charCode ? String.fromCharCode(evt.charCode) : '';
evt.charOrCode = evt.keyChar || evt.keyCode;
};
// Called in Event scope
var stopPropagation = function(){
this.cancelBubble = true;
};
var preventDefault = on._preventDefault = function(){
// Setting keyCode to 0 is the only way to prevent certain keypresses (namely
// ctrl-combinations that correspond to menu accelerator keys).
// Otoh, it prevents upstream listeners from getting this information
// Try to split the difference here by clobbering keyCode only for ctrl
// combinations. If you still need to access the key upstream, bubbledKeyCode is
// provided as a workaround.
this.bubbledKeyCode = this.keyCode;
if(this.ctrlKey){
try{
// squelch errors when keyCode is read-only
// (e.g. if keyCode is ctrl or shift)
this.keyCode = 0;
}catch(e){
}
}
this.defaultPrevented = true;
this.returnValue = false;
this.modified = true; // mark it as modified (for defaultPrevented flag) so the event will be cached in IE
};
}
if(has("touch")){
var Event = function(){};
var windowOrientation = window.orientation;
var fixTouchListener = function(listener){
return function(originalEvent){
//Event normalization(for ontouchxxx and resize):
//1.incorrect e.pageX|pageY in iOS
//2.there are no "e.rotation", "e.scale" and "onorientationchange" in Android
//3.More TBD e.g. force | screenX | screenX | clientX | clientY | radiusX | radiusY
// see if it has already been corrected
var event = originalEvent.corrected;
if(!event){
var type = originalEvent.type;
try{
delete originalEvent.type; // on some JS engines (android), deleting properties make them mutable
}catch(e){}
if(originalEvent.type){
// deleting properties doesn't work (older iOS), have to use delegation
if(has('mozilla')){
// Firefox doesn't like delegated properties, so we have to copy
var event = {};
for(var name in originalEvent){
event[name] = originalEvent[name];
}
}else{
// old iOS branch
Event.prototype = originalEvent;
var event = new Event;
}
// have to delegate methods to make them work
event.preventDefault = function(){
originalEvent.preventDefault();
};
event.stopPropagation = function(){
originalEvent.stopPropagation();
};
}else{
// deletion worked, use property as is
event = originalEvent;
event.type = type;
}
originalEvent.corrected = event;
if(type == 'resize'){
if(windowOrientation == window.orientation){
return null;//double tap causes an unexpected 'resize' in Android
}
windowOrientation = window.orientation;
event.type = "orientationchange";
return listener.call(this, event);
}
// We use the original event and augment, rather than doing an expensive mixin operation
if(!("rotation" in event)){ // test to see if it has rotation
event.rotation = 0;
event.scale = 1;
}
//use event.changedTouches[0].pageX|pageY|screenX|screenY|clientX|clientY|target
var firstChangeTouch = event.changedTouches[0];
for(var i in firstChangeTouch){ // use for-in, we don't need to have dependency on dojo/_base/lang here
delete event[i]; // delete it first to make it mutable
event[i] = firstChangeTouch[i];
}
}
return listener.call(this, event);
};
};
}
return on;
});