PageRenderTime 116ms CodeModel.GetById 3ms app.highlight 97ms RepoModel.GetById 1ms app.codeStats 1ms

/labs/architecture-examples/thorax/bower_components/thorax/thorax.js

https://github.com/0sung1/todomvc
JavaScript | 2670 lines | 2159 code | 279 blank | 232 comment | 528 complexity | f9af3a72cf366753bf4258d743de3e03 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*
   2Copyright (c) 2011-2013 @WalmartLabs
   3
   4Permission is hereby granted, free of charge, to any person obtaining a copy
   5of this software and associated documentation files (the "Software"), to
   6deal in the Software without restriction, including without limitation the
   7rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   8sell copies of the Software, and to permit persons to whom the Software is
   9furnished to do so, subject to the following conditions:
  10
  11The above copyright notice and this permission notice shall be included in
  12all copies or substantial portions of the Software.
  13
  14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  20DEALINGS IN THE SOFTWARE.
  21*/
  22
  23;;
  24(function() {
  25
  26/*global cloneInheritVars, createInheritVars, createRegistryWrapper, getValue, inheritVars */
  27
  28//support zepto.forEach on jQuery
  29if (!$.fn.forEach) {
  30  $.fn.forEach = function(iterator, context) {
  31    $.fn.each.call(this, function(index) {
  32      iterator.call(context || this, this, index);
  33    });
  34  };
  35}
  36
  37var viewNameAttributeName = 'data-view-name',
  38    viewCidAttributeName = 'data-view-cid',
  39    viewHelperAttributeName = 'data-view-helper';
  40
  41//view instances
  42var viewsIndexedByCid = {};
  43
  44var Thorax = this.Thorax = {
  45  VERSION: '2.0.0rc1',
  46  templatePathPrefix: '',
  47  templates: {},
  48  //view classes
  49  Views: {},
  50  //certain error prone pieces of code (on Android only it seems)
  51  //are wrapped in a try catch block, then trigger this handler in
  52  //the catch, with the name of the function or event that was
  53  //trying to be executed. Override this with a custom handler
  54  //to debug / log / etc
  55  onException: function(name, err) {
  56    throw err;
  57  }
  58};
  59
  60Thorax.View = Backbone.View.extend({
  61  constructor: function() {
  62    var response = Backbone.View.apply(this, arguments);
  63    _.each(inheritVars, function(obj) {
  64      if (obj.ctor) {
  65        obj.ctor.call(this, response);
  66      }
  67    }, this);
  68    return response;
  69  },
  70  _configure: function(options) {
  71    var self = this;
  72
  73    this._objectOptionsByCid = {};
  74    this._boundDataObjectsByCid = {};
  75
  76    // Setup object event tracking
  77    _.each(inheritVars, function(obj) {
  78      self[obj.name] = [];
  79    });
  80
  81    viewsIndexedByCid[this.cid] = this;
  82    this.children = {};
  83    this._renderCount = 0;
  84
  85    //this.options is removed in Thorax.View, we merge passed
  86    //properties directly with the view and template context
  87    _.extend(this, options || {});
  88
  89    // Setup helpers
  90    bindHelpers.call(this);
  91
  92    //compile a string if it is set as this.template
  93    if (typeof this.template === 'string') {
  94      this.template = Handlebars.compile(this.template, {data: true});
  95    } else if (this.name && !this.template) {
  96      //fetch the template
  97      this.template = Thorax.Util.getTemplate(this.name, true);
  98    }
  99
 100    _.each(inheritVars, function(obj) {
 101      if (obj.configure) {
 102        obj.configure.call(this);
 103      }
 104    }, this);
 105  },
 106
 107  setElement : function() {
 108    var response = Backbone.View.prototype.setElement.apply(this, arguments);
 109    this.name && this.$el.attr(viewNameAttributeName, this.name);
 110    this.$el.attr(viewCidAttributeName, this.cid);
 111    return response;
 112  },
 113
 114  _addChild: function(view) {
 115    this.children[view.cid] = view;
 116    if (!view.parent) {
 117      view.parent = this;
 118    }
 119    this.trigger('child', view);
 120    return view;
 121  },
 122
 123  _removeChild: function(view) {
 124    delete this.children[view.cid];
 125    view.parent = null;
 126    return view;
 127  },
 128
 129  destroy: function(options) {
 130    options = _.defaults(options || {}, {
 131      children: true
 132    });
 133    _.each(this._boundDataObjectsByCid, this.unbindDataObject, this);
 134    this.trigger('destroyed');
 135    delete viewsIndexedByCid[this.cid];
 136    _.each(this.children, function(child) {
 137      this._removeChild(child);
 138      if (options.children) {
 139        child.destroy();
 140      }
 141    }, this);
 142    if (this.parent) {
 143      this.parent._removeChild(this);
 144    }
 145    this.remove(); // Will call stopListening()
 146  },
 147
 148  render: function(output) {
 149    this._previousHelpers = _.filter(this.children, function(child) { return child._helperOptions; });
 150
 151    var children = {};
 152    _.each(this.children, function(child, key) {
 153      if (!child._helperOptions) {
 154        children[key] = child;
 155      }
 156    });
 157    this.children = children;
 158
 159    if (typeof output === 'undefined' || (!_.isElement(output) && !Thorax.Util.is$(output) && !(output && output.el) && typeof output !== 'string' && typeof output !== 'function')) {
 160      if (!this.template) {
 161        //if the name was set after the view was created try one more time to fetch a template
 162        if (this.name) {
 163          this.template = Thorax.Util.getTemplate(this.name, true);
 164        }
 165        if (!this.template) {
 166          throw new Error('View ' + (this.name || this.cid) + '.render() was called with no content and no template set on the view.');
 167        }
 168      }
 169      output = this.renderTemplate(this.template);
 170    } else if (typeof output === 'function') {
 171      output = this.renderTemplate(output);
 172    }
 173
 174    // Destroy any helpers that may be lingering
 175    _.each(this._previousHelpers, function(child) {
 176      child.destroy();
 177      child.parent = undefined;
 178    });
 179    this._previousHelpers = undefined;
 180
 181    //accept a view, string, Handlebars.SafeString or DOM element
 182    this.html((output && output.el) || (output && output.string) || output);
 183    ++this._renderCount;
 184    this.trigger('rendered');
 185    return output;
 186  },
 187
 188  context: function() {
 189    return (this.model && this.model.attributes) || {};
 190  },
 191
 192  _getContext: function() {
 193    return _.extend({}, this, getValue(this, 'context') || {});
 194  },
 195
 196  // Private variables in handlebars / options.data in template helpers
 197  _getData: function(data) {
 198    return {
 199      view: this,
 200      cid: _.uniqueId('t'),
 201      yield: function() {
 202        // fn is seeded by template helper passing context to data
 203        return data.fn && data.fn(data);
 204      }
 205    };
 206  },
 207
 208  _getHelpers: function() {
 209    if (this.helpers) {
 210      return _.extend({}, Handlebars.helpers, this.helpers);
 211    } else {
 212      return Handlebars.helpers;
 213    }
 214
 215  },
 216
 217  renderTemplate: function(file, context, ignoreErrors) {
 218    var template;
 219    context = context || this._getContext();
 220    if (typeof file === 'function') {
 221      template = file;
 222    } else {
 223      template = Thorax.Util.getTemplate(file);
 224    }
 225    if (!template) {
 226      if (ignoreErrors) {
 227        return '';
 228      } else {
 229        throw new Error('Unable to find template ' + file);
 230      }
 231    } else {
 232      return template(context, {
 233        helpers: this._getHelpers(),
 234        data: this._getData(context)
 235      });
 236    }
 237  },
 238
 239  ensureRendered: function() {
 240    !this._renderCount && this.render();
 241  },
 242
 243  appendTo: function(el) {
 244    this.ensureRendered();
 245    $(el).append(this.el);
 246    this.trigger('ready', {target: this});
 247  },
 248
 249  html: function(html) {
 250
 251    function replaceHTML(view) {
 252      view.el.innerHTML = "";
 253      return view.$el.append(html);
 254    }
 255
 256    if (typeof html === 'undefined') {
 257      return this.el.innerHTML;
 258    } else {
 259      // Event for IE element fixes
 260      this.trigger('before:append');
 261      var element;
 262      if (this.collection && this._objectOptionsByCid[this.collection.cid] && this._renderCount) {
 263        // preserve collection element if it was not created with {{collection}} helper
 264        var oldCollectionElement = this.getCollectionElement();
 265        element = replaceHTML(this);
 266        if (!oldCollectionElement.attr('data-view-cid')) {
 267          this.getCollectionElement().replaceWith(oldCollectionElement);
 268        }
 269      } else {
 270        element = replaceHTML(this);
 271      }
 272      this.trigger('append');
 273      return element;
 274    }
 275  },
 276
 277  _anchorClick: function(event) {
 278    var target = $(event.currentTarget),
 279        href = target.attr('href');
 280    // Route anything that starts with # or / (excluding //domain urls)
 281    if (href && (href[0] === '#' || (href[0] === '/' && href[1] !== '/'))) {
 282      Backbone.history.navigate(href, {
 283        trigger: true
 284      });
 285      return false;
 286    }
 287    return true;
 288  }
 289});
 290
 291Thorax.View.extend = function() {
 292  createInheritVars(this);
 293
 294  var child = Backbone.View.extend.apply(this, arguments);
 295  child.__parent__ = this;
 296
 297  resetInheritVars(child);
 298
 299  return child;
 300};
 301
 302createRegistryWrapper(Thorax.View, Thorax.Views);
 303
 304function bindHelpers() {
 305  if (this.helpers) {
 306    _.each(this.helpers, function(helper, name) {
 307      var view = this;
 308      this.helpers[name] = function() {
 309        var args = _.toArray(arguments),
 310            options = _.last(args);
 311        options.context = this;
 312        return helper.apply(view, args);
 313      };
 314    }, this);
 315  }
 316}
 317
 318//$(selector).view() helper
 319$.fn.view = function(options) {
 320  options = _.defaults(options || {}, {
 321    helper: true
 322  });
 323  var selector = '[' + viewCidAttributeName + ']';
 324  if (!options.helper) {
 325    selector += ':not([' + viewHelperAttributeName + '])';
 326  }
 327  var el = $(this).closest(selector);
 328  return (el && viewsIndexedByCid[el.attr(viewCidAttributeName)]) || false;
 329};
 330
 331;;
 332/*global createRegistryWrapper:true, cloneEvents: true */
 333function createRegistryWrapper(klass, hash) {
 334  var $super = klass.extend;
 335  klass.extend = function() {
 336    var child = $super.apply(this, arguments);
 337    if (child.prototype.name) {
 338      hash[child.prototype.name] = child;
 339    }
 340    return child;
 341  };
 342}
 343function registryGet(object, type, name, ignoreErrors) {
 344  var target = object[type],
 345      value;
 346  if (name.indexOf('.') >= 0) {
 347    var bits = name.split(/\./);
 348    name = bits.pop();
 349    _.each(bits, function(key) {
 350      target = target[key];
 351    });
 352  }
 353  target && (value = target[name]);
 354  if (!value && !ignoreErrors) {
 355    throw new Error(type + ': ' + name + ' does not exist.');
 356  } else {
 357    return value;
 358  }
 359}
 360
 361// getValue is used instead of _.result because we
 362// need an extra scope parameter, and will minify
 363// better than _.result
 364function getValue(object, prop, scope) {
 365  if (!(object && object[prop])) {
 366    return null;
 367  }
 368  return _.isFunction(object[prop])
 369    ? object[prop].call(scope || object)
 370    : object[prop];
 371}
 372
 373var inheritVars = {};
 374function createInheritVars(self) {
 375  // Ensure that we have our static event objects
 376  _.each(inheritVars, function(obj) {
 377    if (!self[obj.name]) {
 378      self[obj.name] = [];
 379    }
 380  });
 381}
 382function resetInheritVars(self) {
 383  // Ensure that we have our static event objects
 384  _.each(inheritVars, function(obj) {
 385    self[obj.name] = [];
 386  });
 387}
 388function walkInheritTree(source, fieldName, isStatic, callback) {
 389  var tree = [];
 390  if (_.has(source, fieldName)) {
 391    tree.push(source);
 392  }
 393  var iterate = source;
 394  if (isStatic) {
 395    while (iterate = iterate.__parent__) {
 396      if (_.has(iterate, fieldName)) {
 397        tree.push(iterate);
 398      }
 399    }
 400  } else {
 401    iterate = iterate.constructor;
 402    while (iterate) {
 403      if (iterate.prototype && _.has(iterate.prototype, fieldName)) {
 404        tree.push(iterate.prototype);
 405      }
 406      iterate = iterate.__super__ && iterate.__super__.constructor;
 407    }
 408  }
 409
 410  var i = tree.length;
 411  while (i--) {
 412    _.each(getValue(tree[i], fieldName, source), callback);
 413  }
 414}
 415
 416function objectEvents(target, eventName, callback, context) {
 417  if (_.isObject(callback)) {
 418    var spec = inheritVars[eventName];
 419    if (spec && spec.event) {
 420      addEvents(target['_' + eventName + 'Events'], callback, context);
 421      return true;
 422    }
 423  }
 424}
 425function addEvents(target, source, context) {
 426  _.each(source, function(callback, eventName) {
 427    if (_.isArray(callback)) {
 428      _.each(callback, function(cb) {
 429        target.push([eventName, cb, context]);
 430      });
 431    } else {
 432      target.push([eventName, callback, context]);
 433    }
 434  });
 435}
 436
 437function getOptionsData(options) {
 438  if (!options || !options.data) {
 439    throw new Error('Handlebars template compiled without data, use: Handlebars.compile(template, {data: true})');
 440  }
 441  return options.data;
 442}
 443
 444// These whitelisted attributes will be the only ones passed
 445// from the options hash to Thorax.Util.tag
 446var htmlAttributesToCopy = ['id', 'className', 'tagName'];
 447
 448// In helpers "tagName" or "tag" may be specified, as well
 449// as "class" or "className". Normalize to "tagName" and
 450// "className" to match the property names used by Backbone
 451// jQuery, etc. Special case for "className" in
 452// Thorax.Util.tag: will be rewritten as "class" in
 453// generated HTML.
 454function normalizeHTMLAttributeOptions(options) {
 455  if (options.tag) {
 456    options.tagName = options.tag;
 457    delete options.tag;
 458  }
 459  if (options['class']) {
 460    options.className = options['class'];
 461    delete options['class'];
 462  }
 463}
 464
 465Thorax.Util = {
 466  getViewInstance: function(name, attributes) {
 467    attributes = attributes || {};
 468    if (typeof name === 'string') {
 469      var Klass = registryGet(Thorax, 'Views', name, false);
 470      return Klass.cid ? _.extend(Klass, attributes || {}) : new Klass(attributes);
 471    } else if (typeof name === 'function') {
 472      return new name(attributes);
 473    } else {
 474      return name;
 475    }
 476  },
 477
 478  getTemplate: function(file, ignoreErrors) {
 479    //append the template path prefix if it is missing
 480    var pathPrefix = Thorax.templatePathPrefix,
 481        template;
 482    if (pathPrefix && file.substr(0, pathPrefix.length) !== pathPrefix) {
 483      file = pathPrefix + file;
 484    }
 485
 486    // Without extension
 487    file = file.replace(/\.handlebars$/, '');
 488    template = Thorax.templates[file];
 489    if (!template) {
 490      // With extension
 491      file = file + '.handlebars';
 492      template = Thorax.templates[file];
 493    }
 494
 495    if (template && typeof template === 'string') {
 496      template = Thorax.templates[file] = Handlebars.compile(template, {data: true});
 497    } else if (!template && !ignoreErrors) {
 498      throw new Error('templates: ' + file + ' does not exist.');
 499    }
 500    return template;
 501  },
 502
 503  //'selector' is not present in $('<p></p>')
 504  //TODO: investigage a better detection method
 505  is$: function(obj) {
 506    return typeof obj === 'object' && ('length' in obj);
 507  },
 508  expandToken: function(input, scope) {
 509    if (input && input.indexOf && input.indexOf('{{') >= 0) {
 510      var re = /(?:\{?[^{]+)|(?:\{\{([^}]+)\}\})/g,
 511          match,
 512          ret = [];
 513      function deref(token, scope) {
 514        if (token.match(/^("|')/) && token.match(/("|')$/)) {
 515          return token.replace(/(^("|')|('|")$)/g, '');
 516        }
 517        var segments = token.split('.'),
 518            len = segments.length;
 519        for (var i = 0; scope && i < len; i++) {
 520          if (segments[i] !== 'this') {
 521            scope = scope[segments[i]];
 522          }
 523        }
 524        return scope;
 525      }
 526      while (match = re.exec(input)) {
 527        if (match[1]) {
 528          var params = match[1].split(/\s+/);
 529          if (params.length > 1) {
 530            var helper = params.shift();
 531            params = _.map(params, function(param) { return deref(param, scope); });
 532            if (Handlebars.helpers[helper]) {
 533              ret.push(Handlebars.helpers[helper].apply(scope, params));
 534            } else {
 535              // If the helper is not defined do nothing
 536              ret.push(match[0]);
 537            }
 538          } else {
 539            ret.push(deref(params[0], scope));
 540          }
 541        } else {
 542          ret.push(match[0]);
 543        }
 544      }
 545      input = ret.join('');
 546    }
 547    return input;
 548  },
 549  tag: function(attributes, content, scope) {
 550    var htmlAttributes = _.omit(attributes, 'tagName'),
 551        tag = attributes.tagName || 'div';
 552    return '<' + tag + ' ' + _.map(htmlAttributes, function(value, key) {
 553      if (typeof value === 'undefined' || key === 'expand-tokens') {
 554        return '';
 555      }
 556      var formattedValue = value;
 557      if (scope) {
 558        formattedValue = Thorax.Util.expandToken(value, scope);
 559      }
 560      return (key === 'className' ? 'class' : key) + '="' + Handlebars.Utils.escapeExpression(formattedValue) + '"';
 561    }).join(' ') + '>' + (typeof content === 'undefined' ? '' : content) + '</' + tag + '>';
 562  }
 563};
 564
 565;;
 566/*global createInheritVars, inheritVars */
 567Thorax.Mixins = {};
 568
 569inheritVars.mixins = {
 570  name: 'mixins',
 571  configure: function() {
 572    _.each(this.constructor.mixins, this.mixin, this);
 573    _.each(this.mixins, this.mixin, this);
 574  }
 575};
 576
 577_.extend(Thorax.View, {
 578  mixin: function(mixin) {
 579    createInheritVars(this);
 580    this.mixins.push(mixin);
 581  },
 582  registerMixin: function(name, callback, methods) {
 583    Thorax.Mixins[name] = [callback, methods];
 584  }
 585});
 586
 587Thorax.View.prototype.mixin = function(name) {
 588  if (!this._appliedMixins) {
 589    this._appliedMixins = [];
 590  }
 591  if (this._appliedMixins.indexOf(name) === -1) {
 592    this._appliedMixins.push(name);
 593    if (typeof name === 'function') {
 594      name.call(this);
 595    } else {
 596      var mixin = Thorax.Mixins[name];
 597      _.extend(this, mixin[1]);
 598      //mixin callback may be an array of [callback, arguments]
 599      if (_.isArray(mixin[0])) {
 600        mixin[0][0].apply(this, mixin[0][1]);
 601      } else {
 602        mixin[0].apply(this, _.toArray(arguments).slice(1));
 603      }
 604    }
 605  }
 606};
 607
 608;;
 609/*global createInheritVars, inheritVars, objectEvents, walkInheritTree */
 610// Save a copy of the _on method to call as a $super method
 611var _on = Thorax.View.prototype.on;
 612
 613inheritVars.event = {
 614  name: '_events',
 615
 616  configure: function() {
 617    var self = this;
 618    walkInheritTree(this.constructor, '_events', true, function(event) {
 619      self.on.apply(self, event);
 620    });
 621    walkInheritTree(this, 'events', false, function(handler, eventName) {
 622      self.on(eventName, handler, self);
 623    });
 624  }
 625};
 626
 627_.extend(Thorax.View, {
 628  on: function(eventName, callback) {
 629    createInheritVars(this);
 630
 631    if (objectEvents(this, eventName, callback)) {
 632      return this;
 633    }
 634
 635    //accept on({"rendered": handler})
 636    if (typeof eventName === 'object') {
 637      _.each(eventName, function(value, key) {
 638        this.on(key, value);
 639      }, this);
 640    } else {
 641      //accept on({"rendered": [handler, handler]})
 642      if (_.isArray(callback)) {
 643        _.each(callback, function(cb) {
 644          this._events.push([eventName, cb]);
 645        }, this);
 646      //accept on("rendered", handler)
 647      } else {
 648        this._events.push([eventName, callback]);
 649      }
 650    }
 651    return this;
 652  }
 653});
 654
 655_.extend(Thorax.View.prototype, {
 656  on: function(eventName, callback, context) {
 657    if (objectEvents(this, eventName, callback, context)) {
 658      return this;
 659    }
 660
 661    if (typeof eventName === 'object' && arguments.length < 3) {
 662      //accept on({"rendered": callback})
 663      _.each(eventName, function(value, key) {
 664        this.on(key, value, callback || this);    // callback is context in this form of the call
 665      }, this);
 666    } else {
 667      //accept on("rendered", callback, context)
 668      //accept on("click a", callback, context)
 669      _.each((_.isArray(callback) ? callback : [callback]), function(callback) {
 670        var params = eventParamsFromEventItem.call(this, eventName, callback, context || this);
 671        if (params.type === 'DOM') {
 672          //will call _addEvent during delegateEvents()
 673          if (!this._eventsToDelegate) {
 674            this._eventsToDelegate = [];
 675          }
 676          this._eventsToDelegate.push(params);
 677        } else {
 678          this._addEvent(params);
 679        }
 680      }, this);
 681    }
 682    return this;
 683  },
 684  delegateEvents: function(events) {
 685    this.undelegateEvents();
 686    if (events) {
 687      if (_.isFunction(events)) {
 688        events = events.call(this);
 689      }
 690      this._eventsToDelegate = [];
 691      this.on(events);
 692    }
 693    this._eventsToDelegate && _.each(this._eventsToDelegate, this._addEvent, this);
 694  },
 695  //params may contain:
 696  //- name
 697  //- originalName
 698  //- selector
 699  //- type "view" || "DOM"
 700  //- handler
 701  _addEvent: function(params) {
 702    if (params.type === 'view') {
 703      _.each(params.name.split(/\s+/), function(name) {
 704        _on.call(this, name, bindEventHandler.call(this, 'view-event:', params));
 705      }, this);
 706    } else {
 707      var boundHandler = bindEventHandler.call(this, 'dom-event:', params);
 708      if (!params.nested) {
 709        boundHandler = containHandlerToCurentView(boundHandler, this.cid);
 710      }
 711      if (params.selector) {
 712        var name = params.name + '.delegateEvents' + this.cid;
 713        this.$el.on(name, params.selector, boundHandler);
 714      } else {
 715        this.$el.on(params.name, boundHandler);
 716      }
 717    }
 718  }
 719});
 720
 721// When view is ready trigger ready event on all
 722// children that are present, then register an
 723// event that will trigger ready on new children
 724// when they are added
 725Thorax.View.on('ready', function(options) {
 726  if (!this._isReady) {
 727    this._isReady = true;
 728    function triggerReadyOnChild(child) {
 729      child.trigger('ready', options);
 730    }
 731    _.each(this.children, triggerReadyOnChild);
 732    this.on('child', triggerReadyOnChild);
 733  }
 734});
 735
 736var eventSplitter = /^(nested\s+)?(\S+)(?:\s+(.+))?/;
 737
 738var domEvents = [],
 739    domEventRegexp;
 740function pushDomEvents(events) {
 741  domEvents.push.apply(domEvents, events);
 742  domEventRegexp = new RegExp('^(nested\\s+)?(' + domEvents.join('|') + ')(?:\\s|$)');
 743}
 744pushDomEvents([
 745  'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
 746  'touchstart', 'touchend', 'touchmove',
 747  'click', 'dblclick',
 748  'keyup', 'keydown', 'keypress',
 749  'submit', 'change',
 750  'focus', 'blur'
 751]);
 752
 753function containHandlerToCurentView(handler, cid) {
 754  return function(event) {
 755    var view = $(event.target).view({helper: false});
 756    if (view && view.cid === cid) {
 757      event.originalContext = this;
 758      handler(event);
 759    }
 760  };
 761}
 762
 763function bindEventHandler(eventName, params) {
 764  eventName += params.originalName;
 765
 766  var callback = params.handler,
 767      method = typeof callback === 'function' ? callback : this[callback];
 768  if (!method) {
 769    throw new Error('Event "' + callback + '" does not exist ' + (this.name || this.cid) + ':' + eventName);
 770  }
 771  return _.bind(function() {
 772    try {
 773      method.apply(this, arguments);
 774    } catch (e) {
 775      Thorax.onException('thorax-exception: ' + (this.name || this.cid) + ':' + eventName, e);
 776    }
 777  }, params.context || this);
 778}
 779
 780function eventParamsFromEventItem(name, handler, context) {
 781  var params = {
 782    originalName: name,
 783    handler: typeof handler === 'string' ? this[handler] : handler
 784  };
 785  if (name.match(domEventRegexp)) {
 786    var match = eventSplitter.exec(name);
 787    params.nested = !!match[1];
 788    params.name = match[2];
 789    params.type = 'DOM';
 790    params.selector = match[3];
 791  } else {
 792    params.name = name;
 793    params.type = 'view';
 794  }
 795  params.context = context;
 796  return params;
 797}
 798
 799;;
 800/*global getOptionsData, viewHelperAttributeName */
 801var viewPlaceholderAttributeName = 'data-view-tmp';
 802var viewTemplateOverrides = {};
 803
 804Thorax.HelperView = Thorax.View.extend({
 805  _ensureElement: function() {
 806    Thorax.View.prototype._ensureElement.apply(this, arguments);
 807    this.$el.attr(viewHelperAttributeName, this._helperName);
 808  },
 809  _getContext: function() {
 810    return this.parent._getContext.apply(this.parent, arguments);
 811  },
 812});
 813
 814// Ensure nested inline helpers will always have this.parent
 815// set to the view containing the template
 816function getParent(parent) {
 817  // The `view` helper is a special case as it embeds
 818  // a view instead of creating a new one
 819  while (parent._helperName && parent._helperName !== 'view') {
 820    parent = parent.parent;
 821  }
 822  return parent;
 823}
 824
 825Handlebars.registerViewHelper = function(name, ViewClass, callback) {
 826  if (arguments.length === 2) {
 827    if (ViewClass.factory) {
 828      callback = ViewClass.callback;
 829    } else {
 830      callback = ViewClass;
 831      ViewClass = Thorax.HelperView;
 832    }
 833  }
 834  Handlebars.registerHelper(name, function() {
 835    var args = _.toArray(arguments),
 836        options = args.pop(),
 837        declaringView = getOptionsData(options).view;
 838
 839    var viewOptions = {
 840      template: options.fn || Handlebars.VM.noop,
 841      inverse: options.inverse,
 842      options: options.hash,
 843      declaringView: declaringView,
 844      parent: getParent(declaringView),
 845      _helperName: name,
 846      _helperOptions: {
 847        options: cloneHelperOptions(options),
 848        args: _.clone(args)
 849      }
 850    };
 851
 852    normalizeHTMLAttributeOptions(options.hash);
 853    _.extend(viewOptions, _.pick(options.hash, htmlAttributesToCopy));
 854
 855    // Check to see if we have an existing instance that we can reuse
 856    var instance = _.find(declaringView._previousHelpers, function(child) {
 857      return compareHelperOptions(viewOptions, child);
 858    });
 859
 860    // Create the instance if we don't already have one
 861    if (!instance) {
 862      if (ViewClass.factory) {
 863        instance = ViewClass.factory(args, viewOptions);
 864        if (!instance) {
 865          return '';
 866        }
 867
 868        instance._helperName = viewOptions._helperName;
 869        instance._helperOptions = viewOptions._helperOptions;
 870      } else {
 871        instance = new ViewClass(viewOptions);
 872      }
 873
 874      args.push(instance);
 875      declaringView._addChild(instance);
 876      declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
 877      declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
 878
 879      callback && callback.apply(this, args);
 880    } else {
 881      declaringView._previousHelpers = _.without(declaringView._previousHelpers, instance);
 882      declaringView.children[instance.cid] = instance;
 883    }
 884
 885    var htmlAttributes = _.pick(options.hash, htmlAttributesToCopy);
 886    htmlAttributes[viewPlaceholderAttributeName] = instance.cid;
 887
 888    var expandTokens = options.hash['expand-tokens'];
 889    return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, '', expandTokens ? this : null));
 890  });
 891  var helper = Handlebars.helpers[name];
 892  return helper;
 893};
 894
 895Thorax.View.on('append', function(scope, callback) {
 896  (scope || this.$el).find('[' + viewPlaceholderAttributeName + ']').forEach(function(el) {
 897    var placeholderId = el.getAttribute(viewPlaceholderAttributeName),
 898        view = this.children[placeholderId];
 899    if (view) {
 900      //see if the view helper declared an override for the view
 901      //if not, ensure the view has been rendered at least once
 902      if (viewTemplateOverrides[placeholderId]) {
 903        view.render(viewTemplateOverrides[placeholderId]);
 904        delete viewTemplateOverrides[placeholderId];
 905      } else {
 906        view.ensureRendered();
 907      }
 908      $(el).replaceWith(view.el);
 909      callback && callback(view.el);
 910    }
 911  }, this);
 912});
 913
 914
 915/**
 916 * Clones the helper options, dropping items that are known to change
 917 * between rendering cycles as appropriate.
 918 */
 919function cloneHelperOptions(options) {
 920  var ret = _.pick(options, 'fn', 'inverse', 'hash', 'data');
 921  ret.data = _.omit(options.data, 'cid', 'view', 'yield');
 922  return ret;
 923}
 924
 925/**
 926 * Checks for basic equality between two sets of parameters for a helper view.
 927 *
 928 * Checked fields include:
 929 *  - _helperName
 930 *  - All args
 931 *  - Hash
 932 *  - Data
 933 *  - Function and Invert (id based if possible)
 934 *
 935 * This method allows us to determine if the inputs to a given view are the same. If they
 936 * are then we make the assumption that the rendering will be the same (or the child view will
 937 * otherwise rerendering it by monitoring it's parameters as necessary) and reuse the view on
 938 * rerender of the parent view.
 939 */
 940function compareHelperOptions(a, b) {
 941  function compareValues(a, b) {
 942    return _.every(a, function(value, key) {
 943      return b[key] === value;
 944    });
 945  }
 946
 947  if (a._helperName !== b._helperName) {
 948    return false;
 949  }
 950
 951  a = a._helperOptions;
 952  b = b._helperOptions;
 953
 954  // Implements a first level depth comparison
 955  return a.args.length === b.args.length
 956      && compareValues(a.args, b.args)
 957      && _.isEqual(_.keys(a.options), _.keys(b.options))
 958      && _.every(a.options, function(value, key) {
 959          if (key === 'data' || key === 'hash') {
 960            return compareValues(a.options[key], b.options[key]);
 961          } else if (key === 'fn' || key === 'inverse') {
 962            return b.options[key] === value
 963                || (value && _.has(value, 'program') && ((b.options[key] || {}).program === value.program));
 964          }
 965          return b.options[key] === value;
 966        });
 967}
 968
 969;;
 970/*global getValue, inheritVars, walkInheritTree */
 971
 972function dataObject(type, spec) {
 973  spec = inheritVars[type] = _.defaults({
 974    name: '_' + type + 'Events',
 975    event: true
 976  }, spec);
 977
 978  // Add a callback in the view constructor
 979  spec.ctor = function() {
 980    if (this[type]) {
 981      // Need to null this.model/collection so setModel/Collection will
 982      // not treat it as the old model/collection and immediately return
 983      var object = this[type];
 984      this[type] = null;
 985      this[spec.set](object);
 986    }
 987  };
 988
 989  function setObject(dataObject, options) {
 990    var old = this[type],
 991        $el = getValue(this, spec.$el);
 992
 993    if (dataObject === old) {
 994      return this;
 995    }
 996    if (old) {
 997      this.unbindDataObject(old);
 998    }
 999
1000    if (dataObject) {
1001      this[type] = dataObject;
1002
1003      if (spec.loading) {
1004        spec.loading.call(this);
1005      }
1006
1007      this.bindDataObject(type, dataObject, _.extend({}, this.options, options));
1008      $el.attr(spec.cidAttrName, dataObject.cid);
1009      dataObject.trigger('set', dataObject, old);
1010    } else {
1011      this[type] = false;
1012      if (spec.change) {
1013        spec.change.call(this, false);
1014      }
1015      $el.removeAttr(spec.cidAttrName);
1016    }
1017    this.trigger('change:data-object', type, dataObject, old);
1018    return this;
1019  }
1020
1021  Thorax.View.prototype[spec.set] = setObject;
1022}
1023
1024_.extend(Thorax.View.prototype, {
1025  bindDataObject: function(type, dataObject, options) {
1026    if (this._boundDataObjectsByCid[dataObject.cid]) {
1027      return false;
1028    }
1029    this._boundDataObjectsByCid[dataObject.cid] = dataObject;
1030
1031    var options = this._modifyDataObjectOptions(dataObject, _.extend({}, inheritVars[type].defaultOptions, options));
1032    this._objectOptionsByCid[dataObject.cid] = options;
1033
1034    bindEvents.call(this, type, dataObject, this.constructor);
1035    bindEvents.call(this, type, dataObject, this);
1036
1037    var spec = inheritVars[type];
1038    spec.bindCallback && spec.bindCallback.call(this, dataObject, options);
1039
1040    if (dataObject.shouldFetch && dataObject.shouldFetch(options)) {
1041      loadObject(dataObject, options);
1042    } else if (inheritVars[type].change) {
1043      // want to trigger built in rendering without triggering event on model
1044      inheritVars[type].change.call(this, dataObject, options);
1045    }
1046
1047    return true;
1048  },
1049
1050  unbindDataObject: function (dataObject) {
1051    if (!this._boundDataObjectsByCid[dataObject.cid]) {
1052      return false;
1053    }
1054    delete this._boundDataObjectsByCid[dataObject.cid];
1055    this.stopListening(dataObject);
1056    delete this._objectOptionsByCid[dataObject.cid];
1057    return true;
1058  },
1059
1060  _modifyDataObjectOptions: function(dataObject, options) {
1061    return options;
1062  }
1063});
1064
1065function bindEvents(type, target, source) {
1066  var context = this;
1067  walkInheritTree(source, '_' + type + 'Events', true, function(event) {
1068    // getEventCallback will resolve if it is a string or a method
1069    // and return a method
1070    context.listenTo(target, event[0], _.bind(getEventCallback(event[1], context), event[2] || context));
1071  });
1072}
1073
1074function loadObject(dataObject, options) {
1075  if (dataObject.load) {
1076    dataObject.load(function() {
1077      options && options.success && options.success(dataObject);
1078    }, options);
1079  } else {
1080    dataObject.fetch(options);
1081  }
1082}
1083
1084function getEventCallback(callback, context) {
1085  if (typeof callback === 'function') {
1086    return callback;
1087  } else {
1088    return context[callback];
1089  }
1090}
1091
1092;;
1093/*global createRegistryWrapper, dataObject, getValue */
1094var modelCidAttributeName = 'data-model-cid';
1095
1096Thorax.Model = Backbone.Model.extend({
1097  isEmpty: function() {
1098    return !this.isPopulated();
1099  },
1100  isPopulated: function() {
1101    // We are populated if we have attributes set
1102    var attributes = _.clone(this.attributes),
1103        defaults = getValue(this, 'defaults') || {};
1104    for (var default_key in defaults) {
1105      if (attributes[default_key] != defaults[default_key]) {
1106        return true;
1107      }
1108      delete attributes[default_key];
1109    }
1110    var keys = _.keys(attributes);
1111    return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
1112  },
1113  shouldFetch: function(options) {
1114    // url() will throw if model has no `urlRoot` and no `collection`
1115    // or has `collection` and `collection` has no `url`
1116    var url;
1117    try {
1118      url = getValue(this, 'url');
1119    } catch(e) {
1120      url = false;
1121    }
1122    return options.fetch && !!url && !this.isPopulated();
1123  }
1124});
1125
1126Thorax.Models = {};
1127createRegistryWrapper(Thorax.Model, Thorax.Models);
1128
1129dataObject('model', {
1130  set: 'setModel',
1131  defaultOptions: {
1132    render: true,
1133    fetch: true,
1134    success: false,
1135    errors: true
1136  },
1137  change: onModelChange,
1138  $el: '$el',
1139  cidAttrName: modelCidAttributeName
1140});
1141
1142function onModelChange(model) {
1143  var modelOptions = model && this._objectOptionsByCid[model.cid];
1144  // !modelOptions will be true when setModel(false) is called
1145  if (!modelOptions || (modelOptions && modelOptions.render)) {
1146    this.render();
1147  }
1148}
1149
1150Thorax.View.on({
1151  model: {
1152    error: function(model, errors) {
1153      if (this._objectOptionsByCid[model.cid].errors) {
1154        this.trigger('error', errors, model);
1155      }
1156    },
1157    change: function(model) {
1158      onModelChange.call(this, model);
1159    }
1160  }
1161});
1162
1163$.fn.model = function(view) {
1164  var $this = $(this),
1165      modelElement = $this.closest('[' + modelCidAttributeName + ']'),
1166      modelCid = modelElement && modelElement.attr(modelCidAttributeName);
1167  if (modelCid) {
1168    var view = view || $this.view();
1169    if (view && view.model && view.model.cid === modelCid) {
1170      return view.model || false;
1171    }
1172    var collection = $this.collection(view);
1173    if (collection) {
1174      return collection.get(modelCid);
1175    }
1176  }
1177  return false;
1178};
1179
1180;;
1181/*global createRegistryWrapper, dataObject, getEventCallback, getValue, modelCidAttributeName, viewCidAttributeName */
1182var _fetch = Backbone.Collection.prototype.fetch,
1183    _reset = Backbone.Collection.prototype.reset,
1184    collectionCidAttributeName = 'data-collection-cid',
1185    collectionEmptyAttributeName = 'data-collection-empty',
1186    collectionElementAttributeName = 'data-collection-element',
1187    ELEMENT_NODE_TYPE = 1;
1188
1189Thorax.Collection = Backbone.Collection.extend({
1190  model: Thorax.Model || Backbone.Model,
1191  initialize: function() {
1192    this.cid = _.uniqueId('collection');
1193    return Backbone.Collection.prototype.initialize.apply(this, arguments);
1194  },
1195  isEmpty: function() {
1196    if (this.length > 0) {
1197      return false;
1198    } else {
1199      return this.length === 0 && this.isPopulated();
1200    }
1201  },
1202  isPopulated: function() {
1203    return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
1204  },
1205  shouldFetch: function(options) {
1206    return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
1207  },
1208  fetch: function(options) {
1209    options = options || {};
1210    var success = options.success;
1211    options.success = function(collection, response) {
1212      collection._fetched = true;
1213      success && success(collection, response);
1214    };
1215    return _fetch.apply(this, arguments);
1216  },
1217  reset: function(models, options) {
1218    this._fetched = !!models;
1219    return _reset.call(this, models, options);
1220  }
1221});
1222
1223Thorax.Collections = {};
1224createRegistryWrapper(Thorax.Collection, Thorax.Collections);
1225
1226dataObject('collection', {
1227  set: 'setCollection',
1228  bindCallback: onSetCollection,
1229  defaultOptions: {
1230    render: true,
1231    fetch: true,
1232    success: false,
1233    errors: true
1234  },
1235  change: onCollectionReset,
1236  $el: 'getCollectionElement',
1237  cidAttrName: collectionCidAttributeName
1238});
1239
1240_.extend(Thorax.View.prototype, {
1241  _collectionSelector: '[' + collectionElementAttributeName + ']',
1242  //appendItem(model [,index])
1243  //appendItem(html_string, index)
1244  //appendItem(view, index)
1245  appendItem: function(model, index, options) {
1246    //empty item
1247    if (!model) {
1248      return;
1249    }
1250    var itemView,
1251        $el = this.getCollectionElement();
1252    options = _.defaults(options || {}, {
1253      filter: true
1254    });
1255    //if index argument is a view
1256    index && index.el && (index = $el.children().indexOf(index.el) + 1);
1257    //if argument is a view, or html string
1258    if (model.el || typeof model === 'string') {
1259      itemView = model;
1260      model = false;
1261    } else {
1262      index = index || this.collection.indexOf(model) || 0;
1263      itemView = this.renderItem(model, index);
1264    }
1265    if (itemView) {
1266      itemView.cid && this._addChild(itemView);
1267      //if the renderer's output wasn't contained in a tag, wrap it in a div
1268      //plain text, or a mixture of top level text nodes and element nodes
1269      //will get wrapped
1270      if (typeof itemView === 'string' && !itemView.match(/^\s*</m)) {
1271        itemView = '<div>' + itemView + '</div>';
1272      }
1273      var itemElement = itemView.el ? [itemView.el] : _.filter($(itemView), function(node) {
1274        //filter out top level whitespace nodes
1275        return node.nodeType === ELEMENT_NODE_TYPE;
1276      });
1277      model && $(itemElement).attr(modelCidAttributeName, model.cid);
1278      var previousModel = index > 0 ? this.collection.at(index - 1) : false;
1279      if (!previousModel) {
1280        $el.prepend(itemElement);
1281      } else {
1282        //use last() as appendItem can accept multiple nodes from a template
1283        var last = $el.find('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
1284        last.after(itemElement);
1285      }
1286
1287      this.trigger('append', null, function(el) {
1288        el.setAttribute(modelCidAttributeName, model.cid);
1289      });
1290
1291      !options.silent && this.trigger('rendered:item', this, this.collection, model, itemElement, index);
1292      options.filter && applyItemVisiblityFilter.call(this, model);
1293    }
1294    return itemView;
1295  },
1296  // updateItem only useful if there is no item view, otherwise
1297  // itemView.render() provides the same functionality
1298  updateItem: function(model) {
1299    this.removeItem(model);
1300    this.appendItem(model);
1301  },
1302  removeItem: function(model) {
1303    var $el = this.getCollectionElement(),
1304        viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
1305    if (!viewEl.length) {
1306      return false;
1307    }
1308    viewEl.remove();
1309    var viewCid = viewEl.attr(viewCidAttributeName),
1310        child = this.children[viewCid];
1311    if (child) {
1312      this._removeChild(child);
1313      child.destroy();
1314    }
1315    return true;
1316  },
1317  renderCollection: function() {
1318    this.ensureRendered();
1319    if (!this.collectionRenderer) {
1320      return;
1321    }
1322    if (this.collection) {
1323      if (this.collection.isEmpty()) {
1324        handleChangeFromNotEmptyToEmpty.call(this);
1325      } else {
1326        handleChangeFromEmptyToNotEmpty.call(this);
1327        this.collection.forEach(function(item, i) {
1328          this.appendItem(item, i);
1329        }, this);
1330      }
1331      this.trigger('rendered:collection', this, this.collection);
1332      applyVisibilityFilter.call(this);
1333    } else {
1334      handleChangeFromNotEmptyToEmpty.call(this);
1335    }
1336  },
1337  emptyClass: 'empty',
1338  renderEmpty: function() {
1339    if (this.emptyView) {
1340      var viewOptions = {};
1341      if (this.emptyTemplate) {
1342        viewOptions.template = this.emptyTemplate;
1343      }
1344      var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
1345      view.ensureRendered();
1346      return view;
1347    } else {
1348      if (!this.emptyTemplate) {
1349        this.emptyTemplate = Thorax.Util.getTemplate(this.name + '-empty', true);
1350      }
1351      return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
1352    }
1353  },
1354  renderItem: function(model, i) {
1355    if (this.itemView) {
1356      var viewOptions = {
1357        model: model
1358      };
1359      if (this.itemTemplate) {
1360        viewOptions.template = this.itemTemplate;
1361      }
1362      var view = Thorax.Util.getViewInstance(this.itemView, viewOptions);
1363      view.ensureRendered();
1364      return view;
1365    } else {
1366      if (!this.itemTemplate) {
1367        this.itemTemplate = Thorax.Util.getTemplate(this.name + '-item');
1368      }
1369      return this.renderTemplate(this.itemTemplate, this.itemContext(model, i));
1370    }
1371  },
1372  itemContext: function(model /*, i */) {
1373    return model.attributes;
1374  },
1375  appendEmpty: function() {
1376    var $el = this.getCollectionElement();
1377    $el.empty();
1378    var emptyContent = this.renderEmpty();
1379    emptyContent && this.appendItem(emptyContent, 0, {
1380      silent: true,
1381      filter: false
1382    });
1383    this.trigger('rendered:empty', this, this.collection);
1384  },
1385  getCollectionElement: function() {
1386    var element = this.$(this._collectionSelector);
1387    return element.length === 0 ? this.$el : element;
1388  },
1389  // Events that will only be bound to "this.collection"
1390  _collectionRenderingEvents: {
1391    reset: onCollectionReset,
1392    sort: onCollectionReset,
1393    filter: function() {
1394      applyVisibilityFilter.call(this);
1395    },
1396    change: function(model) {
1397      // If we rendered with item views, model changes will be observed
1398      // by the generated item view but if we rendered with templates
1399      // then model changes need to be bound as nothing is watching
1400      !this.itemView && this.updateItem(model);
1401      applyItemVisiblityFilter.call(this, model);
1402    },
1403    add: function(model) {
1404      var $el = this.getCollectionElement();
1405      this.collection.length === 1 && $el.length && handleChangeFromEmptyToNotEmpty.call(this);
1406      if ($el.length) {
1407        var index = this.collection.indexOf(model);
1408        this.appendItem(model, index);
1409      }
1410    },
1411    remove: function(model) {
1412      var $el = this.getCollectionElement();
1413      this.removeItem(model);
1414      this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
1415    }
1416  }
1417});
1418
1419Thorax.View.on({
1420  collection: {
1421    error: function(collection, message) {
1422      if (this._objectOptionsByCid[collection.cid].errors) {
1423        this.trigger('error', message, collection);
1424      }
1425    }
1426  }
1427});
1428
1429function onCollectionReset(collection) {
1430  var options = collection && this._objectOptionsByCid[collection.cid];
1431  // we would want to still render in the case that the
1432  // collection has transitioned to being falsy
1433  if (!collection || (options && options.render)) {
1434    this.renderCollection();
1435  }
1436}
1437
1438function onSetCollection(collection) {
1439  if (this.collectionRenderer && collection) {
1440    _.each(this._collectionRenderingEvents, function(callback, eventName) {
1441      // getEventCallback will resolve if it is a string or a method
1442      // and return a method
1443      this.listenTo(collection, eventName, getEventCallback(callback, this));
1444    }, this);
1445  }
1446}
1447
1448function applyVisibilityFilter() {
1449  if (this.itemFilter) {
1450    this.collection.forEach(function(model) {
1451      applyItemVisiblityFilter.call(this, model);
1452    }, this);
1453  }
1454}
1455
1456function applyItemVisiblityFilter(model) {
1457  var $el = this.getCollectionElement();
1458  this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
1459}
1460
1461function itemShouldBeVisible(model) {
1462  return this.itemFilter(model, this.collection.indexOf(model));
1463}
1464
1465function handleChangeFromEmptyToNotEmpty() {
1466  var $el = this.getCollectionElement();
1467  this.emptyClass && $el.removeClass(this.emptyClass);
1468  $el.removeAttr(collectionEmptyAttributeName);
1469  $el.empty();
1470}
1471
1472function handleChangeFromNotEmptyToEmpty() {
1473  var $el = this.getCollectionElement();
1474  this.emptyClass && $el.addClass(this.emptyClass);
1475  $el.attr(collectionEmptyAttributeName, true);
1476  this.appendEmpty();
1477}
1478
1479//$(selector).collection() helper
1480$.fn.collection = function(view) {
1481  if (view && view.collection) {
1482    return view.collection;
1483  }
1484  var $this = $(this),
1485      collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
1486      collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
1487  if (collectionCid) {
1488    view = $this.view();
1489    if (view) {
1490      return view.collection;
1491    }
1492  }
1493  return false;
1494};
1495
1496;;
1497/*global inheritVars */
1498
1499inheritVars.model.defaultOptions.populate = true;
1500
1501var oldModelChange = inheritVars.model.change;
1502inheritVars.model.change = function() {
1503  oldModelChange.apply(this, arguments);
1504  // TODO : What can we do to remove this duplication?
1505  var modelOptions = this.model && this._objectOptionsByCid[this.model.cid];
1506  if (modelOptions && modelOptions.populate) {
1507    this.populate(this.model.attributes, modelOptions.populate === true ? {} : modelOptions.populate);
1508  }
1509};
1510inheritVars.model.defaultOptions.populate = true;
1511
1512_.extend(Thorax.View.prototype, {
1513  //serializes a form present in the view, returning the serialized data
1514  //as an object
1515  //pass {set:false} to not update this.model if present
1516  //can pass options, callback or event in any order
1517  serialize: function() {
1518    var callback, options, event;
1519    //ignore undefined arguments in case event was null
1520    for (var i = 0; i < arguments.length; ++i) {
1521      if (typeof arguments[i] === 'function') {
1522        callback = arguments[i];
1523      } else if (typeof arguments[i] === 'object') {
1524        if ('stopPropagation' in arguments[i] && 'preventDefault' in arguments[i]) {
1525          event = arguments[i];
1526        } else {
1527          options = arguments[i];
1528        }
1529      }
1530    }
1531
1532    if (event && !this._preventDuplicateSubmission(event)) {
1533      return;
1534    }
1535
1536    options = _.extend({
1537      set: true,
1538      validate: true,
1539      children: true,
1540      silent: true
1541    }, options || {});
1542
1543    var attributes = options.attributes || {};
1544
1545    //callback has context of element
1546    var view = this;
1547    var errors = [];
1548    eachNamedInput.call(this, options, function() {
1549      var value = view._getInputValue(this, options, errors);
1550      if (typeof value !== 'undefined') {
1551        objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'serialize'}, function(object, key) {
1552          if (!object[key]) {
1553            object[key] = value;
1554          } else if (_.isArray(object[key])) {
1555            object[key].push(value);
1556          } else {
1557            object[key] = [object[key], value];
1558          }
1559        });
1560      }
1561    });
1562
1563    this.trigger('serialize', attributes, options);
1564
1565    if (options.validate) {
1566      var validateInputErrors = this.validateInput(attributes);
1567      if (validateInputErrors && validateInputErrors.length) {
1568        errors = errors.concat(validateInputErrors);
1569      }
1570      this.trigger('validate', attributes, errors, options);
1571      if (errors.length) {
1572        this.trigger('error', errors);
1573        return;
1574      }
1575    }
1576
1577    if (options.set && this.model) {
1578      if (!this.model.set(attributes, {silent: options.silent})) {
1579        return false;
1580      }
1581    }
1582
1583    callback && callback.call(this, attributes, _.bind(resetSubmitState, this));
1584    return attributes;
1585  },
1586
1587  _preventDuplicateSubmission: function(event, callback) {
1588    event.preventDefault();
1589
1590    var form = $(event.target);
1591    if ((event.target.tagName || '').toLowerCase() !== 'form') {
1592      // Handle non-submit events by gating on the form
1593      form = $(event.target).closest('form');
1594    }
1595
1596    if (!form.attr('data-submit-wait')) {
1597      form.attr('data-submit-wait', 'true');
1598      if (callback) {
1599        callback.call(this, event);
1600      }
1601      return true;
1602    } else {
1603      return false;
1604    }
1605  },
1606
1607  //populate a form from the passed attributes or this.model if present
1608  populate: function(attributes, options) {
1609    options = _.extend({
1610      children: true
1611    }, options || {});
1612    var value, attributes = attributes || this._getContext();
1613    //callback has context of element
1614    eachNamedInput.call(this, options, function() {
1615      objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'populate'}, function(object, key) {
1616        if (object && typeof (value = object[key]) !== 'undefined') {
1617          //will only execute if we have a name that matches the structure in attributes
1618          if (this.type === 'checkbox' && _.isBoolean(value)) {
1619            this.checked = value;
1620          } else if (this.type === 'checkbox' || this.type === 'radio') {
1621            this.checked = value == this.value;
1622          } else {
1623            this.value = value;
1624          }
1625        }
1626      });
1627    });
1628
1629    this.trigger('populate', attributes);
1630  },
1631
1632  //perform form validation, implemented by child class
1633  validateInput: function(/* attributes, options, errors */) {},
1634
1635  _getInputValue: function(input /* , options, errors */) {
1636    if (input.type === 'checkbox' || input.type === 'radio') {
1637      if (input.checked) {
1638        return input.value;
1639      }
1640    } else if (input.multiple === true) {
1641      var values = [];
1642      $('option', input).each(function() {
1643        if (this.selected) {
1644          values.push(this.value);
1645        }
1646      });
1647      return values;
1648    } else {
1649      return input.value;
1650    }
1651  }
1652});
1653
1654Thorax.View.on({
1655  error: function() {
1656    resetSubmitState.call(this);
1657
1658    // If we errored with a model we want to reset the content but leave the UI
1659    // intact. If the user updates the data and serializes any overwritten data
1660    // will be restored.
1661    if (this.model && this.model.previousAttributes) {
1662      this.model.set(this.model.previousAttributes(), {
1663        silent: true
1664      });
1665    }
1666  },
1667  deactivated: function() {
1668    resetSubmitState.call(this);
1669  }
1670});
1671
1672function eachNamedInput(options, iterator, context) {
1673  var i = 0,
1674      self = this;
1675
1676  this.$('select,input,textarea', opti

Large files files are truncated, but you can click here to view the full file