PageRenderTime 82ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

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

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