PageRenderTime 21ms CodeModel.GetById 3ms app.highlight 76ms RepoModel.GetById 2ms app.codeStats 0ms

/labs/dependency-examples/thorax_require/js/lib/thorax.js

https://github.com/sagarrakshe/todomvc
JavaScript | 2307 lines | 1901 code | 226 blank | 180 comment | 495 complexity | 64df5839fe0cc6c80862ad6125536441 MD5 | raw file

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

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

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