UNPKG

todomvc

Version:

> Helping you select an MV\* framework

628 lines (570 loc) 17.9 kB
/*! * CanJS - 2.0.3 * http://canjs.us/ * Copyright (c) 2013 Bitovi * Tue, 26 Nov 2013 18:21:22 GMT * Licensed MIT * Includes: CanJS default build * Download from: http://canjs.us/ */ define(["can/util/can", "dojo", "can/util/event", "can/util/fragment", "can/util/array/each", "can/util/object/isplain", "can/util/deferred", "can/util/hashchange", "can/util/inserted"], function(can) { define("plugd/trigger", ["dojo"], function( dojo ) { var d = dojo, isfn = d.isFunction, leaveRe = /mouse(enter|leave)/, _fix = function( _, p ) { return "mouse" + (p == "enter" ? "over" : "out"); }, mix = d._mixin, // the guts of the node triggering logic: // the function accepts node (not string|node), "on"-less event name, // and an object of args to mix into the event. realTrigger = d.doc.createEvent ? function( n, e, a ) { // the sane branch var ev = d.doc.createEvent("HTMLEvents"); e = e.replace(leaveRe, _fix); // removed / inserted events should not bubble ev.initEvent(e, e === "removed" || e === "inserted"? false : true, true); a && mix(ev, a); n.dispatchEvent(ev); } : function( n, e, a ) { // the janktastic branch var ev = "on" + e, stop = false, lc = e.toLowerCase(), node = n; try { // FIXME: is this worth it? for mixed-case native event support:? Opera ends up in the // createEvent path above, and also fails on _some_ native-named events. // if(lc !== e && d.indexOf(d.NodeList.events, lc) >= 0){ // // if the event is one of those listed in our NodeList list // // in lowercase form but is mixed case, throw to avoid // // fireEvent. /me sighs. http://gist.github.com/315318 // throw("janktastic"); // } var evObj = document.createEventObject(); mix(evObj, a); n.fireEvent(ev, evObj); } catch (er) { // a lame duck to work with. we're probably a 'custom event' var evdata = mix({ type: e, target: n, faux: true, // HACK: [needs] added support for customStopper to _base/event.js // some tests will fail until del._stopPropagation has support. _stopper: function() { stop = this.cancelBubble; } }, a); isfn(n[ev]) && n[ev](evdata); // handle bubbling of custom events, unless the event was stopped. while (!stop && n !== d.doc && n.parentNode ) { n = n.parentNode; isfn(n[ev]) && n[ev](evdata); } } }; d._trigger = function( node, event, extraArgs ) { if(typeof event !== "string"){ extraArgs = event; event=extraArgs.type; delete extraArgs.type; } // summary: // Helper for `dojo.trigger`, which handles the DOM cases. We should never // be here without a domNode reference and a string eventname. var n = d.byId(node), ev = event && event.slice(0, 2) == "on" ? event.slice(2) : event; realTrigger(n, ev, extraArgs); }; d.trigger = function( obj, event, extraArgs ) { // summary: // Trigger some event. It can be either a Dom Event, Custom Event, // or direct function call. // // description: // Trigger some event. It can be either a Dom Event, Custom Event, // or direct function call. NOTE: This function does not trigger // default behavior, only triggers bound event listeneres. eg: // one cannot trigger("anchorNode", "onclick") and expect the browser // to follow the href="" attribute naturally. // // obj: String|DomNode|Object|Function // An ID, or DomNode reference, from which to trigger the event. // If an Object, fire the `event` in the scope of this object, // similar to calling dojo.hitch(obj, event)(). The return value // in this case is returned from `dojo.trigger` // // event: String|Function // The name of the event to trigger. can be any DOM level 2 event // and can be in either form: "onclick" or "click" for instance. // In the object-firing case, this method can be a function or // a string version of a member function, just like `dojo.hitch`. // // extraArgs: Object? // An object to mix into the `event` object passed to any bound // listeners. Be careful not to override important members, like // `type`, or `preventDefault`. It will likely error. // // Additionally, extraArgs is moot in the object-triggering case, // as all arguments beyond the `event` are curried onto the triggered // function. // // example: // | dojo.connect(node, "onclick", function(e){ /* stuff */ }); // | // later: // | dojo.trigger(node, "onclick"); // // example: // | // or from within dojo.query: (requires dojo.NodeList) // | dojo.query("a").onclick(function(){}).trigger("onclick"); // // example: // | // fire obj.method() in scope of obj // | dojo.trigger(obj, "method"); // // example: // | // fire an anonymous function: // | dojo.trigger(d.global, function(){ /* stuff */ }); // // example: // | // fire and anonymous function in the scope of obj // | dojo.trigger(obj, function(){ this == obj; }); // // example: // | // with a connected function like: // | dojo.connect(dojo.doc, "onclick", function(e){ // | if(e && e.manuallydone){ // | console.log("this was a triggered onclick, not natural"); // | } // | }); // | // fire onclick, passing in a custom bit of info // | dojo.trigger("someId", "onclick", { manuallydone:true }); // // returns: Anything // Will not return anything in the Dom event case, but will return whatever // return value is received from the triggered event. return (isfn(obj) || isfn(event) || isfn(obj[event])) ? d.hitch.apply(d, arguments)() : d._trigger.apply(d, arguments); }; d.NodeList.prototype.trigger = d.NodeList._adaptAsForEach(d._trigger); // if the node.js module is available, extend trigger into that. if ( d._Node && !d._Node.prototype.trigger ) { d.extend(d._Node, { trigger: function( ev, data ) { // summary: // Fire some some event originating from this node. // Only available if both the `dojo.trigger` and `dojo.node` plugin // are enabled. Allows chaining as all `dojo._Node` methods do. // // ev: String // Some string event name to fire. eg: "onclick", "submit" // // data: Object // Just like `extraArgs` for `dojo.trigger`, additional data // to mix into the event object. // // example: // | // fire onlick orginiating from a node with id="someAnchorId" // | dojo.node("someAnchorId").trigger("click"); d._trigger(this, ev, data); return this; // dojo._Node } }); } return d.trigger; }); // dojo.js // --------- // _dojo node list._ // // These are pre-loaded by `steal` -> no callback. require(["dojo", "dojo/query", "plugd/trigger", "dojo/NodeList-dom"]); // Map string helpers. can.trim = function( s ) { return s && dojo.trim(s); } // Map array helpers. can.makeArray = function( arr ) { var array = []; dojo.forEach(arr, function( item ) { array.push(item) }); return array; }; can.isArray = dojo.isArray; can.inArray = function( item, arr, from ) { return dojo.indexOf(arr, item, from); }; can.map = function( arr, fn ) { return dojo.map(can.makeArray(arr || []), fn); }; // Map object helpers. can.extend = function( first ) { if ( first === true ) { var args = can.makeArray(arguments); args.shift(); return dojo.mixin.apply(dojo, args) } return dojo.mixin.apply(dojo, arguments) } can.isEmptyObject = function( object ) { var prop; for ( prop in object ) { break; } return prop === undefined;; } // Use a version of param similar to jQuery's param that // handles nested data instead of dojo.objectToQuery which doesn't can.param = function( object ) { var pairs = [], add = function( key, value ) { pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)) }; for ( var name in object ) { can.buildParam(name, object[name], add); } return pairs.join("&").replace(/%20/g, "+"); } can.buildParam = function( prefix, obj, add ) { if ( can.isArray(obj) ) { for ( var i = 0, l = obj.length; i < l; ++i ) { add(prefix + "[]", obj[i]) } } else if ( dojo.isObject(obj) ) { for ( var name in obj ) { can.buildParam(prefix + "[" + name + "]", obj[name], add); } } else { add(prefix, obj); } } // Map function helpers. can.proxy = function( func, context ) { return dojo.hitch(context, func) } can.isFunction = function( f ) { return dojo.isFunction(f); } /** * EVENTS * * Dojo does not use the callback handler when unbinding. Instead * when binding (dojo.connect or dojo.on) an object with a remove * method is returned. * * Because of this, we have to map each callback to the "remove" * object to it can be passed to dojo.disconnect. */ // The id of the `function` to be bound, used as an expando on the `function` // so we can lookup it's `remove` object. var dojoId = 0, // Takes a node list, goes through each node // and adds events data that has a map of events to // callbackId to `remove` object. It looks like // `{click: {5: {remove: fn}}}`. dojoAddBinding = function( nodelist, ev, cb ) { nodelist.forEach(function( node ) { // Converting a raw select node to a node list // returns a node list of its options due to a // bug in Dojo 1.7.1, this is sovled by wrapping // it in an array. node = new dojo.NodeList(node.nodeName === "SELECT" ? [node] : node) var events = can.data(node, "events"); if (!events ) { can.data(node, "events", events = {}) } if (!events[ev] ) { events[ev] = {}; } if ( cb.__bindingsIds === undefined ) { cb.__bindingsIds = dojoId++; } events[ev][cb.__bindingsIds] = node.on(ev, cb)[0] }); }, // Removes a binding on a `nodelist` by finding // the remove object within the object's data. dojoRemoveBinding = function( nodelist, ev, cb ) { nodelist.forEach(function( node ) { var node = new dojo.NodeList(node), events = can.data(node, "events"); if(!events){ return } var handlers = events[ev]; if(!handlers){ return } var handler = handlers[cb.__bindingsIds]; dojo.disconnect(handler); delete handlers[cb.__bindingsIds]; if ( can.isEmptyObject(handlers) ) { delete events[ev] } }); } can.bind = function( ev, cb ) { // If we can bind to it... if ( this.bind && this.bind !== can.bind ) { this.bind(ev, cb) // Otherwise it's an element or `nodeList`. } else if ( this.on || this.nodeType ) { // Converting a raw select node to a node list // returns a node list of its options due to a // bug in Dojo 1.7.1, this is sovled by wrapping // it in an array. dojoAddBinding(new dojo.NodeList(this.nodeName === "SELECT" ? [this] : this), ev, cb) } else if ( this.addEvent ) { this.addEvent(ev, cb) } else { // Make it bind-able... can.addEvent.call(this, ev, cb) } return this; } can.unbind = function( ev, cb ) { // If we can bind to it... if ( this.unbind && this.unbind !== can.unbind ) { this.unbind(ev, cb) } else if ( this.on || this.nodeType ) { dojoRemoveBinding(new dojo.NodeList(this), ev, cb); } else { // Make it bind-able... can.removeEvent.call(this, ev, cb) } return this; } // Alias on/off to bind/unbind respectively can.on = can.bind; can.off = can.unbind; can.trigger = function( item, event, args, bubble ) { if ((!(item instanceof dojo.NodeList)) && (item.nodeName || item === window)) { item = can.$(item); } if ( item.trigger ) { if ( bubble === false ) { if (!item[0] || item[0].nodeType === 3 ) { return; } // Force stop propagation by // listening to `on` and then immediately disconnecting. var connect = item.on(event, function( ev ) { ev.stopPropagation && ev.stopPropagation(); ev.cancelBubble = true; ev._stopper && ev._stopper(); dojo.disconnect(connect); }) item.trigger(event, args) } else { item.trigger(event, args) } } else { if ( typeof event === 'string' ) { event = { type: event } } event.target = event.target || item; can.dispatch.call(item, event, args) } } can.delegate = function( selector, ev, cb ) { if ( this.on || this.nodeType ) { dojoAddBinding(new dojo.NodeList(this), selector + ":" + ev, cb) } else if ( this.delegate ) { this.delegate(selector, ev, cb) } return this; } can.undelegate = function( selector, ev, cb ) { if ( this.on || this.nodeType ) { dojoRemoveBinding(new dojo.NodeList(this), selector + ":" + ev, cb); } else if ( this.undelegate ) { this.undelegate(selector, ev, cb) } return this; } /** * Ajax */ var optionsMap = { type: "method", success: undefined, error: undefined } var updateDeferred = function( xhr, d ) { for ( var prop in xhr ) { if ( typeof d[prop] == 'function' ) { d[prop] = function() { xhr[prop].apply(xhr, arguments) } } else { d[prop] = prop[xhr] } } } can.ajax = function( options ) { var type = can.capitalize((options.type || "get").toLowerCase()), method = dojo["xhr" + type]; var success = options.success, error = options.error, d = new can.Deferred(); var def = method({ url: options.url, handleAs: options.dataType, sync: !options.async, headers: options.headers, content: options.data }) def.then(function( data, ioargs ) { updateDeferred(xhr, d); d.resolve(data, "success", xhr); success && success(data, "success", xhr); }, function( data, ioargs ) { updateDeferred(xhr, d); d.reject(xhr, "error"); error(xhr, "error"); }) var xhr = def.ioArgs.xhr; updateDeferred(xhr, d); return d; } // Element - get the wrapped helper. can.$ = function( selector ) { if ( selector === window ) { return window; } if ( typeof selector === "string" ) { return dojo.query(selector) } else { return new dojo.NodeList(selector); } } can.append = function( wrapped, html ) { return wrapped.forEach(function( node ) { dojo.place(html, node) }); } /** * can.data * * can.data is used to store arbitrary data on an element. * Dojo does not support this, so we implement it itself. * * The important part is to call cleanData on any elements * that are removed from the DOM. For this to happen, we * overwrite * * -dojo.empty * -dojo.destroy * -dojo.place when "replace" is used TODO!!!! * * For can.Control, we also need to trigger a non bubbling event * when an element is removed. We do this also in cleanData. */ var data = {}, uuid = can.uuid = +new Date(), exp = can.expando = 'can' + uuid; function getData(node, name) { var id = node[exp], store = id && data[id]; return name === undefined ? store || setData(node) : (store && store[name]); } function setData(node, name, value) { var id = node[exp] || (node[exp] = ++uuid), store = data[id] || (data[id] = {}); if ( name !== undefined ) store[name] = value; return store; }; var cleanData = function( elems ) { can.trigger(new dojo.NodeList(elems), "removed", [], false) for ( var i = 0, elem; (elem = elems[i]) !== undefined; i++ ) { var id = elem[exp] delete data[id]; } }; can.data = function( wrapped, name, value ) { return value === undefined ? wrapped.length == 0 ? undefined : getData(wrapped[0], name) : wrapped.forEach(function( node ) { setData(node, name, value); }); }; // Overwrite `dojo.destroy`, `dojo.empty` and `dojo.place`. var empty = dojo.empty; dojo.empty = function() { for ( var c; c = node.lastChild; ) { // Intentional assignment. dojo.destroy(c); } } var destroy = dojo.destroy; dojo.destroy = function( node ) { node = dojo.byId(node); // we must call clean data at one time var nodes = [node]; node.getElementsByTagName && nodes.concat(can.makeArray(node.getElementsByTagName('*'))) cleanData(nodes); return destroy.apply(dojo, arguments); }; var place = dojo.place; dojo.place = function(/*DOMNode|String*/ node, /*DOMNode|String*/ refNode, /*String|Number?*/ position){ if(typeof node === "string" && /^\s*</.test(node)){ node = can.buildFragment(node); } var elems; if( node.nodeType === 11 ) { elems = can.makeArray(node.childNodes); } else { elems = [node] } var ret = place.call(this, node, refNode, position); can.inserted( elems ); return ret; } can.addClass = function( wrapped, className ) { return wrapped.addClass(className); } // removes a NodeList ... but it doesn't seem like dojo's NodeList has a destroy method? can.remove = function( wrapped ) { // We need to remove text nodes ourselves. var nodes = []; wrapped.forEach(function(node){ nodes.push(node); node.getElementsByTagName && nodes.push.apply(nodes,can.makeArray(node.getElementsByTagName('*'))) }) cleanData(nodes); wrapped.forEach(destroy); return wrapped; } can.get = function( wrapped, index ) { return wrapped[index]; } can.has = function(wrapped, element){ if( dojo.isDescendant(element, wrapped[0]) ){ return wrapped } else { return [] } } // Add pipe to `dojo.Deferred`. can.extend(dojo.Deferred.prototype, { pipe: function( done, fail ) { var d = new dojo.Deferred(); this.addCallback(function() { d.resolve(done.apply(this, arguments)); }); this.addErrback(function() { if ( fail ) { d.reject(fail.apply(this, arguments)); } else { d.reject.apply(d, arguments); } }); return d; } }); return can; });