UNPKG

todomvc

Version:

> Helping you select an MV\* framework

867 lines (818 loc) 27.6 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/library", "can/construct"], function( can ) { // ## control.js // `can.Control` // _Controller_ // Binds an element, returns a function that unbinds. var bind = function( el, ev, callback ) { can.bind.call( el, ev, callback ); return function() { can.unbind.call(el, ev, callback); }; }, isFunction = can.isFunction, extend = can.extend, each = can.each, slice = [].slice, paramReplacer = /\{([^\}]+)\}/g, special = can.getObject("$.event.special", [can]) || {}, // Binds an element, returns a function that unbinds. delegate = function( el, selector, ev, callback ) { can.delegate.call(el, selector, ev, callback); return function() { can.undelegate.call(el, selector, ev, callback); }; }, // Calls bind or unbind depending if there is a selector. binder = function( el, ev, callback, selector ) { return selector ? delegate( el, can.trim( selector ), ev, callback ) : bind( el, ev, callback ); }, basicProcessor; var Control = can.Control = can.Construct( /** * @add can.Control */ // /** * @static */ { // Setup pre-processes which methods are event listeners. /** * @hide * * Setup pre-process which methods are event listeners. * */ setup: function() { // Allow contollers to inherit "defaults" from super-classes as it // done in `can.Construct` can.Construct.setup.apply( this, arguments ); // If you didn't provide a name, or are `control`, don't do anything. if ( can.Control ) { // Cache the underscored names. var control = this, funcName; // Calculate and cache actions. control.actions = {}; for ( funcName in control.prototype ) { if ( control._isAction(funcName) ) { control.actions[funcName] = control._action(funcName); } } } }, // Moves `this` to the first argument, wraps it with `jQuery` if it's an element _shifter : function( context, name ) { var method = typeof name == "string" ? context[name] : name; if ( ! isFunction( method )) { method = context[ method ]; } return function() { context.called = name; return method.apply(context, [this.nodeName ? can.$(this) : this].concat( slice.call(arguments, 0))); }; }, // Return `true` if is an action. /** * @hide * @param {String} methodName a prototype function * @return {Boolean} truthy if an action or not */ _isAction: function( methodName ) { var val = this.prototype[methodName], type = typeof val; // if not the constructor return (methodName !== 'constructor') && // and is a function or links to a function ( type == "function" || (type == "string" && isFunction(this.prototype[val] ) ) ) && // and is in special, a processor, or has a funny character !! ( special[methodName] || processors[methodName] || /[^\w]/.test(methodName) ); }, // Takes a method name and the options passed to a control // and tries to return the data necessary to pass to a processor // (something that binds things). /** * @hide * Takes a method name and the options passed to a control * and tries to return the data necessary to pass to a processor * (something that binds things). * * For performance reasons, this called twice. First, it is called when * the Control class is created. If the methodName is templated * like: "{window} foo", it returns null. If it is not templated * it returns event binding data. * * The resulting data is added to this.actions. * * When a control instance is created, _action is called again, but only * on templated actions. * * @param {Object} methodName the method that will be bound * @param {Object} [options] first param merged with class default options * @return {Object} null or the processor and pre-split parts. * The processor is what does the binding/subscribing. */ _action: function( methodName, options ) { // If we don't have options (a `control` instance), we'll run this // later. paramReplacer.lastIndex = 0; if ( options || ! paramReplacer.test( methodName )) { // If we have options, run sub to replace templates `{}` with a // value from the options or the window var convertedName = options ? can.sub(methodName, this._lookup(options)) : methodName; if(!convertedName) { return null; } // If a `{}` template resolves to an object, `convertedName` will be // an array var arr = can.isArray(convertedName), // Get the name name = arr ? convertedName[1] : convertedName, // Grab the event off the end parts = name.split(/\s+/g), event = parts.pop(); return { processor: processors[event] || basicProcessor, parts: [name, parts.join(" "), event], delegate : arr ? convertedName[0] : undefined }; } }, _lookup: function(options){ return [options, window] }, // An object of `{eventName : function}` pairs that Control uses to // hook up events auto-magically. /** * @property {Object.<can.Control.processor>} can.Control.processors processors * @parent can.Control.static * * @description A collection of hookups for custom events on Controls. * * @body * `processors` is an object that allows you to add new events to bind * to on a control, or to change how existent events are bound. Each * key-value pair of `processors` is a specification that pertains to * an event where the key is the name of the event, and the value is * a function that processes calls to bind to the event. * * The processor function takes five arguments: * * - _el_: The Control's element. * - _event_: The event type. * - _selector_: The selector preceding the event in the binding used on the Control. * - _callback_: The callback function being bound. * - _control_: The Control the event is bound on. * * Inside your processor function, you should bind _callback_ to the event, and * return a function for can.Control to call when _callback_ needs to be unbound. * (If _selector_ is defined, you will likely want to use some form of delegation * to bind the event.) * * Here is a Control with a custom event processor set and two callbacks bound * to that event: * * @codestart * can.Control.processors.birthday = function(el, ev, selector, callback, control) { * if(selector) { * myFramework.delegate(ev, el, selector, callback); * return function() { myFramework.undelegate(ev, el, selector, callback); }; * } else { * myFramework.bind(ev, el, callback); * return function() { myFramework.unbind(ev, el, callback); }; * } * }; * * can.Control("EventTarget", { }, { * 'birthday': function(el, ev) { * // do something appropriate for the occasion * }, * '.grandchild birthday': function(el, ev) { * // do something appropriate for the occasion * } * }); * * var target = new EventTarget('#person'); * @codeend * * When `target` is initialized, can.Control will call `can.Control.processors.birthday` * twice (because there are two event hookups for the _birthday_ event). The first * time it's called, the arguments will be: * * - _el_: A NodeList that wraps the element with id 'person'. * - _ev_: `'birthday'` * - _selector_: `''` * - _callback_: The function assigned to `' birthday'` in the prototype section of `EventTarget`'s * definition. * - _control_: `target` itself. * * The second time, the arguments are slightly different: * * - _el_: A NodeList that wraps the element with id 'person'. * - _ev_: `'birthday'` * - _selector_: `'.grandchild'` * - _callback_: The function assigned to `'.grandchild birthday'` in the prototype section of `EventTarget`'s * definition. * - _control_: `target` itself. * * can.Control already has processors for these events: * * - change * - click * - contextmenu * - dblclick * - focusin * - focusout * - keydown * - keyup * - keypress * - mousedown * - mouseenter * - mouseleave * - mousemove * - mouseout * - mouseover * - mouseup * - reset * - resize * - scroll * - select * - submit */ processors: {}, // A object of name-value pairs that act as default values for a // control instance defaults: {} /** * @property {Object} can.Control.defaults defaults * @parent can.Control.static * @description Default values for the Control's options. * * @body * `defaults` provides default values for a Control's options. * Options passed into the constructor function will be shallowly merged * into the values from defaults in [can.Control::setup], and * the result will be stored in [can.Control::options this.options]. * * Message = can.Control.extend({ * defaults: { * message: "Hello World" * } * }, { * init: function(){ * this.element.text( this.options.message ); * } * }); * * new Message( "#el1" ); //writes "Hello World" * new Message( "#el12", { message: "hi" } ); //writes hi */ }, { /** * @prototype */ // Sets `this.element`, saves the control in `data, binds event // handlers. /** * @property {NodeList} can.Control.prototype.element element * @parent can.Control.prototype * @description The element associated with this control. * * @body * The library-wrapped element this control is associated with, * as passed into the constructor. If you want to change the element * that a Control will attach to, you should do it in [can.Control::setup setup]. * If you change the element later, make sure to call [can.Control::on on] * to rebind all the bindings. * * If `element` is removed from the DOM, [can.Control::destroy] will * be called and the Control will be destroyed. */ // /** * @function can.Control.prototype.setup setup * @parent can.Control.prototype * @description Perform pre-initialization logic. * @signature `control.setup(element, options)` * @param {HTMLElement|NodeList|String} element The element as passed to the constructor. * @param {Object} [options] option values for the control. These get added to * this.options and merged with [can.Control.static.defaults defaults]. * @return {undefined|Array} return an array if you want to change what init is called with. By * default it is called with the element and options passed to the control. * * @body * Setup is where most of control's magic happens. It does the following: * * ### Sets this.element * * The first parameter passed to new Control( el, options ) is expected to be * an element. This gets converted to a Wrapped NodeList element and set as * [can.Control.prototype.element this.element]. * * ### Adds the control's name to the element's className * * Control adds it's plugin name to the element's className for easier * debugging. For example, if your Control is named "Foo.Bar", it adds * "foo_bar" to the className. * * ### Saves the control in $.data * * A reference to the control instance is saved in $.data. You can find * instances of "Foo.Bar" like: * * $( '#el' ).data( 'controls' )[ 'foo_bar' ] * * ### Merges Options * Merges the default options with optional user-supplied ones. * Additionally, default values are exposed in the static [can.Control.static.defaults defaults] * so that users can change them. * * ### Binds event handlers * * Setup does the event binding described in [can.Control]. */ setup: function( element, options ) { var cls = this.constructor, pluginname = cls.pluginName || cls._fullName, arr; // Want the raw element here. this.element = can.$(element) if ( pluginname && pluginname !== 'can_control') { // Set element and `className` on element. this.element.addClass(pluginname); } (arr = can.data(this.element,"controls")) || can.data(this.element,"controls",arr = []); arr.push(this); // Option merging. /** * @property {Object} can.Control.prototype.options options * @parent can.Control.prototype * * @description * * Options used to configure a control. * * @body * * The `this.options` property is an Object that contains * configuration data passed to a control when it is * created (`new can.Control(element, options)`). * * In the following example, an options object with * a message is passed to a `Greeting` control. The * `Greeting` control changes the text of its [can.Control::element element] * to the options' message value. * * var Greeting = can.Control.extend({ * init: function(){ * this.element.text( this.options.message ) * } * }) * * new Greeting("#greeting",{message: "I understand this.options"}) * * The options argument passed when creating the control * is merged with [can.Control.defaults defaults] in * [can.Control.prototype.setup setup]. * * In the following example, if no message property is provided, * the defaults' message property is used. * * var Greeting = can.Control.extend({ * defaults: { * message: "Defaults merged into this.options" * } * },{ * init: function(){ * this.element.text( this.options.message ) * } * }) * * new Greeting("#greeting") * */ this.options = extend({}, cls.defaults, options); // Bind all event handlers. this.on(); // Gets passed into `init`. /** * @property {can.NodeList} can.Control.prototype.element element * * @description The element the Control is associated with. * * @parent can.Control.prototype * * @body * * The control instance's HTMLElement (or window) wrapped by the * util library for ease of use. It is set by the first * parameter to `new can.Construct( element, options )` * in [can.Control::setup]. By default, a control listens to events on `this.element`. * * ### Quick Example * * The following `HelloWorld` control sets the control`s text to "Hello World": * * HelloWorld = can.Control({ * init: function(){ * this.element.text( 'Hello World' ); * } * }); * * // create the controller on the element * new HelloWorld( document.getElementById( '#helloworld' ) ); * * ## Wrapped NodeList * * `this.element` is a wrapped NodeList of one HTMLELement (or window). This * is for convenience in libraries like jQuery where all methods operate only on a * NodeList. To get the raw HTMLElement, write: * * this.element[0] //-> HTMLElement * * The following details the NodeList used by each library with * an example of updating its text: * * __jQuery__ `jQuery( HTMLElement )` * * this.element.text("Hello World") * * __Zepto__ `Zepto( HTMLElement )` * * this.element.text("Hello World") * * __Dojo__ `new dojo.NodeList( HTMLElement )` * * this.element.text("Hello World") * * __Mootools__ `$$( HTMLElement )` * * this.element.empty().appendText("Hello World") * * __YUI__ * * this.element.set("text", "Hello World") * * * ## Changing `this.element` * * Sometimes you don't want what's passed to `new can.Control` * to be this.element. You can change this by overwriting * setup or by unbinding, setting this.element, and rebinding. * * ### Overwriting Setup * * The following Combobox overwrites setup to wrap a * select element with a div. That div is used * as `this.element`. Notice how `destroy` sets back the * original element. * * Combobox = can.Control({ * setup: function( el, options ) { * this.oldElement = $( el ); * var newEl = $( '<div/>' ); * this.oldElement.wrap( newEl ); * can.Control.prototype.setup.call( this, newEl, options ); * }, * init: function() { * this.element //-> the div * }, * ".option click": function() { * // event handler bound on the div * }, * destroy: function() { * var div = this.element; //save reference * can.Control.prototype.destroy.call( this ); * div.replaceWith( this.oldElement ); * } * }); * * ### unbinding, setting, and rebinding. * * You could also change this.element by calling * [can.Control::off], setting this.element, and * then calling [can.Control::on] like: * * move: function( newElement ) { * this.off(); * this.element = $( newElement ); * this.on(); * } */ return [this.element, this.options]; }, /** * @function can.Control.prototype.on on * @parent can.Control.prototype * * @description Bind an event handler to a Control, or rebind all event handlers on a Control. * * @signature `control.on([el,] selector, eventName, func)` * @param {HTMLElement|jQuery collection|Object} [el=this.element] * The element to be bound. If no element is provided, the control's element is used instead. * @param {CSSSelectorString} selector A css selector for event delegation. * @param {String} eventName The event to listen for. * @param {Function|String} func A callback function or the String name of a control function. If a control * function name is given, the control function is called back with the bound element and event as the first * and second parameter. Otherwise the function is called back like a normal bind. * @return {Number} The id of the binding in this._bindings * * @body * `on(el, selector, eventName, func)` binds an event handler for an event to a selector under the scope of the given element. * * @signature `control.on()` * * Rebind all of a control's event handlers. * * @return {Number} The number of handlers bound to this Control. * * @body * `this.on()` is used to rebind * all event handlers when [can.Control::options this.options] has changed. It * can also be used to bind or delegate from other elements or objects. * * ## Rebinding * * By using templated event handlers, a control can listen to objects outside * `this.element`. This is extremely common in MVC programming. For example, * the following control might listen to a task model's `completed` property and * toggle a strike className like: * * TaskStriker = can.Control({ * "{task} completed": function(){ * this.update(); * }, * update: function(){ * if ( this.options.task.completed ) { * this.element.addClass( 'strike' ); * } else { * this.element.removeClass( 'strike' ); * } * } * }); * * var taskstriker = new TaskStriker({ * task: new Task({ completed: 'true' }) * }); * * To update the `taskstriker`'s task, add a task method that updates * this.options and rebinds the event handlers for the new task like: * * TaskStriker = can.Control({ * "{task} completed": function(){ * this.update(); * }, * update: function() { * if ( this.options.task.completed ) { * this.element.addClass( 'strike' ); * } else { * this.element.removeClass( 'strike' ); * } * }, * task: function( newTask ) { * this.options.task = newTask; * this.on(); * this.update(); * } * }); * * var taskstriker = new TaskStriker({ * task: new Task({ completed: true }) * }); * taskstriker.task( new TaskStriker({ * task: new Task({ completed: false }) * })); * * ## Adding new events * * If events need to be bound to outside of the control and templated event handlers * are not sufficient, you can call this.on to bind or delegate programmatically: * * init: function() { * // calls somethingClicked( el, ev ) * this.on( 'click', 'somethingClicked' ); * * // calls function when the window is clicked * this.on( window, 'click', function( ev ) { * //do something * }); * }, * somethingClicked: function( el, ev ) { * * } */ on: function( el, selector, eventName, func ) { if ( ! el ) { // Adds bindings. this.off(); // Go through the cached list of actions and use the processor // to bind var cls = this.constructor, bindings = this._bindings, actions = cls.actions, element = this.element, destroyCB = can.Control._shifter(this,"destroy"), funcName, ready; for ( funcName in actions ) { // Only push if we have the action and no option is `undefined` if ( actions.hasOwnProperty( funcName ) && (ready = actions[funcName] || cls._action(funcName, this.options))) { bindings.push(ready.processor(ready.delegate || element, ready.parts[2], ready.parts[1], funcName, this)); } } // Setup to be destroyed... // don't bind because we don't want to remove it. can.bind.call(element,"removed", destroyCB); bindings.push(function( el ) { can.unbind.call(el,"removed", destroyCB); }); return bindings.length; } if ( typeof el == 'string' ) { func = eventName; eventName = selector; selector = el; el = this.element; } if(func === undefined) { func = eventName; eventName = selector; selector = null; } if ( typeof func == 'string' ) { func = can.Control._shifter(this,func); } this._bindings.push( binder( el, eventName, func, selector )); return this._bindings.length; }, // Unbinds all event handlers on the controller. /** * @hide * Unbinds all event handlers on the controller. You should never * be calling this unless in use with [can.Control::on]. */ off : function(){ var el = this.element[0]; each(this._bindings || [], function( value ) { value(el); }); // Adds bindings. this._bindings = []; }, // Prepares a `control` for garbage collection /** * @description Remove a Control from an element and clean up the Control. * @signature `control.destroy()` * * Prepares a control for garbage collection and is a place to * reset any changes the control has made. * * @function can.Control.prototype.destroy destroy * @parent can.Control.prototype * * @body * * * ## Allowing Garbage Collection * * Destroy is called whenever a control's element is removed from the page using * the library's standard HTML modifier methods. This means that you * don't have to call destroy yourself and it * will be called automatically when appropriate. * * The following `Clicker` widget listens on the window for clicks and updates * its element's innerHTML. If we remove the element, the window's event handler * is removed auto-magically: * * * Clickr = can.Control({ * "{window} click": function() { * this.element.html( this.count ? * this.count++ : this.count = 0 ); * } * }); * * // create a clicker on an element * new Clicker( "#clickme" ); * * // remove the element * $( '#clickme' ).remove(); * * * The methods you can use that will destroy controls automatically by library: * * __jQuery and Zepto__ * * - $.fn.remove * - $.fn.html * - $.fn.replaceWith * - $.fn.empty * * __Dojo__ * * - dojo.destroy * - dojo.empty * - dojo.place (with the replace option) * * __Mootools__ * * - Element.prototype.destroy * * __YUI__ * * - Y.Node.prototype.remove * - Y.Node.prototype.destroy * * * ## Teardown in Destroy * * Sometimes, you want to reset a controlled element back to its * original state when the control is destroyed. Overwriting destroy * lets you write teardown code of this manner. __When overwriting * destroy, make sure you call Control's base functionality__. * * The following example changes an element's text when the control is * created and sets it back when the control is removed: * * Changer = can.Control.extend({ * init: function() { * this.oldText = this.element.text(); * this.element.text( "Changed!!!" ); * }, * destroy: function() { * this.element.text( this.oldText ); * can.Control.prototype.destroy.call( this ); * } * }); * * // create a changer which changes #myel's text * var changer = new Changer( '#myel' ); * * // destroy changer which will reset it * changer.destroy(); * * ## Base Functionality * * Control prepares the control for garbage collection by: * * - unbinding all event handlers * - clearing references to this.element and this.options * - clearing the element's reference to the control * - removing it's [can.Control.pluginName] from the element's className * */ destroy: function() { //Control already destroyed if(this.element === null) { return; } var Class = this.constructor, pluginName = Class.pluginName || Class._fullName, controls; // Unbind bindings. this.off(); if(pluginName && pluginName !== 'can_control'){ // Remove the `className`. this.element.removeClass(pluginName); } // Remove from `data`. controls = can.data(this.element,"controls"); controls.splice(can.inArray(this, controls),1); can.trigger( this, "destroyed"); // In case we want to know if the `control` is removed. this.element = null; } }); var processors = can.Control.processors, // Processors do the binding. // They return a function that unbinds when called. // // The basic processor that binds events. basicProcessor = function( el, event, selector, methodName, control ) { return binder( el, event, can.Control._shifter(control, methodName), selector); }; // Set common events to be processed as a `basicProcessor` each(["change", "click", "contextmenu", "dblclick", "keydown", "keyup", "keypress", "mousedown", "mousemove", "mouseout", "mouseover", "mouseup", "reset", "resize", "scroll", "select", "submit", "focusin", "focusout", "mouseenter", "mouseleave", // #104 - Add touch events as default processors // TOOD feature detect? "touchstart", "touchmove", "touchcancel", "touchend", "touchleave" ], function( v ) { processors[v] = basicProcessor; }); return Control; });