/examples/thorax/bower_components/thorax/thorax.js
JavaScript | 2669 lines | 2159 code | 278 blank | 232 comment | 528 complexity | d25b7722d0b37fd3d4a2f5684dc84c59 MD5 | raw file
Possible License(s): BSD-3-Clause
- /*
- Copyright (c) 2011-2013 @WalmartLabs
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to
- deal in the Software without restriction, including without limitation the
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- sell copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- DEALINGS IN THE SOFTWARE.
- */
- ;;
- (function() {
- /*global cloneInheritVars, createInheritVars, createRegistryWrapper, getValue, inheritVars */
- //support zepto.forEach on jQuery
- if (!$.fn.forEach) {
- $.fn.forEach = function(iterator, context) {
- $.fn.each.call(this, function(index) {
- iterator.call(context || this, this, index);
- });
- };
- }
- var viewNameAttributeName = 'data-view-name',
- viewCidAttributeName = 'data-view-cid',
- viewHelperAttributeName = 'data-view-helper';
- //view instances
- var viewsIndexedByCid = {};
- var Thorax = this.Thorax = {
- VERSION: '2.0.0rc1',
- templatePathPrefix: '',
- templates: {},
- //view classes
- Views: {},
- //certain error prone pieces of code (on Android only it seems)
- //are wrapped in a try catch block, then trigger this handler in
- //the catch, with the name of the function or event that was
- //trying to be executed. Override this with a custom handler
- //to debug / log / etc
- onException: function(name, err) {
- throw err;
- }
- };
- Thorax.View = Backbone.View.extend({
- constructor: function() {
- var response = Backbone.View.apply(this, arguments);
- _.each(inheritVars, function(obj) {
- if (obj.ctor) {
- obj.ctor.call(this, response);
- }
- }, this);
- return response;
- },
- _configure: function(options) {
- var self = this;
- this._objectOptionsByCid = {};
- this._boundDataObjectsByCid = {};
- // Setup object event tracking
- _.each(inheritVars, function(obj) {
- self[obj.name] = [];
- });
- viewsIndexedByCid[this.cid] = this;
- this.children = {};
- this._renderCount = 0;
- //this.options is removed in Thorax.View, we merge passed
- //properties directly with the view and template context
- _.extend(this, options || {});
- // Setup helpers
- bindHelpers.call(this);
- //compile a string if it is set as this.template
- if (typeof this.template === 'string') {
- this.template = Handlebars.compile(this.template, {data: true});
- } else if (this.name && !this.template) {
- //fetch the template
- this.template = Thorax.Util.getTemplate(this.name, true);
- }
- _.each(inheritVars, function(obj) {
- if (obj.configure) {
- obj.configure.call(this);
- }
- }, this);
- },
- setElement : function() {
- var response = Backbone.View.prototype.setElement.apply(this, arguments);
- this.name && this.$el.attr(viewNameAttributeName, this.name);
- this.$el.attr(viewCidAttributeName, this.cid);
- return response;
- },
- _addChild: function(view) {
- this.children[view.cid] = view;
- if (!view.parent) {
- view.parent = this;
- }
- this.trigger('child', view);
- return view;
- },
- _removeChild: function(view) {
- delete this.children[view.cid];
- view.parent = null;
- return view;
- },
- destroy: function(options) {
- options = _.defaults(options || {}, {
- children: true
- });
- _.each(this._boundDataObjectsByCid, this.unbindDataObject, this);
- this.trigger('destroyed');
- delete viewsIndexedByCid[this.cid];
- _.each(this.children, function(child) {
- this._removeChild(child);
- if (options.children) {
- child.destroy();
- }
- }, this);
- if (this.parent) {
- this.parent._removeChild(this);
- }
- this.remove(); // Will call stopListening()
- },
- render: function(output) {
- this._previousHelpers = _.filter(this.children, function(child) { return child._helperOptions; });
- var children = {};
- _.each(this.children, function(child, key) {
- if (!child._helperOptions) {
- children[key] = child;
- }
- });
- this.children = children;
- if (typeof output === 'undefined' || (!_.isElement(output) && !Thorax.Util.is$(output) && !(output && output.el) && typeof output !== 'string' && typeof output !== 'function')) {
- if (!this.template) {
- //if the name was set after the view was created try one more time to fetch a template
- if (this.name) {
- this.template = Thorax.Util.getTemplate(this.name, true);
- }
- if (!this.template) {
- throw new Error('View ' + (this.name || this.cid) + '.render() was called with no content and no template set on the view.');
- }
- }
- output = this.renderTemplate(this.template);
- } else if (typeof output === 'function') {
- output = this.renderTemplate(output);
- }
- // Destroy any helpers that may be lingering
- _.each(this._previousHelpers, function(child) {
- child.destroy();
- child.parent = undefined;
- });
- this._previousHelpers = undefined;
- //accept a view, string, Handlebars.SafeString or DOM element
- this.html((output && output.el) || (output && output.string) || output);
- ++this._renderCount;
- this.trigger('rendered');
- return output;
- },
- context: function() {
- return (this.model && this.model.attributes) || {};
- },
- _getContext: function() {
- return _.extend({}, this, getValue(this, 'context') || {});
- },
- // Private variables in handlebars / options.data in template helpers
- _getData: function(data) {
- return {
- view: this,
- cid: _.uniqueId('t'),
- yield: function() {
- // fn is seeded by template helper passing context to data
- return data.fn && data.fn(data);
- }
- };
- },
- _getHelpers: function() {
- if (this.helpers) {
- return _.extend({}, Handlebars.helpers, this.helpers);
- } else {
- return Handlebars.helpers;
- }
- },
- renderTemplate: function(file, context, ignoreErrors) {
- var template;
- context = context || this._getContext();
- if (typeof file === 'function') {
- template = file;
- } else {
- template = Thorax.Util.getTemplate(file);
- }
- if (!template) {
- if (ignoreErrors) {
- return '';
- } else {
- throw new Error('Unable to find template ' + file);
- }
- } else {
- return template(context, {
- helpers: this._getHelpers(),
- data: this._getData(context)
- });
- }
- },
- ensureRendered: function() {
- !this._renderCount && this.render();
- },
- appendTo: function(el) {
- this.ensureRendered();
- $(el).append(this.el);
- this.trigger('ready', {target: this});
- },
- html: function(html) {
- function replaceHTML(view) {
- view.el.innerHTML = "";
- return view.$el.append(html);
- }
- if (typeof html === 'undefined') {
- return this.el.innerHTML;
- } else {
- // Event for IE element fixes
- this.trigger('before:append');
- var element;
- if (this.collection && this._objectOptionsByCid[this.collection.cid] && this._renderCount) {
- // preserve collection element if it was not created with {{collection}} helper
- var oldCollectionElement = this.getCollectionElement();
- element = replaceHTML(this);
- if (!oldCollectionElement.attr('data-view-cid')) {
- this.getCollectionElement().replaceWith(oldCollectionElement);
- }
- } else {
- element = replaceHTML(this);
- }
- this.trigger('append');
- return element;
- }
- },
- _anchorClick: function(event) {
- var target = $(event.currentTarget),
- href = target.attr('href');
- // Route anything that starts with # or / (excluding //domain urls)
- if (href && (href[0] === '#' || (href[0] === '/' && href[1] !== '/'))) {
- Backbone.history.navigate(href, {
- trigger: true
- });
- return false;
- }
- return true;
- }
- });
- Thorax.View.extend = function() {
- createInheritVars(this);
- var child = Backbone.View.extend.apply(this, arguments);
- child.__parent__ = this;
- resetInheritVars(child);
- return child;
- };
- createRegistryWrapper(Thorax.View, Thorax.Views);
- function bindHelpers() {
- if (this.helpers) {
- _.each(this.helpers, function(helper, name) {
- var view = this;
- this.helpers[name] = function() {
- var args = _.toArray(arguments),
- options = _.last(args);
- options.context = this;
- return helper.apply(view, args);
- };
- }, this);
- }
- }
- //$(selector).view() helper
- $.fn.view = function(options) {
- options = _.defaults(options || {}, {
- helper: true
- });
- var selector = '[' + viewCidAttributeName + ']';
- if (!options.helper) {
- selector += ':not([' + viewHelperAttributeName + '])';
- }
- var el = $(this).closest(selector);
- return (el && viewsIndexedByCid[el.attr(viewCidAttributeName)]) || false;
- };
- ;;
- /*global createRegistryWrapper:true, cloneEvents: true */
- function createRegistryWrapper(klass, hash) {
- var $super = klass.extend;
- klass.extend = function() {
- var child = $super.apply(this, arguments);
- if (child.prototype.name) {
- hash[child.prototype.name] = child;
- }
- return child;
- };
- }
- function registryGet(object, type, name, ignoreErrors) {
- var target = object[type],
- value;
- if (name.indexOf('.') >= 0) {
- var bits = name.split(/\./);
- name = bits.pop();
- _.each(bits, function(key) {
- target = target[key];
- });
- }
- target && (value = target[name]);
- if (!value && !ignoreErrors) {
- throw new Error(type + ': ' + name + ' does not exist.');
- } else {
- return value;
- }
- }
- // getValue is used instead of _.result because we
- // need an extra scope parameter, and will minify
- // better than _.result
- function getValue(object, prop, scope) {
- if (!(object && object[prop])) {
- return null;
- }
- return _.isFunction(object[prop])
- ? object[prop].call(scope || object)
- : object[prop];
- }
- var inheritVars = {};
- function createInheritVars(self) {
- // Ensure that we have our static event objects
- _.each(inheritVars, function(obj) {
- if (!self[obj.name]) {
- self[obj.name] = [];
- }
- });
- }
- function resetInheritVars(self) {
- // Ensure that we have our static event objects
- _.each(inheritVars, function(obj) {
- self[obj.name] = [];
- });
- }
- function walkInheritTree(source, fieldName, isStatic, callback) {
- var tree = [];
- if (_.has(source, fieldName)) {
- tree.push(source);
- }
- var iterate = source;
- if (isStatic) {
- while (iterate = iterate.__parent__) {
- if (_.has(iterate, fieldName)) {
- tree.push(iterate);
- }
- }
- } else {
- iterate = iterate.constructor;
- while (iterate) {
- if (iterate.prototype && _.has(iterate.prototype, fieldName)) {
- tree.push(iterate.prototype);
- }
- iterate = iterate.__super__ && iterate.__super__.constructor;
- }
- }
- var i = tree.length;
- while (i--) {
- _.each(getValue(tree[i], fieldName, source), callback);
- }
- }
- function objectEvents(target, eventName, callback, context) {
- if (_.isObject(callback)) {
- var spec = inheritVars[eventName];
- if (spec && spec.event) {
- addEvents(target['_' + eventName + 'Events'], callback, context);
- return true;
- }
- }
- }
- function addEvents(target, source, context) {
- _.each(source, function(callback, eventName) {
- if (_.isArray(callback)) {
- _.each(callback, function(cb) {
- target.push([eventName, cb, context]);
- });
- } else {
- target.push([eventName, callback, context]);
- }
- });
- }
- function getOptionsData(options) {
- if (!options || !options.data) {
- throw new Error('Handlebars template compiled without data, use: Handlebars.compile(template, {data: true})');
- }
- return options.data;
- }
- // These whitelisted attributes will be the only ones passed
- // from the options hash to Thorax.Util.tag
- var htmlAttributesToCopy = ['id', 'className', 'tagName'];
- // In helpers "tagName" or "tag" may be specified, as well
- // as "class" or "className". Normalize to "tagName" and
- // "className" to match the property names used by Backbone
- // jQuery, etc. Special case for "className" in
- // Thorax.Util.tag: will be rewritten as "class" in
- // generated HTML.
- function normalizeHTMLAttributeOptions(options) {
- if (options.tag) {
- options.tagName = options.tag;
- delete options.tag;
- }
- if (options['class']) {
- options.className = options['class'];
- delete options['class'];
- }
- }
- Thorax.Util = {
- getViewInstance: function(name, attributes) {
- attributes = attributes || {};
- if (typeof name === 'string') {
- var Klass = registryGet(Thorax, 'Views', name, false);
- return Klass.cid ? _.extend(Klass, attributes || {}) : new Klass(attributes);
- } else if (typeof name === 'function') {
- return new name(attributes);
- } else {
- return name;
- }
- },
- getTemplate: function(file, ignoreErrors) {
- //append the template path prefix if it is missing
- var pathPrefix = Thorax.templatePathPrefix,
- template;
- if (pathPrefix && file.substr(0, pathPrefix.length) !== pathPrefix) {
- file = pathPrefix + file;
- }
- // Without extension
- file = file.replace(/\.handlebars$/, '');
- template = Thorax.templates[file];
- if (!template) {
- // With extension
- file = file + '.handlebars';
- template = Thorax.templates[file];
- }
- if (template && typeof template === 'string') {
- template = Thorax.templates[file] = Handlebars.compile(template, {data: true});
- } else if (!template && !ignoreErrors) {
- throw new Error('templates: ' + file + ' does not exist.');
- }
- return template;
- },
- //'selector' is not present in $('<p></p>')
- //TODO: investigage a better detection method
- is$: function(obj) {
- return typeof obj === 'object' && ('length' in obj);
- },
- expandToken: function(input, scope) {
- if (input && input.indexOf && input.indexOf('{{') >= 0) {
- var re = /(?:\{?[^{]+)|(?:\{\{([^}]+)\}\})/g,
- match,
- ret = [];
- function deref(token, scope) {
- if (token.match(/^("|')/) && token.match(/("|')$/)) {
- return token.replace(/(^("|')|('|")$)/g, '');
- }
- var segments = token.split('.'),
- len = segments.length;
- for (var i = 0; scope && i < len; i++) {
- if (segments[i] !== 'this') {
- scope = scope[segments[i]];
- }
- }
- return scope;
- }
- while (match = re.exec(input)) {
- if (match[1]) {
- var params = match[1].split(/\s+/);
- if (params.length > 1) {
- var helper = params.shift();
- params = _.map(params, function(param) { return deref(param, scope); });
- if (Handlebars.helpers[helper]) {
- ret.push(Handlebars.helpers[helper].apply(scope, params));
- } else {
- // If the helper is not defined do nothing
- ret.push(match[0]);
- }
- } else {
- ret.push(deref(params[0], scope));
- }
- } else {
- ret.push(match[0]);
- }
- }
- input = ret.join('');
- }
- return input;
- },
- tag: function(attributes, content, scope) {
- var htmlAttributes = _.omit(attributes, 'tagName'),
- tag = attributes.tagName || 'div';
- return '<' + tag + ' ' + _.map(htmlAttributes, function(value, key) {
- if (typeof value === 'undefined' || key === 'expand-tokens') {
- return '';
- }
- var formattedValue = value;
- if (scope) {
- formattedValue = Thorax.Util.expandToken(value, scope);
- }
- return (key === 'className' ? 'class' : key) + '="' + Handlebars.Utils.escapeExpression(formattedValue) + '"';
- }).join(' ') + '>' + (typeof content === 'undefined' ? '' : content) + '</' + tag + '>';
- }
- };
- ;;
- /*global createInheritVars, inheritVars */
- Thorax.Mixins = {};
- inheritVars.mixins = {
- name: 'mixins',
- configure: function() {
- _.each(this.constructor.mixins, this.mixin, this);
- _.each(this.mixins, this.mixin, this);
- }
- };
- _.extend(Thorax.View, {
- mixin: function(mixin) {
- createInheritVars(this);
- this.mixins.push(mixin);
- },
- registerMixin: function(name, callback, methods) {
- Thorax.Mixins[name] = [callback, methods];
- }
- });
- Thorax.View.prototype.mixin = function(name) {
- if (!this._appliedMixins) {
- this._appliedMixins = [];
- }
- if (this._appliedMixins.indexOf(name) === -1) {
- this._appliedMixins.push(name);
- if (typeof name === 'function') {
- name.call(this);
- } else {
- var mixin = Thorax.Mixins[name];
- _.extend(this, mixin[1]);
- //mixin callback may be an array of [callback, arguments]
- if (_.isArray(mixin[0])) {
- mixin[0][0].apply(this, mixin[0][1]);
- } else {
- mixin[0].apply(this, _.toArray(arguments).slice(1));
- }
- }
- }
- };
- ;;
- /*global createInheritVars, inheritVars, objectEvents, walkInheritTree */
- // Save a copy of the _on method to call as a $super method
- var _on = Thorax.View.prototype.on;
- inheritVars.event = {
- name: '_events',
- configure: function() {
- var self = this;
- walkInheritTree(this.constructor, '_events', true, function(event) {
- self.on.apply(self, event);
- });
- walkInheritTree(this, 'events', false, function(handler, eventName) {
- self.on(eventName, handler, self);
- });
- }
- };
- _.extend(Thorax.View, {
- on: function(eventName, callback) {
- createInheritVars(this);
- if (objectEvents(this, eventName, callback)) {
- return this;
- }
- //accept on({"rendered": handler})
- if (typeof eventName === 'object') {
- _.each(eventName, function(value, key) {
- this.on(key, value);
- }, this);
- } else {
- //accept on({"rendered": [handler, handler]})
- if (_.isArray(callback)) {
- _.each(callback, function(cb) {
- this._events.push([eventName, cb]);
- }, this);
- //accept on("rendered", handler)
- } else {
- this._events.push([eventName, callback]);
- }
- }
- return this;
- }
- });
- _.extend(Thorax.View.prototype, {
- on: function(eventName, callback, context) {
- if (objectEvents(this, eventName, callback, context)) {
- return this;
- }
- if (typeof eventName === 'object' && arguments.length < 3) {
- //accept on({"rendered": callback})
- _.each(eventName, function(value, key) {
- this.on(key, value, callback || this); // callback is context in this form of the call
- }, this);
- } else {
- //accept on("rendered", callback, context)
- //accept on("click a", callback, context)
- _.each((_.isArray(callback) ? callback : [callback]), function(callback) {
- var params = eventParamsFromEventItem.call(this, eventName, callback, context || this);
- if (params.type === 'DOM') {
- //will call _addEvent during delegateEvents()
- if (!this._eventsToDelegate) {
- this._eventsToDelegate = [];
- }
- this._eventsToDelegate.push(params);
- } else {
- this._addEvent(params);
- }
- }, this);
- }
- return this;
- },
- delegateEvents: function(events) {
- this.undelegateEvents();
- if (events) {
- if (_.isFunction(events)) {
- events = events.call(this);
- }
- this._eventsToDelegate = [];
- this.on(events);
- }
- this._eventsToDelegate && _.each(this._eventsToDelegate, this._addEvent, this);
- },
- //params may contain:
- //- name
- //- originalName
- //- selector
- //- type "view" || "DOM"
- //- handler
- _addEvent: function(params) {
- if (params.type === 'view') {
- _.each(params.name.split(/\s+/), function(name) {
- _on.call(this, name, bindEventHandler.call(this, 'view-event:', params));
- }, this);
- } else {
- var boundHandler = bindEventHandler.call(this, 'dom-event:', params);
- if (!params.nested) {
- boundHandler = containHandlerToCurentView(boundHandler, this.cid);
- }
- if (params.selector) {
- var name = params.name + '.delegateEvents' + this.cid;
- this.$el.on(name, params.selector, boundHandler);
- } else {
- this.$el.on(params.name, boundHandler);
- }
- }
- }
- });
- // When view is ready trigger ready event on all
- // children that are present, then register an
- // event that will trigger ready on new children
- // when they are added
- Thorax.View.on('ready', function(options) {
- if (!this._isReady) {
- this._isReady = true;
- function triggerReadyOnChild(child) {
- child.trigger('ready', options);
- }
- _.each(this.children, triggerReadyOnChild);
- this.on('child', triggerReadyOnChild);
- }
- });
- var eventSplitter = /^(nested\s+)?(\S+)(?:\s+(.+))?/;
- var domEvents = [],
- domEventRegexp;
- function pushDomEvents(events) {
- domEvents.push.apply(domEvents, events);
- domEventRegexp = new RegExp('^(nested\\s+)?(' + domEvents.join('|') + ')(?:\\s|$)');
- }
- pushDomEvents([
- 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
- 'touchstart', 'touchend', 'touchmove',
- 'click', 'dblclick',
- 'keyup', 'keydown', 'keypress',
- 'submit', 'change',
- 'focus', 'blur'
- ]);
- function containHandlerToCurentView(handler, cid) {
- return function(event) {
- var view = $(event.target).view({helper: false});
- if (view && view.cid === cid) {
- event.originalContext = this;
- handler(event);
- }
- };
- }
- function bindEventHandler(eventName, params) {
- eventName += params.originalName;
- var callback = params.handler,
- method = typeof callback === 'function' ? callback : this[callback];
- if (!method) {
- throw new Error('Event "' + callback + '" does not exist ' + (this.name || this.cid) + ':' + eventName);
- }
- return _.bind(function() {
- try {
- method.apply(this, arguments);
- } catch (e) {
- Thorax.onException('thorax-exception: ' + (this.name || this.cid) + ':' + eventName, e);
- }
- }, params.context || this);
- }
- function eventParamsFromEventItem(name, handler, context) {
- var params = {
- originalName: name,
- handler: typeof handler === 'string' ? this[handler] : handler
- };
- if (name.match(domEventRegexp)) {
- var match = eventSplitter.exec(name);
- params.nested = !!match[1];
- params.name = match[2];
- params.type = 'DOM';
- params.selector = match[3];
- } else {
- params.name = name;
- params.type = 'view';
- }
- params.context = context;
- return params;
- }
- ;;
- /*global getOptionsData, viewHelperAttributeName */
- var viewPlaceholderAttributeName = 'data-view-tmp';
- var viewTemplateOverrides = {};
- Thorax.HelperView = Thorax.View.extend({
- _ensureElement: function() {
- Thorax.View.prototype._ensureElement.apply(this, arguments);
- this.$el.attr(viewHelperAttributeName, this._helperName);
- },
- _getContext: function() {
- return this.parent._getContext.apply(this.parent, arguments);
- },
- });
- // Ensure nested inline helpers will always have this.parent
- // set to the view containing the template
- function getParent(parent) {
- // The `view` helper is a special case as it embeds
- // a view instead of creating a new one
- while (parent._helperName && parent._helperName !== 'view') {
- parent = parent.parent;
- }
- return parent;
- }
- Handlebars.registerViewHelper = function(name, ViewClass, callback) {
- if (arguments.length === 2) {
- if (ViewClass.factory) {
- callback = ViewClass.callback;
- } else {
- callback = ViewClass;
- ViewClass = Thorax.HelperView;
- }
- }
- Handlebars.registerHelper(name, function() {
- var args = _.toArray(arguments),
- options = args.pop(),
- declaringView = getOptionsData(options).view;
- var viewOptions = {
- template: options.fn || Handlebars.VM.noop,
- inverse: options.inverse,
- options: options.hash,
- declaringView: declaringView,
- parent: getParent(declaringView),
- _helperName: name,
- _helperOptions: {
- options: cloneHelperOptions(options),
- args: _.clone(args)
- }
- };
- normalizeHTMLAttributeOptions(options.hash);
- _.extend(viewOptions, _.pick(options.hash, htmlAttributesToCopy));
- // Check to see if we have an existing instance that we can reuse
- var instance = _.find(declaringView._previousHelpers, function(child) {
- return compareHelperOptions(viewOptions, child);
- });
- // Create the instance if we don't already have one
- if (!instance) {
- if (ViewClass.factory) {
- instance = ViewClass.factory(args, viewOptions);
- if (!instance) {
- return '';
- }
- instance._helperName = viewOptions._helperName;
- instance._helperOptions = viewOptions._helperOptions;
- } else {
- instance = new ViewClass(viewOptions);
- }
- args.push(instance);
- declaringView._addChild(instance);
- declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
- declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
- callback && callback.apply(this, args);
- } else {
- declaringView._previousHelpers = _.without(declaringView._previousHelpers, instance);
- declaringView.children[instance.cid] = instance;
- }
- var htmlAttributes = _.pick(options.hash, htmlAttributesToCopy);
- htmlAttributes[viewPlaceholderAttributeName] = instance.cid;
- var expandTokens = options.hash['expand-tokens'];
- return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, '', expandTokens ? this : null));
- });
- var helper = Handlebars.helpers[name];
- return helper;
- };
- Thorax.View.on('append', function(scope, callback) {
- (scope || this.$el).find('[' + viewPlaceholderAttributeName + ']').forEach(function(el) {
- var placeholderId = el.getAttribute(viewPlaceholderAttributeName),
- view = this.children[placeholderId];
- if (view) {
- //see if the view helper declared an override for the view
- //if not, ensure the view has been rendered at least once
- if (viewTemplateOverrides[placeholderId]) {
- view.render(viewTemplateOverrides[placeholderId]);
- delete viewTemplateOverrides[placeholderId];
- } else {
- view.ensureRendered();
- }
- $(el).replaceWith(view.el);
- callback && callback(view.el);
- }
- }, this);
- });
- /**
- * Clones the helper options, dropping items that are known to change
- * between rendering cycles as appropriate.
- */
- function cloneHelperOptions(options) {
- var ret = _.pick(options, 'fn', 'inverse', 'hash', 'data');
- ret.data = _.omit(options.data, 'cid', 'view', 'yield');
- return ret;
- }
- /**
- * Checks for basic equality between two sets of parameters for a helper view.
- *
- * Checked fields include:
- * - _helperName
- * - All args
- * - Hash
- * - Data
- * - Function and Invert (id based if possible)
- *
- * This method allows us to determine if the inputs to a given view are the same. If they
- * are then we make the assumption that the rendering will be the same (or the child view will
- * otherwise rerendering it by monitoring it's parameters as necessary) and reuse the view on
- * rerender of the parent view.
- */
- function compareHelperOptions(a, b) {
- function compareValues(a, b) {
- return _.every(a, function(value, key) {
- return b[key] === value;
- });
- }
- if (a._helperName !== b._helperName) {
- return false;
- }
- a = a._helperOptions;
- b = b._helperOptions;
- // Implements a first level depth comparison
- return a.args.length === b.args.length
- && compareValues(a.args, b.args)
- && _.isEqual(_.keys(a.options), _.keys(b.options))
- && _.every(a.options, function(value, key) {
- if (key === 'data' || key === 'hash') {
- return compareValues(a.options[key], b.options[key]);
- } else if (key === 'fn' || key === 'inverse') {
- return b.options[key] === value
- || (value && _.has(value, 'program') && ((b.options[key] || {}).program === value.program));
- }
- return b.options[key] === value;
- });
- }
- ;;
- /*global getValue, inheritVars, walkInheritTree */
- function dataObject(type, spec) {
- spec = inheritVars[type] = _.defaults({
- name: '_' + type + 'Events',
- event: true
- }, spec);
- // Add a callback in the view constructor
- spec.ctor = function() {
- if (this[type]) {
- // Need to null this.model/collection so setModel/Collection will
- // not treat it as the old model/collection and immediately return
- var object = this[type];
- this[type] = null;
- this[spec.set](object);
- }
- };
- function setObject(dataObject, options) {
- var old = this[type],
- $el = getValue(this, spec.$el);
- if (dataObject === old) {
- return this;
- }
- if (old) {
- this.unbindDataObject(old);
- }
- if (dataObject) {
- this[type] = dataObject;
- if (spec.loading) {
- spec.loading.call(this);
- }
- this.bindDataObject(type, dataObject, _.extend({}, this.options, options));
- $el.attr(spec.cidAttrName, dataObject.cid);
- dataObject.trigger('set', dataObject, old);
- } else {
- this[type] = false;
- if (spec.change) {
- spec.change.call(this, false);
- }
- $el.removeAttr(spec.cidAttrName);
- }
- this.trigger('change:data-object', type, dataObject, old);
- return this;
- }
- Thorax.View.prototype[spec.set] = setObject;
- }
- _.extend(Thorax.View.prototype, {
- bindDataObject: function(type, dataObject, options) {
- if (this._boundDataObjectsByCid[dataObject.cid]) {
- return false;
- }
- this._boundDataObjectsByCid[dataObject.cid] = dataObject;
- var options = this._modifyDataObjectOptions(dataObject, _.extend({}, inheritVars[type].defaultOptions, options));
- this._objectOptionsByCid[dataObject.cid] = options;
- bindEvents.call(this, type, dataObject, this.constructor);
- bindEvents.call(this, type, dataObject, this);
- var spec = inheritVars[type];
- spec.bindCallback && spec.bindCallback.call(this, dataObject, options);
- if (dataObject.shouldFetch && dataObject.shouldFetch(options)) {
- loadObject(dataObject, options);
- } else if (inheritVars[type].change) {
- // want to trigger built in rendering without triggering event on model
- inheritVars[type].change.call(this, dataObject, options);
- }
- return true;
- },
- unbindDataObject: function (dataObject) {
- if (!this._boundDataObjectsByCid[dataObject.cid]) {
- return false;
- }
- delete this._boundDataObjectsByCid[dataObject.cid];
- this.stopListening(dataObject);
- delete this._objectOptionsByCid[dataObject.cid];
- return true;
- },
- _modifyDataObjectOptions: function(dataObject, options) {
- return options;
- }
- });
- function bindEvents(type, target, source) {
- var context = this;
- walkInheritTree(source, '_' + type + 'Events', true, function(event) {
- // getEventCallback will resolve if it is a string or a method
- // and return a method
- context.listenTo(target, event[0], _.bind(getEventCallback(event[1], context), event[2] || context));
- });
- }
- function loadObject(dataObject, options) {
- if (dataObject.load) {
- dataObject.load(function() {
- options && options.success && options.success(dataObject);
- }, options);
- } else {
- dataObject.fetch(options);
- }
- }
- function getEventCallback(callback, context) {
- if (typeof callback === 'function') {
- return callback;
- } else {
- return context[callback];
- }
- }
- ;;
- /*global createRegistryWrapper, dataObject, getValue */
- var modelCidAttributeName = 'data-model-cid';
- Thorax.Model = Backbone.Model.extend({
- isEmpty: function() {
- return !this.isPopulated();
- },
- isPopulated: function() {
- // We are populated if we have attributes set
- var attributes = _.clone(this.attributes),
- defaults = getValue(this, 'defaults') || {};
- for (var default_key in defaults) {
- if (attributes[default_key] != defaults[default_key]) {
- return true;
- }
- delete attributes[default_key];
- }
- var keys = _.keys(attributes);
- return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
- },
- shouldFetch: function(options) {
- // url() will throw if model has no `urlRoot` and no `collection`
- // or has `collection` and `collection` has no `url`
- var url;
- try {
- url = getValue(this, 'url');
- } catch(e) {
- url = false;
- }
- return options.fetch && !!url && !this.isPopulated();
- }
- });
- Thorax.Models = {};
- createRegistryWrapper(Thorax.Model, Thorax.Models);
- dataObject('model', {
- set: 'setModel',
- defaultOptions: {
- render: true,
- fetch: true,
- success: false,
- errors: true
- },
- change: onModelChange,
- $el: '$el',
- cidAttrName: modelCidAttributeName
- });
- function onModelChange(model) {
- var modelOptions = model && this._objectOptionsByCid[model.cid];
- // !modelOptions will be true when setModel(false) is called
- if (!modelOptions || (modelOptions && modelOptions.render)) {
- this.render();
- }
- }
- Thorax.View.on({
- model: {
- error: function(model, errors) {
- if (this._objectOptionsByCid[model.cid].errors) {
- this.trigger('error', errors, model);
- }
- },
- change: function(model) {
- onModelChange.call(this, model);
- }
- }
- });
- $.fn.model = function(view) {
- var $this = $(this),
- modelElement = $this.closest('[' + modelCidAttributeName + ']'),
- modelCid = modelElement && modelElement.attr(modelCidAttributeName);
- if (modelCid) {
- var view = view || $this.view();
- if (view && view.model && view.model.cid === modelCid) {
- return view.model || false;
- }
- var collection = $this.collection(view);
- if (collection) {
- return collection.get(modelCid);
- }
- }
- return false;
- };
- ;;
- /*global createRegistryWrapper, dataObject, getEventCallback, getValue, modelCidAttributeName, viewCidAttributeName */
- var _fetch = Backbone.Collection.prototype.fetch,
- _reset = Backbone.Collection.prototype.reset,
- collectionCidAttributeName = 'data-collection-cid',
- collectionEmptyAttributeName = 'data-collection-empty',
- collectionElementAttributeName = 'data-collection-element',
- ELEMENT_NODE_TYPE = 1;
- Thorax.Collection = Backbone.Collection.extend({
- model: Thorax.Model || Backbone.Model,
- initialize: function() {
- this.cid = _.uniqueId('collection');
- return Backbone.Collection.prototype.initialize.apply(this, arguments);
- },
- isEmpty: function() {
- if (this.length > 0) {
- return false;
- } else {
- return this.length === 0 && this.isPopulated();
- }
- },
- isPopulated: function() {
- return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
- },
- shouldFetch: function(options) {
- return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
- },
- fetch: function(options) {
- options = options || {};
- var success = options.success;
- options.success = function(collection, response) {
- collection._fetched = true;
- success && success(collection, response);
- };
- return _fetch.apply(this, arguments);
- },
- reset: function(models, options) {
- this._fetched = !!models;
- return _reset.call(this, models, options);
- }
- });
- Thorax.Collections = {};
- createRegistryWrapper(Thorax.Collection, Thorax.Collections);
- dataObject('collection', {
- set: 'setCollection',
- bindCallback: onSetCollection,
- defaultOptions: {
- render: true,
- fetch: true,
- success: false,
- errors: true
- },
- change: onCollectionReset,
- $el: 'getCollectionElement',
- cidAttrName: collectionCidAttributeName
- });
- _.extend(Thorax.View.prototype, {
- _collectionSelector: '[' + collectionElementAttributeName + ']',
- //appendItem(model [,index])
- //appendItem(html_string, index)
- //appendItem(view, index)
- appendItem: function(model, index, options) {
- //empty item
- if (!model) {
- return;
- }
- var itemView,
- $el = this.getCollectionElement();
- options = _.defaults(options || {}, {
- filter: true
- });
- //if index argument is a view
- index && index.el && (index = $el.children().indexOf(index.el) + 1);
- //if argument is a view, or html string
- if (model.el || typeof model === 'string') {
- itemView = model;
- model = false;
- } else {
- index = index || this.collection.indexOf(model) || 0;
- itemView = this.renderItem(model, index);
- }
- if (itemView) {
- itemView.cid && this._addChild(itemView);
- //if the renderer's output wasn't contained in a tag, wrap it in a div
- //plain text, or a mixture of top level text nodes and element nodes
- //will get wrapped
- if (typeof itemView === 'string' && !itemView.match(/^\s*</m)) {
- itemView = '<div>' + itemView + '</div>';
- }
- var itemElement = itemView.el ? [itemView.el] : _.filter($(itemView), function(node) {
- //filter out top level whitespace nodes
- return node.nodeType === ELEMENT_NODE_TYPE;
- });
- model && $(itemElement).attr(modelCidAttributeName, model.cid);
- var previousModel = index > 0 ? this.collection.at(index - 1) : false;
- if (!previousModel) {
- $el.prepend(itemElement);
- } else {
- //use last() as appendItem can accept multiple nodes from a template
- var last = $el.find('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
- last.after(itemElement);
- }
- this.trigger('append', null, function(el) {
- el.setAttribute(modelCidAttributeName, model.cid);
- });
- !options.silent && this.trigger('rendered:item', this, this.collection, model, itemElement, index);
- options.filter && applyItemVisiblityFilter.call(this, model);
- }
- return itemView;
- },
- //Â updateItem only useful if there is no item view, otherwise
- //Â itemView.render() provides the same functionality
- updateItem: function(model) {
- this.removeItem(model);
- this.appendItem(model);
- },
- removeItem: function(model) {
- var $el = this.getCollectionElement(),
- viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
- if (!viewEl.length) {
- return false;
- }
- viewEl.remove();
- var viewCid = viewEl.attr(viewCidAttributeName),
- child = this.children[viewCid];
- if (child) {
- this._removeChild(child);
- child.destroy();
- }
- return true;
- },
- renderCollection: function() {
- this.ensureRendered();
- if (!this.collectionRenderer) {
- return;
- }
- if (this.collection) {
- if (this.collection.isEmpty()) {
- handleChangeFromNotEmptyToEmpty.call(this);
- } else {
- handleChangeFromEmptyToNotEmpty.call(this);
- this.collection.forEach(function(item, i) {
- this.appendItem(item, i);
- }, this);
- }
- this.trigger('rendered:collection', this, this.collection);
- applyVisibilityFilter.call(this);
- } else {
- handleChangeFromNotEmptyToEmpty.call(this);
- }
- },
- emptyClass: 'empty',
- renderEmpty: function() {
- if (this.emptyView) {
- var viewOptions = {};
- if (this.emptyTemplate) {
- viewOptions.template = this.emptyTemplate;
- }
- var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
- view.ensureRendered();
- return view;
- } else {
- if (!this.emptyTemplate) {
- this.emptyTemplate = Thorax.Util.getTemplate(this.name + '-empty', true);
- }
- return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
- }
- },
- renderItem: function(model, i) {
- if (this.itemView) {
- var viewOptions = {
- model: model
- };
- if (this.itemTemplate) {
- viewOptions.template = this.itemTemplate;
- }
- var view = Thorax.Util.getViewInstance(this.itemView, viewOptions);
- view.ensureRendered();
- return view;
- } else {
- if (!this.itemTemplate) {
- this.itemTemplate = Thorax.Util.getTemplate(this.name + '-item');
- }
- return this.renderTemplate(this.itemTemplate, this.itemContext(model, i));
- }
- },
- itemContext: function(model /*, i */) {
- return model.attributes;
- },
- appendEmpty: function() {
- var $el = this.getCollectionElement();
- $el.empty();
- var emptyContent = this.renderEmpty();
- emptyContent && this.appendItem(emptyContent, 0, {
- silent: true,
- filter: false
- });
- this.trigger('rendered:empty', this, this.collection);
- },
- getCollectionElement: function() {
- var element = this.$(this._collectionSelector);
- return element.length === 0 ? this.$el : element;
- },
- // Events that will only be bound to "this.collection"
- _collectionRenderingEvents: {
- reset: onCollectionReset,
- sort: onCollectionReset,
- filter: function() {
- applyVisibilityFilter.call(this);
- },
- change: function(model) {
- // If we rendered with item views, model changes will be observed
- // by the generated item view but if we rendered with templates
- // then model changes need to be bound as nothing is watching
- !this.itemView && this.updateItem(model);
- applyItemVisiblityFilter.call(this, model);
- },
- add: function(model) {
- var $el = this.getCollectionElement();
- this.collection.length === 1 && $el.length && handleChangeFromEmptyToNotEmpty.call(this);
- if ($el.length) {
- var index = this.collection.indexOf(model);
- this.appendItem(model, index);
- }
- },
- remove: function(model) {
- var $el = this.getCollectionElement();
- this.removeItem(model);
- this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
- }
- }
- });
- Thorax.View.on({
- collection: {
- error: function(collection, message) {
- if (this._objectOptionsByCid[collection.cid].errors) {
- this.trigger('error', message, collection);
- }
- }
- }
- });
- function onCollectionReset(collection) {
- var options = collection && this._objectOptionsByCid[collection.cid];
- // we would want to still render in the case that the
- // collection has transitioned to being falsy
- if (!collection || (options && options.render)) {
- this.renderCollection();
- }
- }
- function onSetCollection(collection) {
- if (this.collectionRenderer && collection) {
- _.each(this._collectionRenderingEvents, function(callback, eventName) {
- // getEventCallback will resolve if it is a string or a method
- // and return a method
- this.listenTo(collection, eventName, getEventCallback(callback, this));
- }, this);
- }
- }
- function applyVisibilityFilter() {
- if (this.itemFilter) {
- this.collection.forEach(function(model) {
- applyItemVisiblityFilter.call(this, model);
- }, this);
- }
- }
- function applyItemVisiblityFilter(model) {
- var $el = this.getCollectionElement();
- this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
- }
- function itemShouldBeVisible(model) {
- return this.itemFilter(model, this.collection.indexOf(model));
- }
- function handleChangeFromEmptyToNotEmpty() {
- var $el = this.getCollectionElement();
- this.emptyClass && $el.removeClass(this.emptyClass);
- $el.removeAttr(collectionEmptyAttributeName);
- $el.empty();
- }
- function handleChangeFromNotEmptyToEmpty() {
- var $el = this.getCollectionElement();
- this.emptyClass && $el.addClass(this.emptyClass);
- $el.attr(collectionEmptyAttributeName, true);
- this.appendEmpty();
- }
- //$(selector).collection() helper
- $.fn.collection = function(view) {
- if (view && view.collection) {
- return view.collection;
- }
- var $this = $(this),
- collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
- collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
- if (collectionCid) {
- view = $this.view();
- if (view) {
- return view.collection;
- }
- }
- return false;
- };
- ;;
- /*global inheritVars */
- inheritVars.model.defaultOptions.populate = true;
- var oldModelChange = inheritVars.model.change;
- inheritVars.model.change = function() {
- oldModelChange.apply(this, arguments);
- // TODO : What can we do to remove this duplication?
- var modelOptions = this.model && this._objectOptionsByCid[this.model.cid];
- if (modelOptions && modelOptions.populate) {
- this.populate(this.model.attributes, modelOptions.populate === true ? {} : modelOptions.populate);
- }
- };
- inheritVars.model.defaultOptions.populate = true;
- _.extend(Thorax.View.prototype, {
- //serializes a form present in the view, returning the serialized data
- //as an object
- //pass {set:false} to not update this.model if present
- //can pass options, callback or event in any order
- serialize: function() {
- var callback, options, event;
- //ignore undefined arguments in case event was null
- for (var i = 0; i < arguments.length; ++i) {
- if (typeof arguments[i] === 'function') {
- callback = arguments[i];
- } else if (typeof arguments[i] === 'object') {
- if ('stopPropagation' in arguments[i] && 'preventDefault' in arguments[i]) {
- event = arguments[i];
- } else {
- options = arguments[i];
- }
- }
- }
- if (event && !this._preventDuplicateSubmission(event)) {
- return;
- }
- options = _.extend({
- set: true,
- validate: true,
- children: true,
- silent: true
- }, options || {});
- var attributes = options.attributes || {};
- //callback has context of element
- var view = this;
- var errors = [];
- eachNamedInput.call(this, options, function() {
- var value = view._getInputValue(this, options, errors);
- if (typeof value !== 'undefined') {
- objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'serialize'}, function(object, key) {
- if (!object[key]) {
- object[key] = value;
- } else if (_.isArray(object[key])) {
- object[key].push(value);
- } else {
- object[key] = [object[key], value];
- }
- });
- }
- });
- this.trigger('serialize', attributes, options);
- if (options.validate) {
- var validateInputErrors = this.validateInput(attributes);
- if (validateInputErrors && validateInputErrors.length) {
- errors = errors.concat(validateInputErrors);
- }
- this.trigger('validate', attributes, errors, options);
- if (errors.length) {
- this.trigger('error', errors);
- return;
- }
- }
- if (options.set && this.model) {
- if (!this.model.set(attributes, {silent: options.silent})) {
- return false;
- }
- }
- callback && callback.call(this, attributes, _.bind(resetSubmitState, this));
- return attributes;
- },
- _preventDuplicateSubmission: function(event, callback) {
- event.preventDefault();
- var form = $(event.target);
- if ((event.target.tagName || '').toLowerCase() !== 'form') {
- // Handle non-submit events by gating on the form
- form = $(event.target).closest('form');
- }
- if (!form.attr('data-submit-wait')) {
- form.attr('data-submit-wait', 'true');
- if (callback) {
- callback.call(this, event);
- }
- return true;
- } else {
- return false;
- }
- },
- //populate a form from the passed attributes or this.model if present
- populate: function(attributes, options) {
- options = _.extend({
- children: true
- }, options || {});
- var value, attributes = attributes || this._getContext();
- //callback has context of element
- eachNamedInput.call(this, options, function() {
- objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'populate'}, function(object, key) {
- if (object && typeof (value = object[key]) !== 'undefined') {
- //will only execute if we have a name that matches the structure in attributes
- if (this.type === 'checkbox' && _.isBoolean(value)) {
- this.checked = value;
- } else if (this.type === 'checkbox' || this.type === 'radio') {
- this.checked = value == this.value;
- } else {
- this.value = value;
- }
- }
- });
- });
- this.trigger('populate', attributes);
- },
- //perform form validation, implemented by child class
- validateInput: function(/* attributes, options, errors */) {},
- _getInputValue: function(input /* , options, errors */) {
- if (input.type === 'checkbox' || input.type === 'radio') {
- if (input.checked) {
- return input.value;
- }
- } else if (input.multiple === true) {
- var values = [];
- $('option', input).each(function() {
- if (this.selected) {
- values.push(this.value);
- }
- });
- return values;
- } else {
- return input.value;
- }
- }
- });
- Thorax.View.on({
- error: function() {
- resetSubmitState.call(this);
- // If we errored with a model we want to reset the content but leave the UI
- // intact. If the user updates the data and serializes any overwritten data
- // will be restored.
- if (this.model && this.model.previousAttributes) {
- this.model.set(this.model.previousAttributes(), {
- silent: true
- });
- }
- },
- deactivated: function() {
- resetSubmitState.call(this);
- }
- });
- function eachNamedInput(options, iterator, context) {
- var i = 0,
- self = this;
- this.$('select,input,textarea', options.root || this.el).each(function() {
- if (!options.children) {
- if (self !== $(this).view({helper: false})) {
- return;
- }
- }
- if (this.type !== 'button' && this.type !== 'cancel' && this.type !== 'submit' && this.name && this.name !== '') {
- iterator.call(context || this, i, this);
- ++i;
- }
- });
- }
- //calls a callback with the correct object fragment and key from a compound name
- function objectAndKeyFromAttributesAndName(attributes, name, options, callback) {
- var key,
- object = attributes,
- keys = name.split('['),
- mode = options.mode;
- for (var i = 0; i < keys.length - 1; ++i) {
- key = keys[i].replace(']', '');
- if (!object[key]) {
- if (mode === 'serialize') {
- object[key] = {};
- } else {
- return callback.call(this, false, key);
- }
- }
- object = object[key];
- }
- key = keys[keys.length - 1].replace(']', '');
- callback.call(this, object, key);
- }
- function resetSubmitState() {
- this.$('form').removeAttr('data-submit-wait');
- }
- ;;
- var layoutCidAttributeName = 'data-layout-cid';
- Thorax.LayoutView = Thorax.View.extend({
- render: function(output) {
- //TODO: fixme, lumbar inserts templates after JS, most of the time this is fine
- //but Application will be created in init.js (unlike most views)
- //so need to put this here so the template will be picked up
- var layoutTemplate;
- if (this.name) {
- layoutTemplate = Thorax.Util.getTemplate(this.name, true);
- }
- //a template is optional in a layout
- if (output || this.template || layoutTemplate) {
- //but if present, it must have embedded an element containing layoutCidAttributeName
- var response = Thorax.View.prototype.render.call(this, output || this.template || layoutTemplate);
- ensureLayoutViewsTargetElement.call(this);
- return response;
- } else {
- ensureLayoutCid.call(this);
- }
- },
- setView: function(view, options) {
- options = _.extend({
- scroll: true,
- destroy: true
- }, options || {});
- if (typeof view === 'string') {
- view = new (Thorax.Util.registryGet(Thorax, 'Views', view, false))();
- }
- this.ensureRendered();
- var oldView = this._view;
- if (view === oldView) {
- return false;
- }
- if (options.destroy && view) {
- view._shouldDestroyOnNextSetView = true;
- }
- this.trigger('change:view:start', view, oldView, options);
- if (oldView) {
- this._removeChild(oldView);
- oldView.$el.remove();
- triggerLifecycleEvent.call(oldView, 'deactivated', options);
- if (oldView._shouldDestroyOnNextSetView) {
- oldView.destroy();
- }
- }
- if (view) {
- triggerLifecycleEvent.call(this, 'activated', options);
- view.trigger('activated', options);
- this._addChild(view);
- this._view = view;
- this._view.appendTo(getLayoutViewsTargetElement.call(this));
- } else {
- this._view = undefined;
- }
- this.trigger('change:view:end', view, oldView, options);
- return view;
- },
- getView: function() {
- return this._view;
- }
- });
- Handlebars.registerHelper('layout', function(options) {
- options.hash[layoutCidAttributeName] = getOptionsData(options).view.cid;
- return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, '', this));
- });
- function triggerLifecycleEvent(eventName, options) {
- options = options || {};
- options.target = this;
- this.trigger(eventName, options);
- _.each(this.children, function(child) {
- child.trigger(eventName, options);
- });
- }
- function ensureLayoutCid() {
- ++this._renderCount;
- //set the layoutCidAttributeName on this.$el if there was no template
- this.$el.attr(layoutCidAttributeName, this.cid);
- }
- function ensureLayoutViewsTargetElement() {
- if (!this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0]) {
- throw new Error('No layout element found in ' + (this.name || this.cid));
- }
- }
- function getLayoutViewsTargetElement() {
- return this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0] || this.el[0] || this.el;
- }
- ;;
- /*global createRegistryWrapper */
- //Router
- function initializeRouter() {
- Backbone.history || (Backbone.history = new Backbone.History());
- Backbone.history.on('route', onRoute, this);
- //router does not have a built in destroy event
- //but ViewController does
- this.on('destroyed', function() {
- Backbone.history.off('route', onRoute, this);
- });
- }
- Thorax.Router = Backbone.Router.extend({
- constructor: function() {
- var response = Thorax.Router.__super__.constructor.apply(this, arguments);
- initializeRouter.call(this);
- return response;
- },
- route: function(route, name, callback) {
- if (!callback) {
- callback = this[name];
- }
- //add a route:before event that is fired before the callback is called
- return Backbone.Router.prototype.route.call(this, route, name, function() {
- this.trigger.apply(this, ['route:before', route, name].concat(Array.prototype.slice.call(arguments)));
- return callback.apply(this, arguments);
- });
- }
- });
- Thorax.Routers = {};
- createRegistryWrapper(Thorax.Router, Thorax.Routers);
- function onRoute(router /* , name */) {
- if (this === router) {
- this.trigger.apply(this, ['route'].concat(Array.prototype.slice.call(arguments, 1)));
- }
- }
- ;;
- Thorax.CollectionHelperView = Thorax.HelperView.extend({
- // Forward render events to the parent
- events: {
- 'rendered:item': forwardRenderEvent('rendered:item'),
- 'rendered:collection': forwardRenderEvent('rendered:collection'),
- 'rendered:empty': forwardRenderEvent('rendered:empty')
- },
- collectionRenderer: true,
- constructor: function(options) {
- _.each(collectionOptionNames, function(viewAttributeName, helperOptionName) {
- if (options.options[helperOptionName]) {
- var value = options.options[helperOptionName];
- if (viewAttributeName === 'itemTemplate' || viewAttributeName === 'emptyTemplate') {
- value = Thorax.Util.getTemplate(value);
- }
- options[viewAttributeName] = value;
- }
- });
- // Handlebars.VM.noop is passed in the handlebars options object as
- // a default for fn and inverse, if a block was present. Need to
- // check to ensure we don't pick the empty / null block up.
- if (!options.itemTemplate && options.template && options.template !== Handlebars.VM.noop) {
- options.itemTemplate = options.template;
- options.template = Handlebars.VM.noop;
- }
- if (!options.emptyTemplate && options.inverse && options.inverse !== Handlebars.VM.noop) {
- options.emptyTemplate = options.inverse;
- options.inverse = Handlebars.VM.noop;
- }
- !options.template && (options.template = Handlebars.VM.noop);
- var response = Thorax.HelperView.call(this, options);
- if (this.parent.name) {
- if (!this.emptyTemplate) {
- this.emptyTemplate = Thorax.Util.getTemplate(this.parent.name + '-empty', true);
- }
- if (!this.itemTemplate) {
- this.itemTemplate = Thorax.Util.getTemplate(this.parent.name + '-item', true);
- }
- }
- return response;
- },
- itemContext: function() {
- return this.parent.itemContext.apply(this.parent, arguments);
- },
- setAsPrimaryCollectionHelper: function() {
- _.each(forwardableProperties, function(propertyName) {
- forwardMissingProperty.call(this, propertyName);
- }, this);
- if (this.parent.itemFilter && !this.itemFilter) {
- this.itemFilter = function() {
- return this.parent.itemFilter.apply(this.parent, arguments);
- };
- }
- }
- });
- var collectionOptionNames = {
- 'item-template': 'itemTemplate',
- 'empty-template': 'emptyTemplate',
- 'item-view': 'itemView',
- 'empty-view': 'emptyView',
- 'empty-class': 'emptyClass'
- };
- function forwardRenderEvent(eventName) {
- return function() {
- var args = _.toArray(arguments);
- args.unshift(eventName);
- this.parent.trigger.apply(this.parent, args);
- }
- }
- var forwardableProperties = [
- 'itemTemplate',
- 'itemView',
- 'emptyTemplate',
- 'emptyView'
- ];
- function forwardMissingProperty(propertyName) {
- var parent = getParent(this);
- if (!this[propertyName]) {
- var prop = parent[propertyName];
- if (prop){
- this[propertyName] = prop;
- }
- }
- }
- Handlebars.registerViewHelper('collection', Thorax.CollectionHelperView, function(collection, view) {
- if (arguments.length === 1) {
- view = collection;
- collection = view.parent.collection;
- collection && view.setAsPrimaryCollectionHelper();
- view.$el.attr(collectionElementAttributeName, 'true');
- // propagate future changes to the parent's collection object
- // to the helper view
- view.listenTo(view.parent, 'change:data-object', function(type, dataObject) {
- if (type === 'collection') {
- view.setAsPrimaryCollectionHelper();
- view.setCollection(dataObject);
- }
- });
- }
- collection && view.setCollection(collection);
- });
- Handlebars.registerHelper('collection-element', function(options) {
- var hash = options.hash;
- normalizeHTMLAttributeOptions(hash);
- hash.tagName = hash.tagName || 'div';
- hash[collectionElementAttributeName] = true;
- return new Handlebars.SafeString(Thorax.Util.tag.call(this, hash, '', this));
- });
- ;;
- Handlebars.registerHelper('empty', function(dataObject, options) {
- if (arguments.length === 1) {
- options = dataObject;
- }
- var view = getOptionsData(options).view;
- if (arguments.length === 1) {
- dataObject = view.model;
- }
- // listeners for the empty helper rather than listeners
- // that are themselves empty
- if (!view._emptyListeners) {
- view._emptyListeners = {};
- }
- // duck type check for collection
- if (dataObject && !view._emptyListeners[dataObject.cid] && dataObject.models && ('length' in dataObject)) {
- view._emptyListeners[dataObject.cid] = true;
- view.listenTo(dataObject, 'remove', function() {
- if (dataObject.length === 0) {
- view.render();
- }
- });
- view.listenTo(dataObject, 'add', function() {
- if (dataObject.length === 1) {
- view.render();
- }
- });
- view.listenTo(dataObject, 'reset', function() {
- view.render();
- });
- }
- return !dataObject || dataObject.isEmpty() ? options.fn(this) : options.inverse(this);
- });
- ;;
- Handlebars.registerHelper('template', function(name, options) {
- var context = _.extend({fn: options && options.fn}, this, options ? options.hash : {});
- var output = getOptionsData(options).view.renderTemplate(name, context);
- return new Handlebars.SafeString(output);
- });
- Handlebars.registerHelper('yield', function(options) {
- return getOptionsData(options).yield && options.data.yield();
- });
- ;;
- Handlebars.registerHelper('url', function(url) {
- var fragment;
- if (arguments.length > 2) {
- fragment = _.map(_.head(arguments, arguments.length - 1), encodeURIComponent).join('/');
- } else {
- var options = arguments[1],
- hash = (options && options.hash) || options;
- if (hash && hash['expand-tokens']) {
- fragment = Thorax.Util.expandToken(url, this);
- } else {
- fragment = url;
- }
- }
- return (Backbone.history._hasPushState ? Backbone.history.options.root : '#') + fragment;
- });
- ;;
- /*global viewTemplateOverrides */
- Handlebars.registerViewHelper('view', {
- factory: function(args, options) {
- var View = args.length >= 1 ? args[0] : Thorax.View;
- return Thorax.Util.getViewInstance(View, options.options);
- },
- callback: function() {
- var instance = arguments[arguments.length-1],
- options = instance._helperOptions.options,
- placeholderId = instance.cid;
- if (options.fn) {
- viewTemplateOverrides[placeholderId] = options.fn;
- }
- }
- });
- ;;
- var callMethodAttributeName = 'data-call-method',
- triggerEventAttributeName = 'data-trigger-event';
- Handlebars.registerHelper('button', function(method, options) {
- if (arguments.length === 1) {
- options = method;
- method = options.hash.method;
- }
- var hash = options.hash,
- expandTokens = hash['expand-tokens'];
- delete hash['expand-tokens'];
- if (!method && !options.hash.trigger) {
- throw new Error("button helper must have a method name as the first argument or a 'trigger', or a 'method' attribute specified.");
- }
- normalizeHTMLAttributeOptions(hash);
- hash.tagName = hash.tagName || 'button';
- hash.trigger && (hash[triggerEventAttributeName] = hash.trigger);
- delete hash.trigger;
- method && (hash[callMethodAttributeName] = method);
- return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
- });
- Handlebars.registerHelper('link', function() {
- var args = _.toArray(arguments),
- options = args.pop(),
- hash = options.hash,
- // url is an array that will be passed to the url helper
- url = args.length === 0 ? [hash.href] : args,
- expandTokens = hash['expand-tokens'];
- delete hash['expand-tokens'];
- if (!url[0] && url[0] !== '') {
- throw new Error("link helper requires an href as the first argument or an 'href' attribute");
- }
- normalizeHTMLAttributeOptions(hash);
- url.push(options);
- hash.href = Handlebars.helpers.url.apply(this, url);
- hash.tagName = hash.tagName || 'a';
- hash.trigger && (hash[triggerEventAttributeName] = options.hash.trigger);
- delete hash.trigger;
- hash[callMethodAttributeName] = '_anchorClick';
- return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
- });
- var clickSelector = '[' + callMethodAttributeName + '], [' + triggerEventAttributeName + ']';
- function handleClick(event) {
- var target = $(event.target),
- view = target.view({helper: false}),
- methodName = target.attr(callMethodAttributeName),
- eventName = target.attr(triggerEventAttributeName),
- methodResponse = false;
- methodName && (methodResponse = view[methodName].call(view, event));
- eventName && view.trigger(eventName, event);
- target.tagName === "A" && methodResponse === false && event.preventDefault();
- }
- var lastClickHandlerEventName;
- function registerClickHandler() {
- unregisterClickHandler();
- lastClickHandlerEventName = Thorax._fastClickEventName || 'click';
- $(document).on(lastClickHandlerEventName, clickSelector, handleClick);
- }
- function unregisterClickHandler() {
- lastClickHandlerEventName && $(document).off(lastClickHandlerEventName, clickSelector, handleClick);
- }
- $(document).ready(function() {
- if (!Thorax._fastClickEventName) {
- registerClickHandler();
- }
- });
- ;;
- var elementPlaceholderAttributeName = 'data-element-tmp';
- Handlebars.registerHelper('element', function(element, options) {
- normalizeHTMLAttributeOptions(options.hash);
- var cid = _.uniqueId('element'),
- declaringView = getOptionsData(options).view,
- htmlAttributes = _.pick(options.hash, htmlAttributesToCopy);
- htmlAttributes[elementPlaceholderAttributeName] = cid;
- declaringView._elementsByCid || (declaringView._elementsByCid = {});
- declaringView._elementsByCid[cid] = element;
- return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes));
- });
- Thorax.View.on('append', function(scope, callback) {
- (scope || this.$el).find('[' + elementPlaceholderAttributeName + ']').forEach(function(el) {
- var $el = $(el),
- cid = $el.attr(elementPlaceholderAttributeName),
- element = this._elementsByCid[cid];
- // A callback function may be specified as the value
- if (_.isFunction(element)) {
- element = element.call(this);
- }
- $el.replaceWith(element);
- callback && callback(element);
- }, this);
- });
- ;;
- Handlebars.registerHelper('super', function(options) {
- var declaringView = getOptionsData(options).view,
- parent = declaringView.constructor && declaringView.constructor.__super__;
- if (parent) {
- var template = parent.template;
- if (!template) {
- if (!parent.name) {
- throw new Error('Cannot use super helper when parent has no name or template.');
- }
- template = Thorax.Util.getTemplate(parent.name, false);
- }
- if (typeof template === 'string') {
- template = Handlebars.compile(template, {data: true});
- }
- return new Handlebars.SafeString(template(this, options));
- } else {
- return '';
- }
- });
- ;;
- /*global collectionOptionNames, inheritVars */
- var loadStart = 'load:start',
- loadEnd = 'load:end',
- rootObject;
- Thorax.setRootObject = function(obj) {
- rootObject = obj;
- };
- Thorax.loadHandler = function(start, end, context) {
- var loadCounter = _.uniqueId();
- return function(message, background, object) {
- var self = context || this;
- self._loadInfo = self._loadInfo || [];
- var loadInfo = self._loadInfo[loadCounter];
- function startLoadTimeout() {
- // If the timeout has been set already but has not triggered yet do nothing
- // Otherwise set a new timeout (either initial or for going from background to
- // non-background loading)
- if (loadInfo.timeout && !loadInfo.run) {
- return;
- }
- var loadingTimeout = self._loadingTimeoutDuration !== undefined ?
- self._loadingTimeoutDuration : Thorax.View.prototype._loadingTimeoutDuration;
- loadInfo.timeout = setTimeout(function() {
- try {
- loadInfo.run = true;
- start.call(self, loadInfo.message, loadInfo.background, loadInfo);
- } catch (e) {
- Thorax.onException('loadStart', e);
- }
- }, loadingTimeout * 1000);
- }
- if (!loadInfo) {
- loadInfo = self._loadInfo[loadCounter] = _.extend({
- events: [],
- timeout: 0,
- message: message,
- background: !!background
- }, Backbone.Events);
- startLoadTimeout();
- } else {
- clearTimeout(loadInfo.endTimeout);
- loadInfo.message = message;
- if (!background && loadInfo.background) {
- loadInfo.background = false;
- startLoadTimeout();
- }
- }
- // Prevent binds to the same object multiple times as this can cause very bad things
- // to happen for the load;load;end;end execution flow.
- if (loadInfo.events.indexOf(object) >= 0) {
- loadInfo.events.push(object);
- return;
- }
- loadInfo.events.push(object);
- object.on(loadEnd, function endCallback() {
- var loadingEndTimeout = self._loadingTimeoutEndDuration;
- if (loadingEndTimeout === void 0) {
- // If we are running on a non-view object pull the default timeout
- loadingEndTimeout = Thorax.View.prototype._loadingTimeoutEndDuration;
- }
- var events = loadInfo.events,
- index = events.indexOf(object);
- if (index >= 0) {
- events.splice(index, 1);
- if (events.indexOf(object) < 0) {
- // Last callback for this particlar object, remove the bind
- object.off(loadEnd, endCallback);
- }
- }
- if (!events.length) {
- clearTimeout(loadInfo.endTimeout);
- loadInfo.endTimeout = setTimeout(function() {
- try {
- if (!events.length) {
- var run = loadInfo.run;
- if (run) {
- // Emit the end behavior, but only if there is a paired start
- end.call(self, loadInfo.background, loadInfo);
- loadInfo.trigger(loadEnd, loadInfo);
- }
- // If stopping make sure we don't run a start
- clearTimeout(loadInfo.timeout);
- loadInfo = self._loadInfo[loadCounter] = undefined;
- }
- } catch (e) {
- Thorax.onException('loadEnd', e);
- }
- }, loadingEndTimeout * 1000);
- }
- });
- };
- };
- /**
- * Helper method for propagating load:start events to other objects.
- *
- * Forwards load:start events that occur on `source` to `dest`.
- */
- Thorax.forwardLoadEvents = function(source, dest, once) {
- function load(message, backgound, object) {
- if (once) {
- source.off(loadStart, load);
- }
- dest.trigger(loadStart, message, backgound, object);
- }
- source.on(loadStart, load);
- return {
- off: function() {
- source.off(loadStart, load);
- }
- };
- };
- //
- // Data load event generation
- //
- /**
- * Mixing for generating load:start and load:end events.
- */
- Thorax.mixinLoadable = function(target, useParent) {
- _.extend(target, {
- //loading config
- _loadingClassName: 'loading',
- _loadingTimeoutDuration: 0.33,
- _loadingTimeoutEndDuration: 0.10,
- // Propagates loading view parameters to the AJAX layer
- onLoadStart: function(message, background, object) {
- var that = useParent ? this.parent : this;
- // Protect against race conditions
- if (!that || !that.el) {
- return;
- }
- if (!that.nonBlockingLoad && !background && rootObject && rootObject !== this) {
- rootObject.trigger(loadStart, message, background, object);
- }
- $(that.el).addClass(that._loadingClassName);
- //used by loading helpers
- if (that._loadingCallbacks) {
- _.each(that._loadingCallbacks, function(callback) {
- callback();
- });
- }
- },
- onLoadEnd: function(/* background, object */) {
- var that = useParent ? this.parent : this;
- // Protect against race conditions
- if (!that || !that.el) {
- return;
- }
- $(that.el).removeClass(that._loadingClassName);
- //used by loading helpers
- if (that._loadingCallbacks) {
- _.each(that._loadingCallbacks, function(callback) {
- callback();
- });
- }
- }
- });
- };
- Thorax.mixinLoadableEvents = function(target, useParent) {
- _.extend(target, {
- loadStart: function(message, background) {
- var that = useParent ? this.parent : this;
- that.trigger(loadStart, message, background, that);
- },
- loadEnd: function() {
- var that = useParent ? this.parent : this;
- that.trigger(loadEnd, that);
- }
- });
- };
- Thorax.mixinLoadable(Thorax.View.prototype);
- Thorax.mixinLoadableEvents(Thorax.View.prototype);
- if (Thorax.HelperView) {
- Thorax.mixinLoadable(Thorax.HelperView.prototype, true);
- Thorax.mixinLoadableEvents(Thorax.HelperView.prototype, true);
- }
- Thorax.sync = function(method, dataObj, options) {
- var self = this,
- complete = options.complete;
- options.complete = function() {
- self._request = undefined;
- self._aborted = false;
- complete && complete.apply(this, arguments);
- };
- this._request = Backbone.sync.apply(this, arguments);
- return this._request;
- };
- function bindToRoute(callback, failback) {
- var fragment = Backbone.history.getFragment(),
- routeChanged = false;
- function routeHandler() {
- if (fragment === Backbone.history.getFragment()) {
- return;
- }
- routeChanged = true;
- res.cancel();
- failback && failback();
- }
- Backbone.history.on('route', routeHandler);
- function finalizer() {
- Backbone.history.off('route', routeHandler);
- if (!routeChanged) {
- callback.apply(this, arguments);
- }
- }
- var res = _.bind(finalizer, this);
- res.cancel = function() {
- Backbone.history.off('route', routeHandler);
- };
- return res;
- }
- function loadData(callback, failback, options) {
- if (this.isPopulated()) {
- return callback(this);
- }
- if (arguments.length === 2 && typeof failback !== 'function' && _.isObject(failback)) {
- options = failback;
- failback = false;
- }
- var self = this,
- routeChanged = false,
- successCallback = bindToRoute(_.bind(callback, self), function() {
- routeChanged = true;
- if (self._request) {
- self._aborted = true;
- self._request.abort();
- }
- failback && failback.call(self, false);
- });
- this.fetch(_.defaults({
- success: successCallback,
- error: failback && function() {
- if (!routeChanged) {
- failback.apply(self, [true].concat(_.toArray(arguments)));
- }
- },
- complete: function() {
- successCallback.cancel();
- }
- }, options));
- }
- function fetchQueue(options, $super) {
- if (options.resetQueue) {
- // WARN: Should ensure that loaders are protected from out of band data
- // when using this option
- this.fetchQueue = undefined;
- }
- if (!this.fetchQueue) {
- // Kick off the request
- this.fetchQueue = [options];
- options = _.defaults({
- success: flushQueue(this, this.fetchQueue, 'success'),
- error: flushQueue(this, this.fetchQueue, 'error'),
- complete: flushQueue(this, this.fetchQueue, 'complete')
- }, options);
- $super.call(this, options);
- } else {
- // Currently fetching. Queue and process once complete
- this.fetchQueue.push(options);
- }
- }
- function flushQueue(self, fetchQueue, handler) {
- return function() {
- var args = arguments;
- // Flush the queue. Executes any callback handlers that
- // may have been passed in the fetch options.
- _.each(fetchQueue, function(options) {
- if (options[handler]) {
- options[handler].apply(this, args);
- }
- }, this);
- // Reset the queue if we are still the active request
- if (self.fetchQueue === fetchQueue) {
- self.fetchQueue = undefined;
- }
- };
- }
- var klasses = [];
- Thorax.Model && klasses.push(Thorax.Model);
- Thorax.Collection && klasses.push(Thorax.Collection);
- _.each(klasses, function(DataClass) {
- var $fetch = DataClass.prototype.fetch;
- Thorax.mixinLoadableEvents(DataClass.prototype, false);
- _.extend(DataClass.prototype, {
- sync: Thorax.sync,
- fetch: function(options) {
- options = options || {};
- var self = this,
- complete = options.complete;
- options.complete = function() {
- complete && complete.apply(this, arguments);
- self.loadEnd();
- };
- self.loadStart(undefined, options.background);
- return fetchQueue.call(this, options || {}, $fetch);
- },
- load: function(callback, failback, options) {
- if (arguments.length === 2 && typeof failback !== 'function') {
- options = failback;
- failback = false;
- }
- options = options || {};
- if (!options.background && !this.isPopulated() && rootObject) {
- // Make sure that the global scope sees the proper load events here
- // if we are loading in standalone mode
- Thorax.forwardLoadEvents(this, rootObject, true);
- }
- loadData.call(this, callback, failback, options);
- }
- });
- });
- Thorax.Util.bindToRoute = bindToRoute;
- if (Thorax.Router) {
- Thorax.Router.bindToRoute = Thorax.Router.prototype.bindToRoute = bindToRoute;
- }
- // Propagates loading view parameters to the AJAX layer
- Thorax.View.prototype._modifyDataObjectOptions = function(dataObject, options) {
- options.ignoreErrors = this.ignoreFetchError;
- options.background = this.nonBlockingLoad;
- return options;
- };
- Thorax.HelperView.prototype._modifyDataObjectOptions = function(dataObject, options) {
- options.ignoreErrors = this.parent.ignoreFetchError;
- options.background = this.parent.nonBlockingLoad;
- return options;
- };
- inheritVars.collection.loading = function() {
- var loadingView = this.loadingView,
- loadingTemplate = this.loadingTemplate,
- loadingPlacement = this.loadingPlacement;
- //add "loading-view" and "loading-template" options to collection helper
- if (loadingView || loadingTemplate) {
- var callback = Thorax.loadHandler(_.bind(function() {
- var item;
- if (this.collection.length === 0) {
- this.$el.empty();
- }
- if (loadingView) {
- var instance = Thorax.Util.getViewInstance(loadingView);
- this._addChild(instance);
- if (loadingTemplate) {
- instance.render(loadingTemplate);
- } else {
- instance.render();
- }
- item = instance;
- } else {
- item = this.renderTemplate(loadingTemplate);
- }
- var index = loadingPlacement
- ? loadingPlacement.call(this)
- : this.collection.length
- ;
- this.appendItem(item, index);
- this.$el.children().eq(index).attr('data-loading-element', this.collection.cid);
- }, this), _.bind(function() {
- this.$el.find('[data-loading-element="' + this.collection.cid + '"]').remove();
- }, this),
- this.collection);
- this.listenTo(this.collection, 'load:start', callback);
- }
- };
- if (typeof collectionOptionNames !== 'undefined') {
- collectionOptionNames['loading-template'] = 'loadingTemplate';
- collectionOptionNames['loading-view'] = 'loadingView';
- collectionOptionNames['loading-placement'] = 'loadingPlacement';
- }
- Thorax.View.on({
- 'load:start': Thorax.loadHandler(
- function(message, background, object) {
- this.onLoadStart(message, background, object);
- },
- function(background, object) {
- this.onLoadEnd(object);
- }),
- collection: {
- 'load:start': function(message, background, object) {
- this.trigger(loadStart, message, background, object);
- }
- },
- model: {
- 'load:start': function(message, background, object) {
- this.trigger(loadStart, message, background, object);
- }
- }
- });
- ;;
- Handlebars.registerViewHelper('loading', function(view) {
- var _render = view.render;
- view.render = function() {
- if (view.parent.$el.hasClass(view.parent._loadingClassName)) {
- return _render.call(this, view.fn);
- } else {
- return _render.call(this, view.inverse);
- }
- };
- var callback = _.bind(view.render, view);
- view.parent._loadingCallbacks = view.parent._loadingCallbacks || [];
- view.parent._loadingCallbacks.push(callback);
- view.on('destroyed', function() {
- view.parent._loadingCallbacks = _.without(view.parent._loadingCallbacks, callback);
- });
- view.render();
- });
- ;;
- var isIE = (/msie [\w.]+/).exec(navigator.userAgent.toLowerCase());
- // IE will lose a reference to the elements if view.el.innerHTML = '';
- // If they are removed one by one the references are not lost.
- // For instance a view's childrens' `el`s will be lost if the view
- // sets it's `el.innerHTML`.
- if (isIE) {
- Thorax.View.on('before:append', function() {
- if (this._renderCount > 0) {
- _.each(this._elementsByCid, function(element) {
- $(element).remove();
- });
- _.each(this.children, function(child) {
- child.$el.remove();
- });
- }
- });
- }
- ;;
- })();