123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- ;(function() {
- "use strict";
- var Sniff = {
- android:navigator.userAgent.toLowerCase().indexOf("android") > -1
- };
- var matchesSelector = function(el, selector, ctx) {
- ctx = ctx || el.parentNode;
- var possibles = ctx.querySelectorAll(selector);
- for (var i = 0; i < possibles.length; i++) {
- if (possibles[i] === el) {
- return true;
- }
- }
- return false;
- },
- _gel = function(el) { return typeof el == "string" ? document.getElementById(el) : el; },
- _t = function(e) { return e.srcElement || e.target; },
- _d = function(l, fn) {
- for (var i = 0, j = l.length; i < j; i++) {
- if (l[i] == fn) break;
- }
- if (i < l.length) l.splice(i, 1);
- },
- guid = 1,
- //
- // this function generates a guid for every handler, sets it on the handler, then adds
- // it to the associated object's map of handlers for the given event. this is what enables us
- // to unbind all events of some type, or all events (the second of which can be requested by the user,
- // but it also used by Mottle when an element is removed.)
- _store = function(obj, event, fn) {
- var g = guid++;
- obj.__ta = obj.__ta || {};
- obj.__ta[event] = obj.__ta[event] || {};
- // store each handler with a unique guid.
- obj.__ta[event][g] = fn;
- // set the guid on the handler.
- fn.__tauid = g;
- return g;
- },
- _unstore = function(obj, event, fn) {
- obj.__ta && obj.__ta[event] && delete obj.__ta[event][fn.__tauid];
- // a handler might have attached extra functions, so we unbind those too.
- if (fn.__taExtra) {
- for (var i = 0; i < fn.__taExtra.length; i++) {
- _unbind(obj, fn.__taExtra[i][0], fn.__taExtra[i][1]);
- }
- fn.__taExtra.length = 0;
- }
- // a handler might have attached an unstore callback
- fn.__taUnstore && fn.__taUnstore();
- },
- _curryChildFilter = function(children, obj, fn, evt) {
- if (children == null) return fn;
- else {
- var c = children.split(","),
- _fn = function(e) {
- _fn.__tauid = fn.__tauid;
- var t = _t(e);
- for (var i = 0; i < c.length; i++) {
- if (matchesSelector(t, c[i], obj)) {
- fn.apply(t, arguments);
- }
- }
- };
- registerExtraFunction(fn, evt, _fn);
- return _fn;
- }
- },
- //
- // registers an 'extra' function on some event listener function we were given - a function that we
- // created and bound to the element as part of our housekeeping, and which we want to unbind and remove
- // whenever the given function is unbound.
- registerExtraFunction = function(fn, evt, newFn) {
- fn.__taExtra = fn.__taExtra || [];
- fn.__taExtra.push([evt, newFn]);
- },
- DefaultHandler = function(obj, evt, fn, children) {
- // 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
- // 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
- // was a device such as an Asus touch pad thing, which has a touch pad but can also be controlled with a mouse.
- //if (isMouseDevice)
- // _bind(obj, evt, _curryChildFilter(children, obj, fn, evt), fn);
-
- if (isTouchDevice && touchMap[evt]) {
- _bind(obj, touchMap[evt], _curryChildFilter(children, obj, fn, touchMap[evt]), fn);
- }
- else
- _bind(obj, evt, _curryChildFilter(children, obj, fn, evt), fn);
- },
- SmartClickHandler = function(obj, evt, fn, children) {
- if (obj.__taSmartClicks == null) {
- var down = function(e) { obj.__tad = _pageLocation(e); },
- up = function(e) { obj.__tau = _pageLocation(e); },
- click = function(e) {
- if (obj.__tad && obj.__tau && obj.__tad[0] === obj.__tau[0] && obj.__tad[1] === obj.__tau[1]) {
- for (var i = 0; i < obj.__taSmartClicks.length; i++)
- obj.__taSmartClicks[i].apply(_t(e), [ e ]);
- }
- };
- DefaultHandler(obj, "mousedown", down, children);
- DefaultHandler(obj, "mouseup", up, children);
- DefaultHandler(obj, "click", click, children);
- obj.__taSmartClicks = [];
- }
-
- // store in the list of callbacks
- obj.__taSmartClicks.push(fn);
- // the unstore function removes this function from the object's listener list for this type.
- fn.__taUnstore = function() {
- _d(obj.__taSmartClicks, fn);
- };
- },
- _tapProfiles = {
- "tap":{touches:1, taps:1},
- "dbltap":{touches:1, taps:2},
- "contextmenu":{touches:2, taps:1}
- },
- TapHandler = function(clickThreshold, dblClickThreshold) {
- return function(obj, evt, fn, children) {
- // if event is contextmenu, for devices which are mouse only, we want to
- // use the default bind.
- if (evt == "contextmenu" && isMouseDevice)
- DefaultHandler(obj, evt, fn, children);
- else {
- // the issue here is that this down handler gets registered only for the
- // child nodes in the first registration. in fact it should be registered with
- // no child selector and then on down we should cycle through the regustered
- // functions to see if one of them matches. on mouseup we should execute ALL of
- // the functions whose children are either null or match the element.
- if (obj.__taTapHandler == null) {
- var tt = obj.__taTapHandler = {
- tap:[],
- dbltap:[],
- contextmenu:[],
- down:false,
- taps:0,
- downSelectors:[]
- };
- var down = function(e) {
- var target = e.srcElement || e.target;
- for (var i = 0; i < tt.downSelectors.length; i++) {
- if (tt.downSelectors[i] == null || matchesSelector(target, tt.downSelectors[i], obj)) {
- tt.down = true;
- setTimeout(clearSingle, clickThreshold);
- setTimeout(clearDouble, dblClickThreshold);
- break; // we only need one match on mousedown
- }
- }
- },
- up = function(e) {
- if (tt.down) {
- var target = e.srcElement || e.target;
- tt.taps++;
- var tc = _touchCount(e);
- for (var eventId in _tapProfiles) {
- var p = _tapProfiles[eventId];
- if (p.touches === tc && (p.taps === 1 || p.taps === tt.taps)) {
- for (var i = 0; i < tt[eventId].length; i++) {
- if (tt[eventId][i][1] == null || matchesSelector(target, tt[eventId][i][1], obj))
- tt[eventId][i][0].apply(_t(e), [ e ]);
- }
- }
- }
- }
- },
- clearSingle = function() {
- tt.down = false;
- },
- clearDouble = function() {
- tt.taps = 0;
- };
-
- DefaultHandler(obj, "mousedown", down/*, children*/);
- DefaultHandler(obj, "mouseup", up/*, children*/);
- }
- // add this child selector (it can be null, that's fine).
- obj.__taTapHandler.downSelectors.push(children);
- obj.__taTapHandler[evt].push([fn, children]);
- // the unstore function removes this function from the object's listener list for this type.
- fn.__taUnstore = function() {
- _d(obj.__taTapHandler[evt], fn);
- };
- }
- };
- },
- meeHelper = function(type, evt, obj, target) {
- for (var i in obj.__tamee[type]) {
- obj.__tamee[type][i].apply(target, [ evt ]);
- }
- },
- MouseEnterExitHandler = function() {
- var activeElements = [];
- return function(obj, evt, fn, children) {
- if (!obj.__tamee) {
- // __tamee holds a flag saying whether the mouse is currently "in" the element, and a list of
- // both mouseenter and mouseexit functions.
- obj.__tamee = { over:false, mouseenter:[], mouseexit:[] };
- // register over and out functions
- var over = function(e) {
- var t = _t(e);
- if ( (children== null && (t == obj && !obj.__tamee.over)) || (matchesSelector(t, children, obj) && (t.__tamee == null || !t.__tamee.over)) ) {
- meeHelper("mouseenter", e, obj, t);
- t.__tamee = t.__tamee || {};
- t.__tamee.over = true;
- activeElements.push(t);
- }
- },
- out = function(e) {
- var t = _t(e);
- // is the current target one of the activeElements? and is the
- // related target NOT a descendant of it?
- for (var i = 0; i < activeElements.length; i++) {
- if (t == activeElements[i] && !matchesSelector((e.relatedTarget || e.toElement), "*", t)) {
- t.__tamee.over = false;
- activeElements.splice(i, 1);
- meeHelper("mouseexit", e, obj, t);
- }
- }
- };
-
- _bind(obj, "mouseover", _curryChildFilter(children, obj, over, "mouseover"), over);
- _bind(obj, "mouseout", _curryChildFilter(children, obj, out, "mouseout"), out);
- }
- fn.__taUnstore = function() {
- delete obj.__tamee[evt][fn.__tauid];
- };
- _store(obj, evt, fn);
- obj.__tamee[evt][fn.__tauid] = fn;
- };
- },
- isTouchDevice = "ontouchstart" in document.documentElement,
- isMouseDevice = "onmousedown" in document.documentElement,
- touchMap = { "mousedown":"touchstart", "mouseup":"touchend", "mousemove":"touchmove" },
- touchstart="touchstart",touchend="touchend",touchmove="touchmove",
- ta_down = "__MottleDown", ta_up = "__MottleUp",
- ta_context_down = "__MottleContextDown", ta_context_up = "__MottleContextUp",
- iev = (function() {
- var rv = -1;
- if (navigator.appName == 'Microsoft Internet Explorer') {
- var ua = navigator.userAgent,
- re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
- if (re.exec(ua) != null)
- rv = parseFloat(RegExp.$1);
- }
- return rv;
- })(),
- isIELT9 = iev > -1 && iev < 9,
- _genLoc = function(e, prefix) {
- if (e == null) return [ 0, 0 ];
- var ts = _touches(e), t = _getTouch(ts, 0);
- return [t[prefix + "X"], t[prefix + "Y"]];
- },
- _pageLocation = function(e) {
- if (e == null) return [ 0, 0 ];
- if (isIELT9) {
- return [ e.clientX + document.documentElement.scrollLeft, e.clientY + document.documentElement.scrollTop ];
- }
- else {
- return _genLoc(e, "page");
- }
- },
- _screenLocation = function(e) {
- return _genLoc(e, "screen");
- },
- _clientLocation = function(e) {
- return _genLoc(e, "client");
- },
- _getTouch = function(touches, idx) { return touches.item ? touches.item(idx) : touches[idx]; },
- _touches = function(e) {
- return e.touches && e.touches.length > 0 ? e.touches :
- e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches :
- e.targetTouches && e.targetTouches.length > 0 ? e.targetTouches :
- [ e ];
- },
- _touchCount = function(e) { return _touches(e).length; },
- //http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html
- _bind = function( obj, type, fn, originalFn) {
- _store(obj, type, fn);
- originalFn.__tauid = fn.__tauid;
- if (obj.addEventListener)
- obj.addEventListener( type, fn, false );
- else if (obj.attachEvent) {
- var key = type + fn.__tauid;
- obj["e" + key] = fn;
- // TODO look at replacing with .call(..)
- obj[key] = function() {
- obj["e"+key] && obj["e"+key]( window.event );
- };
- obj.attachEvent( "on"+type, obj[key] );
- }
- },
- _unbind = function( obj, type, fn) {
- if (fn == null) return;
- _each(obj, function() {
- var _el = _gel(this);
- _unstore(_el, type, fn);
- // it has been bound if there is a tauid. otherwise it was not bound and we can ignore it.
- if (fn.__tauid != null) {
- if (_el.removeEventListener)
- _el.removeEventListener( type, fn, false );
- else if (this.detachEvent) {
- var key = type + fn.__tauid;
- _el[key] && _el.detachEvent( "on"+type, _el[key] );
- _el[key] = null;
- _el["e"+key] = null;
- }
- }
- });
- },
- _devNull = function() {},
- _each = function(obj, fn) {
- if (obj == null) return;
- // if a list (or list-like), use it. if a string, get a list
- // by running the string through querySelectorAll. else, assume
- // it's an Element.
- obj = (typeof obj !== "string") && (obj.tagName == null && obj.length != null) ? obj : typeof obj === "string" ? document.querySelectorAll(obj) : [ obj ];
- for (var i = 0; i < obj.length; i++)
- fn.apply(obj[i]);
- };
- /**
- * Event handler. Offers support for abstracting out the differences
- * between touch and mouse devices, plus "smart click" functionality
- * (don't fire click if the mouse has moved betweeb mousedown and mouseup),
- * and synthesized click/tap events.
- * @class Mottle
- * @constructor
- * @param {Object} params Constructor params
- * @param {Integer} [params.clickThreshold=150] Threshold, in milliseconds beyond which a touchstart followed by a touchend is not considered to be a click.
- * @param {Integer} [params.dblClickThreshold=350] Threshold, in milliseconds beyond which two successive tap events are not considered to be a click.
- * @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
- * requires that Mottle consume the mousedown event, and so may not be viable in all use cases.
- */
- this.Mottle = function(params) {
- params = params || {};
- var self = this,
- clickThreshold = params.clickThreshold || 150,
- dblClickThreshold = params.dblClickThreshold || 350,
- mouseEnterExitHandler = new MouseEnterExitHandler(),
- tapHandler = new TapHandler(clickThreshold, dblClickThreshold),
- _smartClicks = params.smartClicks,
- _doBind = function(obj, evt, fn, children) {
- if (fn == null) return;
- _each(obj, function() {
- var _el = _gel(this);
- if (_smartClicks && evt === "click")
- SmartClickHandler(_el, evt, fn, children);
- else if (evt === "tap" || evt === "dbltap" || evt === "contextmenu") {
- tapHandler(_el, evt, fn, children);
- }
- else if (evt === "mouseenter" || evt == "mouseexit")
- mouseEnterExitHandler(_el, evt, fn, children);
- else
- DefaultHandler(_el, evt, fn, children);
- });
- };
- /**
- * Removes an element from the DOM, and unregisters all event handlers for it. You should use this
- * to ensure you don't leak memory.
- * @method remove
- * @param {String|Element} el Element, or id of the element, to remove.
- * @return {Mottle} The current Mottle instance; you can chain this method.
- */
- this.remove = function(el) {
- _each(el, function() {
- var _el = _gel(this);
- if (_el.__ta) {
- for (var evt in _el.__ta) {
- for (var h in _el.__ta[evt]) {
- _unbind(_el, evt, _el.__ta[evt][h]);
- }
- }
- }
- _el.parentNode && _el.parentNode.removeChild(_el);
- });
- return this;
- };
- /**
- * Register an event handler, optionally as a delegate for some set of descendant elements. Note
- * that this method takes either 3 or 4 arguments - if you supply 3 arguments it is assumed you have
- * omitted the `children` parameter, and that the event handler should be bound directly to the given element.
- * @method on
- * @param {Element[]|Element|String} el Either an Element, or a CSS spec for a list of elements, or an array of Elements.
- * @param {String} [children] Comma-delimited list of selectors identifying allowed children.
- * @param {String} event Event ID.
- * @param {Function} fn Event handler function.
- * @return {Mottle} The current Mottle instance; you can chain this method.
- */
- this.on = function(el, event, children, fn) {
- var _el = arguments[0],
- _c = arguments.length == 4 ? arguments[2] : null,
- _e = arguments[1],
- _f = arguments[arguments.length - 1];
- _doBind(_el, _e, _f, _c);
- return this;
- };
- /**
- * Cancel delegate event handling for the given function. Note that unlike with 'on' you do not supply
- * a list of child selectors here: it removes event delegation from all of the child selectors for which the
- * given function was registered (if any).
- * @method off
- * @param {Element[]|Element|String} el Element - or ID of element - from which to remove event listener.
- * @param {String} event Event ID.
- * @param {Function} fn Event handler function.
- * @return {Mottle} The current Mottle instance; you can chain this method.
- */
- this.off = function(el, evt, fn) {
- _unbind(el, evt, fn);
- return this;
- };
- /**
- * Triggers some event for a given element.
- * @method trigger
- * @param {Element} el Element for which to trigger the event.
- * @param {String} event Event ID.
- * @param {Event} originalEvent The original event. Should be optional of course, but currently is not, due
- * to the jsPlumb use case that caused this method to be added.
- * @param {Object} [payload] Optional object to set as `payload` on the generated event; useful for message passing.
- * @return {Mottle} The current Mottle instance; you can chain this method.
- */
- this.trigger = function(el, event, originalEvent, payload) {
- var eventToBind = (isTouchDevice && touchMap[event]) ? touchMap[event] : event;
- var pl = _pageLocation(originalEvent), sl = _screenLocation(originalEvent), cl = _clientLocation(originalEvent);
- _each(el, function() {
- var _el = _gel(this), evt;
- originalEvent = originalEvent || {
- screenX:sl[0],
- screenY:sl[1],
- clientX:cl[0],
- clientY:cl[1]
- };
- var _decorate = function(_evt) {
- if (payload) _evt.payload = payload;
- };
- var eventGenerators = {
- "TouchEvent":function(evt) {
- var t = document.createTouch(window, _el, 0, pl[0], pl[1],
- sl[0], sl[1],
- cl[0], cl[1],
- 0,0,0,0);
- evt.initTouchEvent(eventToBind, true, true, window, 0,
- sl[0], sl[1],
- cl[0], cl[1],
- false, false, false, false, document.createTouchList(t));
- },
- "MouseEvents":function(evt) {
- evt.initMouseEvent(eventToBind, true, true, window, 0,
- sl[0], sl[1],
- cl[0], cl[1],
- false, false, false, false, 1, _el);
-
- if (Sniff.android) {
- // Android's touch events are not standard.
- var t = document.createTouch(window, _el, 0, pl[0], pl[1],
- sl[0], sl[1],
- cl[0], cl[1],
- 0,0,0,0);
- evt.touches = evt.targetTouches = evt.changedTouches = document.createTouchList(t);
- }
- }
- };
- if (document.createEvent) {
- var ite = (isTouchDevice && touchMap[event] && !Sniff.android), evtName = ite ? "TouchEvent" : "MouseEvents";
- evt = document.createEvent(evtName);
- eventGenerators[evtName](evt);
- _decorate(evt);
- _el.dispatchEvent(evt);
- }
- else if (document.createEventObject) {
- evt = document.createEventObject();
- evt.eventType = evt.eventName = eventToBind;
- evt.screenX = sl[0];
- evt.screenY = sl[1];
- evt.clientX = cl[0];
- evt.clientY = cl[1];
- _decorate(evt);
- _el.fireEvent('on' + eventToBind, evt);
- }
- });
- return this;
- }
- };
- /**
- * Static method to assist in 'consuming' an element: uses `stopPropagation` where available, or sets `e.returnValue=false` where it is not.
- * @method Mottle.consume
- * @param {Event} e Event to consume
- * @param {Boolean} [doNotPreventDefault=false] If true, does not call `preventDefault()` on the event.
- */
- Mottle.consume = function(e, doNotPreventDefault) {
- if (e.stopPropagation)
- e.stopPropagation();
- else
- e.returnValue = false;
- if (!doNotPreventDefault && e.preventDefault)
- e.preventDefault();
- };
- /**
- * Gets the page location corresponding to the given event. For touch events this means get the page location of the first touch.
- * @method Mottle.pageLocation
- * @param {Event} e Event to get page location for.
- * @return {Integer[]} [left, top] for the given event.
- */
- Mottle.pageLocation = _pageLocation;
- }).call(this);
|