mottle-0.4.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. ;(function() {
  2. "use strict";
  3. var Sniff = {
  4. android:navigator.userAgent.toLowerCase().indexOf("android") > -1
  5. };
  6. var matchesSelector = function(el, selector, ctx) {
  7. ctx = ctx || el.parentNode;
  8. var possibles = ctx.querySelectorAll(selector);
  9. for (var i = 0; i < possibles.length; i++) {
  10. if (possibles[i] === el) {
  11. return true;
  12. }
  13. }
  14. return false;
  15. },
  16. _gel = function(el) { return typeof el == "string" ? document.getElementById(el) : el; },
  17. _t = function(e) { return e.srcElement || e.target; },
  18. _d = function(l, fn) {
  19. for (var i = 0, j = l.length; i < j; i++) {
  20. if (l[i] == fn) break;
  21. }
  22. if (i < l.length) l.splice(i, 1);
  23. },
  24. guid = 1,
  25. //
  26. // this function generates a guid for every handler, sets it on the handler, then adds
  27. // it to the associated object's map of handlers for the given event. this is what enables us
  28. // to unbind all events of some type, or all events (the second of which can be requested by the user,
  29. // but it also used by Mottle when an element is removed.)
  30. _store = function(obj, event, fn) {
  31. var g = guid++;
  32. obj.__ta = obj.__ta || {};
  33. obj.__ta[event] = obj.__ta[event] || {};
  34. // store each handler with a unique guid.
  35. obj.__ta[event][g] = fn;
  36. // set the guid on the handler.
  37. fn.__tauid = g;
  38. return g;
  39. },
  40. _unstore = function(obj, event, fn) {
  41. obj.__ta && obj.__ta[event] && delete obj.__ta[event][fn.__tauid];
  42. // a handler might have attached extra functions, so we unbind those too.
  43. if (fn.__taExtra) {
  44. for (var i = 0; i < fn.__taExtra.length; i++) {
  45. _unbind(obj, fn.__taExtra[i][0], fn.__taExtra[i][1]);
  46. }
  47. fn.__taExtra.length = 0;
  48. }
  49. // a handler might have attached an unstore callback
  50. fn.__taUnstore && fn.__taUnstore();
  51. },
  52. _curryChildFilter = function(children, obj, fn, evt) {
  53. if (children == null) return fn;
  54. else {
  55. var c = children.split(","),
  56. _fn = function(e) {
  57. _fn.__tauid = fn.__tauid;
  58. var t = _t(e);
  59. for (var i = 0; i < c.length; i++) {
  60. if (matchesSelector(t, c[i], obj)) {
  61. fn.apply(t, arguments);
  62. }
  63. }
  64. };
  65. registerExtraFunction(fn, evt, _fn);
  66. return _fn;
  67. }
  68. },
  69. //
  70. // registers an 'extra' function on some event listener function we were given - a function that we
  71. // created and bound to the element as part of our housekeeping, and which we want to unbind and remove
  72. // whenever the given function is unbound.
  73. registerExtraFunction = function(fn, evt, newFn) {
  74. fn.__taExtra = fn.__taExtra || [];
  75. fn.__taExtra.push([evt, newFn]);
  76. },
  77. DefaultHandler = function(obj, evt, fn, children) {
  78. // TODO: this was here originally because i wanted to handle devices that are both touch AND mouse. however this can cause certain of the helper
  79. // functions to be bound twice, as - for example - on a nexus 4, both a mouse event and a touch event are fired. the use case i had in mind
  80. // was a device such as an Asus touch pad thing, which has a touch pad but can also be controlled with a mouse.
  81. //if (isMouseDevice)
  82. // _bind(obj, evt, _curryChildFilter(children, obj, fn, evt), fn);
  83. if (isTouchDevice && touchMap[evt]) {
  84. _bind(obj, touchMap[evt], _curryChildFilter(children, obj, fn, touchMap[evt]), fn);
  85. }
  86. else
  87. _bind(obj, evt, _curryChildFilter(children, obj, fn, evt), fn);
  88. },
  89. SmartClickHandler = function(obj, evt, fn, children) {
  90. if (obj.__taSmartClicks == null) {
  91. var down = function(e) { obj.__tad = _pageLocation(e); },
  92. up = function(e) { obj.__tau = _pageLocation(e); },
  93. click = function(e) {
  94. if (obj.__tad && obj.__tau && obj.__tad[0] === obj.__tau[0] && obj.__tad[1] === obj.__tau[1]) {
  95. for (var i = 0; i < obj.__taSmartClicks.length; i++)
  96. obj.__taSmartClicks[i].apply(_t(e), [ e ]);
  97. }
  98. };
  99. DefaultHandler(obj, "mousedown", down, children);
  100. DefaultHandler(obj, "mouseup", up, children);
  101. DefaultHandler(obj, "click", click, children);
  102. obj.__taSmartClicks = [];
  103. }
  104. // store in the list of callbacks
  105. obj.__taSmartClicks.push(fn);
  106. // the unstore function removes this function from the object's listener list for this type.
  107. fn.__taUnstore = function() {
  108. _d(obj.__taSmartClicks, fn);
  109. };
  110. },
  111. _tapProfiles = {
  112. "tap":{touches:1, taps:1},
  113. "dbltap":{touches:1, taps:2},
  114. "contextmenu":{touches:2, taps:1}
  115. },
  116. TapHandler = function(clickThreshold, dblClickThreshold) {
  117. return function(obj, evt, fn, children) {
  118. // if event is contextmenu, for devices which are mouse only, we want to
  119. // use the default bind.
  120. if (evt == "contextmenu" && isMouseDevice)
  121. DefaultHandler(obj, evt, fn, children);
  122. else {
  123. // the issue here is that this down handler gets registered only for the
  124. // child nodes in the first registration. in fact it should be registered with
  125. // no child selector and then on down we should cycle through the regustered
  126. // functions to see if one of them matches. on mouseup we should execute ALL of
  127. // the functions whose children are either null or match the element.
  128. if (obj.__taTapHandler == null) {
  129. var tt = obj.__taTapHandler = {
  130. tap:[],
  131. dbltap:[],
  132. contextmenu:[],
  133. down:false,
  134. taps:0,
  135. downSelectors:[]
  136. };
  137. var down = function(e) {
  138. var target = e.srcElement || e.target;
  139. for (var i = 0; i < tt.downSelectors.length; i++) {
  140. if (tt.downSelectors[i] == null || matchesSelector(target, tt.downSelectors[i], obj)) {
  141. tt.down = true;
  142. setTimeout(clearSingle, clickThreshold);
  143. setTimeout(clearDouble, dblClickThreshold);
  144. break; // we only need one match on mousedown
  145. }
  146. }
  147. },
  148. up = function(e) {
  149. if (tt.down) {
  150. var target = e.srcElement || e.target;
  151. tt.taps++;
  152. var tc = _touchCount(e);
  153. for (var eventId in _tapProfiles) {
  154. var p = _tapProfiles[eventId];
  155. if (p.touches === tc && (p.taps === 1 || p.taps === tt.taps)) {
  156. for (var i = 0; i < tt[eventId].length; i++) {
  157. if (tt[eventId][i][1] == null || matchesSelector(target, tt[eventId][i][1], obj))
  158. tt[eventId][i][0].apply(_t(e), [ e ]);
  159. }
  160. }
  161. }
  162. }
  163. },
  164. clearSingle = function() {
  165. tt.down = false;
  166. },
  167. clearDouble = function() {
  168. tt.taps = 0;
  169. };
  170. DefaultHandler(obj, "mousedown", down/*, children*/);
  171. DefaultHandler(obj, "mouseup", up/*, children*/);
  172. }
  173. // add this child selector (it can be null, that's fine).
  174. obj.__taTapHandler.downSelectors.push(children);
  175. obj.__taTapHandler[evt].push([fn, children]);
  176. // the unstore function removes this function from the object's listener list for this type.
  177. fn.__taUnstore = function() {
  178. _d(obj.__taTapHandler[evt], fn);
  179. };
  180. }
  181. };
  182. },
  183. meeHelper = function(type, evt, obj, target) {
  184. for (var i in obj.__tamee[type]) {
  185. obj.__tamee[type][i].apply(target, [ evt ]);
  186. }
  187. },
  188. MouseEnterExitHandler = function() {
  189. var activeElements = [];
  190. return function(obj, evt, fn, children) {
  191. if (!obj.__tamee) {
  192. // __tamee holds a flag saying whether the mouse is currently "in" the element, and a list of
  193. // both mouseenter and mouseexit functions.
  194. obj.__tamee = { over:false, mouseenter:[], mouseexit:[] };
  195. // register over and out functions
  196. var over = function(e) {
  197. var t = _t(e);
  198. if ( (children== null && (t == obj && !obj.__tamee.over)) || (matchesSelector(t, children, obj) && (t.__tamee == null || !t.__tamee.over)) ) {
  199. meeHelper("mouseenter", e, obj, t);
  200. t.__tamee = t.__tamee || {};
  201. t.__tamee.over = true;
  202. activeElements.push(t);
  203. }
  204. },
  205. out = function(e) {
  206. var t = _t(e);
  207. // is the current target one of the activeElements? and is the
  208. // related target NOT a descendant of it?
  209. for (var i = 0; i < activeElements.length; i++) {
  210. if (t == activeElements[i] && !matchesSelector((e.relatedTarget || e.toElement), "*", t)) {
  211. t.__tamee.over = false;
  212. activeElements.splice(i, 1);
  213. meeHelper("mouseexit", e, obj, t);
  214. }
  215. }
  216. };
  217. _bind(obj, "mouseover", _curryChildFilter(children, obj, over, "mouseover"), over);
  218. _bind(obj, "mouseout", _curryChildFilter(children, obj, out, "mouseout"), out);
  219. }
  220. fn.__taUnstore = function() {
  221. delete obj.__tamee[evt][fn.__tauid];
  222. };
  223. _store(obj, evt, fn);
  224. obj.__tamee[evt][fn.__tauid] = fn;
  225. };
  226. },
  227. isTouchDevice = "ontouchstart" in document.documentElement,
  228. isMouseDevice = "onmousedown" in document.documentElement,
  229. touchMap = { "mousedown":"touchstart", "mouseup":"touchend", "mousemove":"touchmove" },
  230. touchstart="touchstart",touchend="touchend",touchmove="touchmove",
  231. ta_down = "__MottleDown", ta_up = "__MottleUp",
  232. ta_context_down = "__MottleContextDown", ta_context_up = "__MottleContextUp",
  233. iev = (function() {
  234. var rv = -1;
  235. if (navigator.appName == 'Microsoft Internet Explorer') {
  236. var ua = navigator.userAgent,
  237. re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
  238. if (re.exec(ua) != null)
  239. rv = parseFloat(RegExp.$1);
  240. }
  241. return rv;
  242. })(),
  243. isIELT9 = iev > -1 && iev < 9,
  244. _genLoc = function(e, prefix) {
  245. if (e == null) return [ 0, 0 ];
  246. var ts = _touches(e), t = _getTouch(ts, 0);
  247. return [t[prefix + "X"], t[prefix + "Y"]];
  248. },
  249. _pageLocation = function(e) {
  250. if (e == null) return [ 0, 0 ];
  251. if (isIELT9) {
  252. return [ e.clientX + document.documentElement.scrollLeft, e.clientY + document.documentElement.scrollTop ];
  253. }
  254. else {
  255. return _genLoc(e, "page");
  256. }
  257. },
  258. _screenLocation = function(e) {
  259. return _genLoc(e, "screen");
  260. },
  261. _clientLocation = function(e) {
  262. return _genLoc(e, "client");
  263. },
  264. _getTouch = function(touches, idx) { return touches.item ? touches.item(idx) : touches[idx]; },
  265. _touches = function(e) {
  266. return e.touches && e.touches.length > 0 ? e.touches :
  267. e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches :
  268. e.targetTouches && e.targetTouches.length > 0 ? e.targetTouches :
  269. [ e ];
  270. },
  271. _touchCount = function(e) { return _touches(e).length; },
  272. //http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html
  273. _bind = function( obj, type, fn, originalFn) {
  274. _store(obj, type, fn);
  275. originalFn.__tauid = fn.__tauid;
  276. if (obj.addEventListener)
  277. obj.addEventListener( type, fn, false );
  278. else if (obj.attachEvent) {
  279. var key = type + fn.__tauid;
  280. obj["e" + key] = fn;
  281. // TODO look at replacing with .call(..)
  282. obj[key] = function() {
  283. obj["e"+key] && obj["e"+key]( window.event );
  284. };
  285. obj.attachEvent( "on"+type, obj[key] );
  286. }
  287. },
  288. _unbind = function( obj, type, fn) {
  289. if (fn == null) return;
  290. _each(obj, function() {
  291. var _el = _gel(this);
  292. _unstore(_el, type, fn);
  293. // it has been bound if there is a tauid. otherwise it was not bound and we can ignore it.
  294. if (fn.__tauid != null) {
  295. if (_el.removeEventListener)
  296. _el.removeEventListener( type, fn, false );
  297. else if (this.detachEvent) {
  298. var key = type + fn.__tauid;
  299. _el[key] && _el.detachEvent( "on"+type, _el[key] );
  300. _el[key] = null;
  301. _el["e"+key] = null;
  302. }
  303. }
  304. });
  305. },
  306. _devNull = function() {},
  307. _each = function(obj, fn) {
  308. if (obj == null) return;
  309. // if a list (or list-like), use it. if a string, get a list
  310. // by running the string through querySelectorAll. else, assume
  311. // it's an Element.
  312. obj = (typeof obj !== "string") && (obj.tagName == null && obj.length != null) ? obj : typeof obj === "string" ? document.querySelectorAll(obj) : [ obj ];
  313. for (var i = 0; i < obj.length; i++)
  314. fn.apply(obj[i]);
  315. };
  316. /**
  317. * Event handler. Offers support for abstracting out the differences
  318. * between touch and mouse devices, plus "smart click" functionality
  319. * (don't fire click if the mouse has moved betweeb mousedown and mouseup),
  320. * and synthesized click/tap events.
  321. * @class Mottle
  322. * @constructor
  323. * @param {Object} params Constructor params
  324. * @param {Integer} [params.clickThreshold=150] Threshold, in milliseconds beyond which a touchstart followed by a touchend is not considered to be a click.
  325. * @param {Integer} [params.dblClickThreshold=350] Threshold, in milliseconds beyond which two successive tap events are not considered to be a click.
  326. * @param {Boolean} [params.smartClicks=false] If true, won't fire click events if the mouse has moved between mousedown and mouseup. Note that this functionality
  327. * requires that Mottle consume the mousedown event, and so may not be viable in all use cases.
  328. */
  329. this.Mottle = function(params) {
  330. params = params || {};
  331. var self = this,
  332. clickThreshold = params.clickThreshold || 150,
  333. dblClickThreshold = params.dblClickThreshold || 350,
  334. mouseEnterExitHandler = new MouseEnterExitHandler(),
  335. tapHandler = new TapHandler(clickThreshold, dblClickThreshold),
  336. _smartClicks = params.smartClicks,
  337. _doBind = function(obj, evt, fn, children) {
  338. if (fn == null) return;
  339. _each(obj, function() {
  340. var _el = _gel(this);
  341. if (_smartClicks && evt === "click")
  342. SmartClickHandler(_el, evt, fn, children);
  343. else if (evt === "tap" || evt === "dbltap" || evt === "contextmenu") {
  344. tapHandler(_el, evt, fn, children);
  345. }
  346. else if (evt === "mouseenter" || evt == "mouseexit")
  347. mouseEnterExitHandler(_el, evt, fn, children);
  348. else
  349. DefaultHandler(_el, evt, fn, children);
  350. });
  351. };
  352. /**
  353. * Removes an element from the DOM, and unregisters all event handlers for it. You should use this
  354. * to ensure you don't leak memory.
  355. * @method remove
  356. * @param {String|Element} el Element, or id of the element, to remove.
  357. * @return {Mottle} The current Mottle instance; you can chain this method.
  358. */
  359. this.remove = function(el) {
  360. _each(el, function() {
  361. var _el = _gel(this);
  362. if (_el.__ta) {
  363. for (var evt in _el.__ta) {
  364. for (var h in _el.__ta[evt]) {
  365. _unbind(_el, evt, _el.__ta[evt][h]);
  366. }
  367. }
  368. }
  369. _el.parentNode && _el.parentNode.removeChild(_el);
  370. });
  371. return this;
  372. };
  373. /**
  374. * Register an event handler, optionally as a delegate for some set of descendant elements. Note
  375. * that this method takes either 3 or 4 arguments - if you supply 3 arguments it is assumed you have
  376. * omitted the `children` parameter, and that the event handler should be bound directly to the given element.
  377. * @method on
  378. * @param {Element[]|Element|String} el Either an Element, or a CSS spec for a list of elements, or an array of Elements.
  379. * @param {String} [children] Comma-delimited list of selectors identifying allowed children.
  380. * @param {String} event Event ID.
  381. * @param {Function} fn Event handler function.
  382. * @return {Mottle} The current Mottle instance; you can chain this method.
  383. */
  384. this.on = function(el, event, children, fn) {
  385. var _el = arguments[0],
  386. _c = arguments.length == 4 ? arguments[2] : null,
  387. _e = arguments[1],
  388. _f = arguments[arguments.length - 1];
  389. _doBind(_el, _e, _f, _c);
  390. return this;
  391. };
  392. /**
  393. * Cancel delegate event handling for the given function. Note that unlike with 'on' you do not supply
  394. * a list of child selectors here: it removes event delegation from all of the child selectors for which the
  395. * given function was registered (if any).
  396. * @method off
  397. * @param {Element[]|Element|String} el Element - or ID of element - from which to remove event listener.
  398. * @param {String} event Event ID.
  399. * @param {Function} fn Event handler function.
  400. * @return {Mottle} The current Mottle instance; you can chain this method.
  401. */
  402. this.off = function(el, evt, fn) {
  403. _unbind(el, evt, fn);
  404. return this;
  405. };
  406. /**
  407. * Triggers some event for a given element.
  408. * @method trigger
  409. * @param {Element} el Element for which to trigger the event.
  410. * @param {String} event Event ID.
  411. * @param {Event} originalEvent The original event. Should be optional of course, but currently is not, due
  412. * to the jsPlumb use case that caused this method to be added.
  413. * @param {Object} [payload] Optional object to set as `payload` on the generated event; useful for message passing.
  414. * @return {Mottle} The current Mottle instance; you can chain this method.
  415. */
  416. this.trigger = function(el, event, originalEvent, payload) {
  417. var eventToBind = (isTouchDevice && touchMap[event]) ? touchMap[event] : event;
  418. var pl = _pageLocation(originalEvent), sl = _screenLocation(originalEvent), cl = _clientLocation(originalEvent);
  419. _each(el, function() {
  420. var _el = _gel(this), evt;
  421. originalEvent = originalEvent || {
  422. screenX:sl[0],
  423. screenY:sl[1],
  424. clientX:cl[0],
  425. clientY:cl[1]
  426. };
  427. var _decorate = function(_evt) {
  428. if (payload) _evt.payload = payload;
  429. };
  430. var eventGenerators = {
  431. "TouchEvent":function(evt) {
  432. var t = document.createTouch(window, _el, 0, pl[0], pl[1],
  433. sl[0], sl[1],
  434. cl[0], cl[1],
  435. 0,0,0,0);
  436. evt.initTouchEvent(eventToBind, true, true, window, 0,
  437. sl[0], sl[1],
  438. cl[0], cl[1],
  439. false, false, false, false, document.createTouchList(t));
  440. },
  441. "MouseEvents":function(evt) {
  442. evt.initMouseEvent(eventToBind, true, true, window, 0,
  443. sl[0], sl[1],
  444. cl[0], cl[1],
  445. false, false, false, false, 1, _el);
  446. if (Sniff.android) {
  447. // Android's touch events are not standard.
  448. var t = document.createTouch(window, _el, 0, pl[0], pl[1],
  449. sl[0], sl[1],
  450. cl[0], cl[1],
  451. 0,0,0,0);
  452. evt.touches = evt.targetTouches = evt.changedTouches = document.createTouchList(t);
  453. }
  454. }
  455. };
  456. if (document.createEvent) {
  457. var ite = (isTouchDevice && touchMap[event] && !Sniff.android), evtName = ite ? "TouchEvent" : "MouseEvents";
  458. evt = document.createEvent(evtName);
  459. eventGenerators[evtName](evt);
  460. _decorate(evt);
  461. _el.dispatchEvent(evt);
  462. }
  463. else if (document.createEventObject) {
  464. evt = document.createEventObject();
  465. evt.eventType = evt.eventName = eventToBind;
  466. evt.screenX = sl[0];
  467. evt.screenY = sl[1];
  468. evt.clientX = cl[0];
  469. evt.clientY = cl[1];
  470. _decorate(evt);
  471. _el.fireEvent('on' + eventToBind, evt);
  472. }
  473. });
  474. return this;
  475. }
  476. };
  477. /**
  478. * Static method to assist in 'consuming' an element: uses `stopPropagation` where available, or sets `e.returnValue=false` where it is not.
  479. * @method Mottle.consume
  480. * @param {Event} e Event to consume
  481. * @param {Boolean} [doNotPreventDefault=false] If true, does not call `preventDefault()` on the event.
  482. */
  483. Mottle.consume = function(e, doNotPreventDefault) {
  484. if (e.stopPropagation)
  485. e.stopPropagation();
  486. else
  487. e.returnValue = false;
  488. if (!doNotPreventDefault && e.preventDefault)
  489. e.preventDefault();
  490. };
  491. /**
  492. * Gets the page location corresponding to the given event. For touch events this means get the page location of the first touch.
  493. * @method Mottle.pageLocation
  494. * @param {Event} e Event to get page location for.
  495. * @return {Integer[]} [left, top] for the given event.
  496. */
  497. Mottle.pageLocation = _pageLocation;
  498. }).call(this);