on.js 20 KB

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