( function ( M, $ ) { var EventEmitter = M.require( 'eventemitter' ), View, // Cached regex to split keys for `delegate`. delegateEventSplitter = /^(\S+)\s*(.*)$/, idCounter = 0; /** * Generate a unique integer id (unique within the entire client session). * Useful for temporary DOM ids. * @ignore * @param {String} prefix Prefix to be used when generating the id. * @returns {String} */ function uniqueId( prefix ) { var id = ( ++idCounter ).toString(); return prefix ? prefix + id : id; } /** * Should be extended using extend(). * * When options contains el property, this.$el in the constructed object * will be set to the corresponding jQuery object. Otherwise, this.$el * will be an empty div. * * When extended using extend(), if the extended prototype contains * template property, this.$el will be filled with rendered template (with * options parameter used as template data). * * template property can be a string which will be passed to mw.template.compile() * or an object that has a render() function which accepts an object with * template data as its argument (similarly to an object created by * mw.template.compile()). * * You can also define a defaults property which should be an object * containing default values for the template (if they're not present in * the options parameter). * * If this.$el is not a jQuery object bound to existing DOM element, the * view can be attached to an element using appendTo(), prependTo(), * insertBefore(), insertAfter() proxy functions. * * append(), prepend(), before(), after() can be used to modify $el. on() * can be used to bind events. * * You can also use declarative DOM events binding by specifying an `events` * map on the class. The keys will be 'event selector' and the value can be * either the name of a method to call, or a function. All methods and * functions will be executed on the context of the View. * * Inspired from Backbone.js * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128 * * @example * <code> * var MyComponent = View.extend( { * events: { * 'mousedown .title': 'edit', * 'click .button': 'save', * 'click .open': function(e) { ... } * }, * edit: function ( ev ) { * //... * }, * save: function ( ev ) { * //... * } * } ); * </code> * * @class View * @extends EventEmitter * @param {Object} options Options for the view, containing the el or * template data or any other information you want to use in the view. * Example: * @example * <pre> * var View, Section, section; * View = M.require( 'View' ); * Section = View.extend( { * template: mw.template.compile( "<h2>{{title}}</h2>" ), * } ); * section = new Section( { title: 'Test', text: 'Test section body' } ); * section.appendTo( 'body' ); * </pre> */ View = EventEmitter.extend( { /** * Name of tag that contains the rendered template * @property String */ tagName: 'div', /** * @property {Mixed} * Specifies the template used in render(). Object|String|HoganTemplate */ template: undefined, /** * Specifies partials (sub-templates) for the main template. Example: * * @example * // example content for the "some" template (sub-template will be * // inserted where {{>content}} is): * // <h1>Heading</h1> * // {{>content}} * * var SomeView = View.extend( { * template: M.template.get( 'some.hogan' ), * templatePartials: { content: M.template.get( 'sub.hogan' ) } * } * * @property {Object} */ templatePartials: {}, /** * A set of default options that are merged with options passed into the initialize function. * * @cfg {Object} defaults Default options hash. * @cfg {jQuery.Object|String} [defaults.el] jQuery selector to use for rendering. */ defaults: {}, /** * Default events map */ events: null, /** * Run once during construction to set up the View * @method * @param {Object} options Object passed to the constructor. */ initialize: function ( options ) { EventEmitter.prototype.initialize.apply( this, arguments ); this.defaults = $.extend( {}, this._parent.defaults, this.defaults ); this.templatePartials = $.extend( {}, this._parent.templatePartials, this.templatePartials ); options = $.extend( {}, this.defaults, options ); if ( options.el ) { this.$el = $( options.el ); } else { this.$el = $( '<' + this.tagName + '>' ); } this.$el.addClass( this.className ); // FIXME: If this becomes a default should become part of className property. this.$el.addClass( 'view-border-box' ); // TODO: if template compilation is too slow, don't compile them on a // per object basis, but don't worry about it now (maybe add cache to // M.template.compile()) if ( typeof this.template === 'string' ) { this.template = mw.template.compile( this.template ); } this.options = options; this.render( options ); // Assign a unique id for dom events binding/unbinding this.cid = uniqueId( 'view' ); this.delegateEvents(); }, /** * Function called before the view is rendered. Can be redefined in * objects that extend View. * * @method * @param {Object} options Object passed to the constructor. */ preRender: $.noop, /** * Function called after the view is rendered. Can be redefined in * objects that extend View. * * @method * @param {Object} options Object passed to the constructor. */ postRender: $.noop, /** * Fill this.$el with template rendered using data if template is set. * * @method * @param {Object} data Template data. */ render: function ( data ) { data = $.extend( true, {}, this.options, data ); this.preRender( data ); if ( this.template ) { this.$el.html( this.template.render( data, this.templatePartials ) ); } this.postRender( data ); return this; }, /** * Wraps this.$el.find, so that you can search for elements in the view's * ($el's) scope. * * @method * @param {String} query A jQuery CSS selector. * @return {jQuery.Object} jQuery object containing results of the search. */ $: function ( query ) { return this.$el.find( query ); }, /** * Set callbacks, where `this.events` is a hash of * * {"event selector": "callback"} * * { * 'mousedown .title': 'edit', * 'click .button': 'save', * 'click .open': function(e) { ... } * } * * pairs. Callbacks will be bound to the view, with `this` set properly. * Uses event delegation for efficiency. * Omitting the selector binds the event to `this.el`. * * @param {Object} events Optionally set this events instead of the ones on this. */ delegateEvents: function ( events ) { var match, key, method; // Take either the events parameter or the this.events to process events = events || this.events; if ( events ) { // Remove current events before re-binding them this.undelegateEvents(); for ( key in events ) { method = events[ key ]; // If the method is a string name of this.method, get it if ( !$.isFunction( method ) ) { method = this[ events[ key ] ]; } if ( method ) { // Extract event and selector from the key match = key.match( delegateEventSplitter ); this.delegate( match[ 1 ], match[ 2 ], $.proxy( method, this ) ); } } } }, /** * Add a single event listener to the view's element (or a child element * using `selector`). This only works for delegate-able events: not `focus`, * `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. * * @param {String} eventName * @param {String} selector * @param {Function} listener */ delegate: function ( eventName, selector, listener ) { this.$el.on( eventName + '.delegateEvents' + this.cid, selector, listener ); }, /** * Clears all callbacks previously bound to the view by `delegateEvents`. * You usually don't need to use this, but may wish to if you have multiple * views attached to the same DOM element. */ undelegateEvents: function () { if ( this.$el ) { this.$el.off( '.delegateEvents' + this.cid ); } }, /** * A finer-grained `undelegateEvents` for removing a single delegated event. * `selector` and `listener` are both optional. * * @param {String} eventName * @param {String} selector * @param {Function} listener */ undelegate: function ( eventName, selector, listener ) { this.$el.off( eventName + '.delegateEvents' + this.cid, selector, listener ); } } ); $.each( [ 'append', 'prepend', 'appendTo', 'prependTo', 'after', 'before', 'insertAfter', 'insertBefore', 'remove', 'detach' ], function ( i, prop ) { /** @ignore **/ View.prototype[prop] = function () { this.$el[prop].apply( this.$el, arguments ); return this; }; } ); M.define( 'View', View ); }( mw.mobileFrontend, jQuery ) );