PageRenderTime 44ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/labs/dependency-examples/thorax_lumbar/bower_components/thorax/thorax.js

https://github.com/0sung1/todomvc
JavaScript | 2670 lines | 2159 code | 279 blank | 232 comment | 528 complexity | f9af3a72cf366753bf4258d743de3e03 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. /*
  2. Copyright (c) 2011-2013 @WalmartLabs
  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. The above copyright notice and this permission notice shall be included in
  10. all copies or substantial portions of the Software.
  11. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  12. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  13. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  14. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  15. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  16. FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. DEALINGS IN THE SOFTWARE.
  18. */
  19. ;;
  20. (function() {
  21. /*global cloneInheritVars, createInheritVars, createRegistryWrapper, getValue, inheritVars */
  22. //support zepto.forEach on jQuery
  23. if (!$.fn.forEach) {
  24. $.fn.forEach = function(iterator, context) {
  25. $.fn.each.call(this, function(index) {
  26. iterator.call(context || this, this, index);
  27. });
  28. };
  29. }
  30. var viewNameAttributeName = 'data-view-name',
  31. viewCidAttributeName = 'data-view-cid',
  32. viewHelperAttributeName = 'data-view-helper';
  33. //view instances
  34. var viewsIndexedByCid = {};
  35. var Thorax = this.Thorax = {
  36. VERSION: '2.0.0rc1',
  37. templatePathPrefix: '',
  38. templates: {},
  39. //view classes
  40. Views: {},
  41. //certain error prone pieces of code (on Android only it seems)
  42. //are wrapped in a try catch block, then trigger this handler in
  43. //the catch, with the name of the function or event that was
  44. //trying to be executed. Override this with a custom handler
  45. //to debug / log / etc
  46. onException: function(name, err) {
  47. throw err;
  48. }
  49. };
  50. Thorax.View = Backbone.View.extend({
  51. constructor: function() {
  52. var response = Backbone.View.apply(this, arguments);
  53. _.each(inheritVars, function(obj) {
  54. if (obj.ctor) {
  55. obj.ctor.call(this, response);
  56. }
  57. }, this);
  58. return response;
  59. },
  60. _configure: function(options) {
  61. var self = this;
  62. this._objectOptionsByCid = {};
  63. this._boundDataObjectsByCid = {};
  64. // Setup object event tracking
  65. _.each(inheritVars, function(obj) {
  66. self[obj.name] = [];
  67. });
  68. viewsIndexedByCid[this.cid] = this;
  69. this.children = {};
  70. this._renderCount = 0;
  71. //this.options is removed in Thorax.View, we merge passed
  72. //properties directly with the view and template context
  73. _.extend(this, options || {});
  74. // Setup helpers
  75. bindHelpers.call(this);
  76. //compile a string if it is set as this.template
  77. if (typeof this.template === 'string') {
  78. this.template = Handlebars.compile(this.template, {data: true});
  79. } else if (this.name && !this.template) {
  80. //fetch the template
  81. this.template = Thorax.Util.getTemplate(this.name, true);
  82. }
  83. _.each(inheritVars, function(obj) {
  84. if (obj.configure) {
  85. obj.configure.call(this);
  86. }
  87. }, this);
  88. },
  89. setElement : function() {
  90. var response = Backbone.View.prototype.setElement.apply(this, arguments);
  91. this.name && this.$el.attr(viewNameAttributeName, this.name);
  92. this.$el.attr(viewCidAttributeName, this.cid);
  93. return response;
  94. },
  95. _addChild: function(view) {
  96. this.children[view.cid] = view;
  97. if (!view.parent) {
  98. view.parent = this;
  99. }
  100. this.trigger('child', view);
  101. return view;
  102. },
  103. _removeChild: function(view) {
  104. delete this.children[view.cid];
  105. view.parent = null;
  106. return view;
  107. },
  108. destroy: function(options) {
  109. options = _.defaults(options || {}, {
  110. children: true
  111. });
  112. _.each(this._boundDataObjectsByCid, this.unbindDataObject, this);
  113. this.trigger('destroyed');
  114. delete viewsIndexedByCid[this.cid];
  115. _.each(this.children, function(child) {
  116. this._removeChild(child);
  117. if (options.children) {
  118. child.destroy();
  119. }
  120. }, this);
  121. if (this.parent) {
  122. this.parent._removeChild(this);
  123. }
  124. this.remove(); // Will call stopListening()
  125. },
  126. render: function(output) {
  127. this._previousHelpers = _.filter(this.children, function(child) { return child._helperOptions; });
  128. var children = {};
  129. _.each(this.children, function(child, key) {
  130. if (!child._helperOptions) {
  131. children[key] = child;
  132. }
  133. });
  134. this.children = children;
  135. if (typeof output === 'undefined' || (!_.isElement(output) && !Thorax.Util.is$(output) && !(output && output.el) && typeof output !== 'string' && typeof output !== 'function')) {
  136. if (!this.template) {
  137. //if the name was set after the view was created try one more time to fetch a template
  138. if (this.name) {
  139. this.template = Thorax.Util.getTemplate(this.name, true);
  140. }
  141. if (!this.template) {
  142. throw new Error('View ' + (this.name || this.cid) + '.render() was called with no content and no template set on the view.');
  143. }
  144. }
  145. output = this.renderTemplate(this.template);
  146. } else if (typeof output === 'function') {
  147. output = this.renderTemplate(output);
  148. }
  149. // Destroy any helpers that may be lingering
  150. _.each(this._previousHelpers, function(child) {
  151. child.destroy();
  152. child.parent = undefined;
  153. });
  154. this._previousHelpers = undefined;
  155. //accept a view, string, Handlebars.SafeString or DOM element
  156. this.html((output && output.el) || (output && output.string) || output);
  157. ++this._renderCount;
  158. this.trigger('rendered');
  159. return output;
  160. },
  161. context: function() {
  162. return (this.model && this.model.attributes) || {};
  163. },
  164. _getContext: function() {
  165. return _.extend({}, this, getValue(this, 'context') || {});
  166. },
  167. // Private variables in handlebars / options.data in template helpers
  168. _getData: function(data) {
  169. return {
  170. view: this,
  171. cid: _.uniqueId('t'),
  172. yield: function() {
  173. // fn is seeded by template helper passing context to data
  174. return data.fn && data.fn(data);
  175. }
  176. };
  177. },
  178. _getHelpers: function() {
  179. if (this.helpers) {
  180. return _.extend({}, Handlebars.helpers, this.helpers);
  181. } else {
  182. return Handlebars.helpers;
  183. }
  184. },
  185. renderTemplate: function(file, context, ignoreErrors) {
  186. var template;
  187. context = context || this._getContext();
  188. if (typeof file === 'function') {
  189. template = file;
  190. } else {
  191. template = Thorax.Util.getTemplate(file);
  192. }
  193. if (!template) {
  194. if (ignoreErrors) {
  195. return '';
  196. } else {
  197. throw new Error('Unable to find template ' + file);
  198. }
  199. } else {
  200. return template(context, {
  201. helpers: this._getHelpers(),
  202. data: this._getData(context)
  203. });
  204. }
  205. },
  206. ensureRendered: function() {
  207. !this._renderCount && this.render();
  208. },
  209. appendTo: function(el) {
  210. this.ensureRendered();
  211. $(el).append(this.el);
  212. this.trigger('ready', {target: this});
  213. },
  214. html: function(html) {
  215. function replaceHTML(view) {
  216. view.el.innerHTML = "";
  217. return view.$el.append(html);
  218. }
  219. if (typeof html === 'undefined') {
  220. return this.el.innerHTML;
  221. } else {
  222. // Event for IE element fixes
  223. this.trigger('before:append');
  224. var element;
  225. if (this.collection && this._objectOptionsByCid[this.collection.cid] && this._renderCount) {
  226. // preserve collection element if it was not created with {{collection}} helper
  227. var oldCollectionElement = this.getCollectionElement();
  228. element = replaceHTML(this);
  229. if (!oldCollectionElement.attr('data-view-cid')) {
  230. this.getCollectionElement().replaceWith(oldCollectionElement);
  231. }
  232. } else {
  233. element = replaceHTML(this);
  234. }
  235. this.trigger('append');
  236. return element;
  237. }
  238. },
  239. _anchorClick: function(event) {
  240. var target = $(event.currentTarget),
  241. href = target.attr('href');
  242. // Route anything that starts with # or / (excluding //domain urls)
  243. if (href && (href[0] === '#' || (href[0] === '/' && href[1] !== '/'))) {
  244. Backbone.history.navigate(href, {
  245. trigger: true
  246. });
  247. return false;
  248. }
  249. return true;
  250. }
  251. });
  252. Thorax.View.extend = function() {
  253. createInheritVars(this);
  254. var child = Backbone.View.extend.apply(this, arguments);
  255. child.__parent__ = this;
  256. resetInheritVars(child);
  257. return child;
  258. };
  259. createRegistryWrapper(Thorax.View, Thorax.Views);
  260. function bindHelpers() {
  261. if (this.helpers) {
  262. _.each(this.helpers, function(helper, name) {
  263. var view = this;
  264. this.helpers[name] = function() {
  265. var args = _.toArray(arguments),
  266. options = _.last(args);
  267. options.context = this;
  268. return helper.apply(view, args);
  269. };
  270. }, this);
  271. }
  272. }
  273. //$(selector).view() helper
  274. $.fn.view = function(options) {
  275. options = _.defaults(options || {}, {
  276. helper: true
  277. });
  278. var selector = '[' + viewCidAttributeName + ']';
  279. if (!options.helper) {
  280. selector += ':not([' + viewHelperAttributeName + '])';
  281. }
  282. var el = $(this).closest(selector);
  283. return (el && viewsIndexedByCid[el.attr(viewCidAttributeName)]) || false;
  284. };
  285. ;;
  286. /*global createRegistryWrapper:true, cloneEvents: true */
  287. function createRegistryWrapper(klass, hash) {
  288. var $super = klass.extend;
  289. klass.extend = function() {
  290. var child = $super.apply(this, arguments);
  291. if (child.prototype.name) {
  292. hash[child.prototype.name] = child;
  293. }
  294. return child;
  295. };
  296. }
  297. function registryGet(object, type, name, ignoreErrors) {
  298. var target = object[type],
  299. value;
  300. if (name.indexOf('.') >= 0) {
  301. var bits = name.split(/\./);
  302. name = bits.pop();
  303. _.each(bits, function(key) {
  304. target = target[key];
  305. });
  306. }
  307. target && (value = target[name]);
  308. if (!value && !ignoreErrors) {
  309. throw new Error(type + ': ' + name + ' does not exist.');
  310. } else {
  311. return value;
  312. }
  313. }
  314. // getValue is used instead of _.result because we
  315. // need an extra scope parameter, and will minify
  316. // better than _.result
  317. function getValue(object, prop, scope) {
  318. if (!(object && object[prop])) {
  319. return null;
  320. }
  321. return _.isFunction(object[prop])
  322. ? object[prop].call(scope || object)
  323. : object[prop];
  324. }
  325. var inheritVars = {};
  326. function createInheritVars(self) {
  327. // Ensure that we have our static event objects
  328. _.each(inheritVars, function(obj) {
  329. if (!self[obj.name]) {
  330. self[obj.name] = [];
  331. }
  332. });
  333. }
  334. function resetInheritVars(self) {
  335. // Ensure that we have our static event objects
  336. _.each(inheritVars, function(obj) {
  337. self[obj.name] = [];
  338. });
  339. }
  340. function walkInheritTree(source, fieldName, isStatic, callback) {
  341. var tree = [];
  342. if (_.has(source, fieldName)) {
  343. tree.push(source);
  344. }
  345. var iterate = source;
  346. if (isStatic) {
  347. while (iterate = iterate.__parent__) {
  348. if (_.has(iterate, fieldName)) {
  349. tree.push(iterate);
  350. }
  351. }
  352. } else {
  353. iterate = iterate.constructor;
  354. while (iterate) {
  355. if (iterate.prototype && _.has(iterate.prototype, fieldName)) {
  356. tree.push(iterate.prototype);
  357. }
  358. iterate = iterate.__super__ && iterate.__super__.constructor;
  359. }
  360. }
  361. var i = tree.length;
  362. while (i--) {
  363. _.each(getValue(tree[i], fieldName, source), callback);
  364. }
  365. }
  366. function objectEvents(target, eventName, callback, context) {
  367. if (_.isObject(callback)) {
  368. var spec = inheritVars[eventName];
  369. if (spec && spec.event) {
  370. addEvents(target['_' + eventName + 'Events'], callback, context);
  371. return true;
  372. }
  373. }
  374. }
  375. function addEvents(target, source, context) {
  376. _.each(source, function(callback, eventName) {
  377. if (_.isArray(callback)) {
  378. _.each(callback, function(cb) {
  379. target.push([eventName, cb, context]);
  380. });
  381. } else {
  382. target.push([eventName, callback, context]);
  383. }
  384. });
  385. }
  386. function getOptionsData(options) {
  387. if (!options || !options.data) {
  388. throw new Error('Handlebars template compiled without data, use: Handlebars.compile(template, {data: true})');
  389. }
  390. return options.data;
  391. }
  392. // These whitelisted attributes will be the only ones passed
  393. // from the options hash to Thorax.Util.tag
  394. var htmlAttributesToCopy = ['id', 'className', 'tagName'];
  395. // In helpers "tagName" or "tag" may be specified, as well
  396. // as "class" or "className". Normalize to "tagName" and
  397. // "className" to match the property names used by Backbone
  398. // jQuery, etc. Special case for "className" in
  399. // Thorax.Util.tag: will be rewritten as "class" in
  400. // generated HTML.
  401. function normalizeHTMLAttributeOptions(options) {
  402. if (options.tag) {
  403. options.tagName = options.tag;
  404. delete options.tag;
  405. }
  406. if (options['class']) {
  407. options.className = options['class'];
  408. delete options['class'];
  409. }
  410. }
  411. Thorax.Util = {
  412. getViewInstance: function(name, attributes) {
  413. attributes = attributes || {};
  414. if (typeof name === 'string') {
  415. var Klass = registryGet(Thorax, 'Views', name, false);
  416. return Klass.cid ? _.extend(Klass, attributes || {}) : new Klass(attributes);
  417. } else if (typeof name === 'function') {
  418. return new name(attributes);
  419. } else {
  420. return name;
  421. }
  422. },
  423. getTemplate: function(file, ignoreErrors) {
  424. //append the template path prefix if it is missing
  425. var pathPrefix = Thorax.templatePathPrefix,
  426. template;
  427. if (pathPrefix && file.substr(0, pathPrefix.length) !== pathPrefix) {
  428. file = pathPrefix + file;
  429. }
  430. // Without extension
  431. file = file.replace(/\.handlebars$/, '');
  432. template = Thorax.templates[file];
  433. if (!template) {
  434. // With extension
  435. file = file + '.handlebars';
  436. template = Thorax.templates[file];
  437. }
  438. if (template && typeof template === 'string') {
  439. template = Thorax.templates[file] = Handlebars.compile(template, {data: true});
  440. } else if (!template && !ignoreErrors) {
  441. throw new Error('templates: ' + file + ' does not exist.');
  442. }
  443. return template;
  444. },
  445. //'selector' is not present in $('<p></p>')
  446. //TODO: investigage a better detection method
  447. is$: function(obj) {
  448. return typeof obj === 'object' && ('length' in obj);
  449. },
  450. expandToken: function(input, scope) {
  451. if (input && input.indexOf && input.indexOf('{{') >= 0) {
  452. var re = /(?:\{?[^{]+)|(?:\{\{([^}]+)\}\})/g,
  453. match,
  454. ret = [];
  455. function deref(token, scope) {
  456. if (token.match(/^("|')/) && token.match(/("|')$/)) {
  457. return token.replace(/(^("|')|('|")$)/g, '');
  458. }
  459. var segments = token.split('.'),
  460. len = segments.length;
  461. for (var i = 0; scope && i < len; i++) {
  462. if (segments[i] !== 'this') {
  463. scope = scope[segments[i]];
  464. }
  465. }
  466. return scope;
  467. }
  468. while (match = re.exec(input)) {
  469. if (match[1]) {
  470. var params = match[1].split(/\s+/);
  471. if (params.length > 1) {
  472. var helper = params.shift();
  473. params = _.map(params, function(param) { return deref(param, scope); });
  474. if (Handlebars.helpers[helper]) {
  475. ret.push(Handlebars.helpers[helper].apply(scope, params));
  476. } else {
  477. // If the helper is not defined do nothing
  478. ret.push(match[0]);
  479. }
  480. } else {
  481. ret.push(deref(params[0], scope));
  482. }
  483. } else {
  484. ret.push(match[0]);
  485. }
  486. }
  487. input = ret.join('');
  488. }
  489. return input;
  490. },
  491. tag: function(attributes, content, scope) {
  492. var htmlAttributes = _.omit(attributes, 'tagName'),
  493. tag = attributes.tagName || 'div';
  494. return '<' + tag + ' ' + _.map(htmlAttributes, function(value, key) {
  495. if (typeof value === 'undefined' || key === 'expand-tokens') {
  496. return '';
  497. }
  498. var formattedValue = value;
  499. if (scope) {
  500. formattedValue = Thorax.Util.expandToken(value, scope);
  501. }
  502. return (key === 'className' ? 'class' : key) + '="' + Handlebars.Utils.escapeExpression(formattedValue) + '"';
  503. }).join(' ') + '>' + (typeof content === 'undefined' ? '' : content) + '</' + tag + '>';
  504. }
  505. };
  506. ;;
  507. /*global createInheritVars, inheritVars */
  508. Thorax.Mixins = {};
  509. inheritVars.mixins = {
  510. name: 'mixins',
  511. configure: function() {
  512. _.each(this.constructor.mixins, this.mixin, this);
  513. _.each(this.mixins, this.mixin, this);
  514. }
  515. };
  516. _.extend(Thorax.View, {
  517. mixin: function(mixin) {
  518. createInheritVars(this);
  519. this.mixins.push(mixin);
  520. },
  521. registerMixin: function(name, callback, methods) {
  522. Thorax.Mixins[name] = [callback, methods];
  523. }
  524. });
  525. Thorax.View.prototype.mixin = function(name) {
  526. if (!this._appliedMixins) {
  527. this._appliedMixins = [];
  528. }
  529. if (this._appliedMixins.indexOf(name) === -1) {
  530. this._appliedMixins.push(name);
  531. if (typeof name === 'function') {
  532. name.call(this);
  533. } else {
  534. var mixin = Thorax.Mixins[name];
  535. _.extend(this, mixin[1]);
  536. //mixin callback may be an array of [callback, arguments]
  537. if (_.isArray(mixin[0])) {
  538. mixin[0][0].apply(this, mixin[0][1]);
  539. } else {
  540. mixin[0].apply(this, _.toArray(arguments).slice(1));
  541. }
  542. }
  543. }
  544. };
  545. ;;
  546. /*global createInheritVars, inheritVars, objectEvents, walkInheritTree */
  547. // Save a copy of the _on method to call as a $super method
  548. var _on = Thorax.View.prototype.on;
  549. inheritVars.event = {
  550. name: '_events',
  551. configure: function() {
  552. var self = this;
  553. walkInheritTree(this.constructor, '_events', true, function(event) {
  554. self.on.apply(self, event);
  555. });
  556. walkInheritTree(this, 'events', false, function(handler, eventName) {
  557. self.on(eventName, handler, self);
  558. });
  559. }
  560. };
  561. _.extend(Thorax.View, {
  562. on: function(eventName, callback) {
  563. createInheritVars(this);
  564. if (objectEvents(this, eventName, callback)) {
  565. return this;
  566. }
  567. //accept on({"rendered": handler})
  568. if (typeof eventName === 'object') {
  569. _.each(eventName, function(value, key) {
  570. this.on(key, value);
  571. }, this);
  572. } else {
  573. //accept on({"rendered": [handler, handler]})
  574. if (_.isArray(callback)) {
  575. _.each(callback, function(cb) {
  576. this._events.push([eventName, cb]);
  577. }, this);
  578. //accept on("rendered", handler)
  579. } else {
  580. this._events.push([eventName, callback]);
  581. }
  582. }
  583. return this;
  584. }
  585. });
  586. _.extend(Thorax.View.prototype, {
  587. on: function(eventName, callback, context) {
  588. if (objectEvents(this, eventName, callback, context)) {
  589. return this;
  590. }
  591. if (typeof eventName === 'object' && arguments.length < 3) {
  592. //accept on({"rendered": callback})
  593. _.each(eventName, function(value, key) {
  594. this.on(key, value, callback || this); // callback is context in this form of the call
  595. }, this);
  596. } else {
  597. //accept on("rendered", callback, context)
  598. //accept on("click a", callback, context)
  599. _.each((_.isArray(callback) ? callback : [callback]), function(callback) {
  600. var params = eventParamsFromEventItem.call(this, eventName, callback, context || this);
  601. if (params.type === 'DOM') {
  602. //will call _addEvent during delegateEvents()
  603. if (!this._eventsToDelegate) {
  604. this._eventsToDelegate = [];
  605. }
  606. this._eventsToDelegate.push(params);
  607. } else {
  608. this._addEvent(params);
  609. }
  610. }, this);
  611. }
  612. return this;
  613. },
  614. delegateEvents: function(events) {
  615. this.undelegateEvents();
  616. if (events) {
  617. if (_.isFunction(events)) {
  618. events = events.call(this);
  619. }
  620. this._eventsToDelegate = [];
  621. this.on(events);
  622. }
  623. this._eventsToDelegate && _.each(this._eventsToDelegate, this._addEvent, this);
  624. },
  625. //params may contain:
  626. //- name
  627. //- originalName
  628. //- selector
  629. //- type "view" || "DOM"
  630. //- handler
  631. _addEvent: function(params) {
  632. if (params.type === 'view') {
  633. _.each(params.name.split(/\s+/), function(name) {
  634. _on.call(this, name, bindEventHandler.call(this, 'view-event:', params));
  635. }, this);
  636. } else {
  637. var boundHandler = bindEventHandler.call(this, 'dom-event:', params);
  638. if (!params.nested) {
  639. boundHandler = containHandlerToCurentView(boundHandler, this.cid);
  640. }
  641. if (params.selector) {
  642. var name = params.name + '.delegateEvents' + this.cid;
  643. this.$el.on(name, params.selector, boundHandler);
  644. } else {
  645. this.$el.on(params.name, boundHandler);
  646. }
  647. }
  648. }
  649. });
  650. // When view is ready trigger ready event on all
  651. // children that are present, then register an
  652. // event that will trigger ready on new children
  653. // when they are added
  654. Thorax.View.on('ready', function(options) {
  655. if (!this._isReady) {
  656. this._isReady = true;
  657. function triggerReadyOnChild(child) {
  658. child.trigger('ready', options);
  659. }
  660. _.each(this.children, triggerReadyOnChild);
  661. this.on('child', triggerReadyOnChild);
  662. }
  663. });
  664. var eventSplitter = /^(nested\s+)?(\S+)(?:\s+(.+))?/;
  665. var domEvents = [],
  666. domEventRegexp;
  667. function pushDomEvents(events) {
  668. domEvents.push.apply(domEvents, events);
  669. domEventRegexp = new RegExp('^(nested\\s+)?(' + domEvents.join('|') + ')(?:\\s|$)');
  670. }
  671. pushDomEvents([
  672. 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
  673. 'touchstart', 'touchend', 'touchmove',
  674. 'click', 'dblclick',
  675. 'keyup', 'keydown', 'keypress',
  676. 'submit', 'change',
  677. 'focus', 'blur'
  678. ]);
  679. function containHandlerToCurentView(handler, cid) {
  680. return function(event) {
  681. var view = $(event.target).view({helper: false});
  682. if (view && view.cid === cid) {
  683. event.originalContext = this;
  684. handler(event);
  685. }
  686. };
  687. }
  688. function bindEventHandler(eventName, params) {
  689. eventName += params.originalName;
  690. var callback = params.handler,
  691. method = typeof callback === 'function' ? callback : this[callback];
  692. if (!method) {
  693. throw new Error('Event "' + callback + '" does not exist ' + (this.name || this.cid) + ':' + eventName);
  694. }
  695. return _.bind(function() {
  696. try {
  697. method.apply(this, arguments);
  698. } catch (e) {
  699. Thorax.onException('thorax-exception: ' + (this.name || this.cid) + ':' + eventName, e);
  700. }
  701. }, params.context || this);
  702. }
  703. function eventParamsFromEventItem(name, handler, context) {
  704. var params = {
  705. originalName: name,
  706. handler: typeof handler === 'string' ? this[handler] : handler
  707. };
  708. if (name.match(domEventRegexp)) {
  709. var match = eventSplitter.exec(name);
  710. params.nested = !!match[1];
  711. params.name = match[2];
  712. params.type = 'DOM';
  713. params.selector = match[3];
  714. } else {
  715. params.name = name;
  716. params.type = 'view';
  717. }
  718. params.context = context;
  719. return params;
  720. }
  721. ;;
  722. /*global getOptionsData, viewHelperAttributeName */
  723. var viewPlaceholderAttributeName = 'data-view-tmp';
  724. var viewTemplateOverrides = {};
  725. Thorax.HelperView = Thorax.View.extend({
  726. _ensureElement: function() {
  727. Thorax.View.prototype._ensureElement.apply(this, arguments);
  728. this.$el.attr(viewHelperAttributeName, this._helperName);
  729. },
  730. _getContext: function() {
  731. return this.parent._getContext.apply(this.parent, arguments);
  732. },
  733. });
  734. // Ensure nested inline helpers will always have this.parent
  735. // set to the view containing the template
  736. function getParent(parent) {
  737. // The `view` helper is a special case as it embeds
  738. // a view instead of creating a new one
  739. while (parent._helperName && parent._helperName !== 'view') {
  740. parent = parent.parent;
  741. }
  742. return parent;
  743. }
  744. Handlebars.registerViewHelper = function(name, ViewClass, callback) {
  745. if (arguments.length === 2) {
  746. if (ViewClass.factory) {
  747. callback = ViewClass.callback;
  748. } else {
  749. callback = ViewClass;
  750. ViewClass = Thorax.HelperView;
  751. }
  752. }
  753. Handlebars.registerHelper(name, function() {
  754. var args = _.toArray(arguments),
  755. options = args.pop(),
  756. declaringView = getOptionsData(options).view;
  757. var viewOptions = {
  758. template: options.fn || Handlebars.VM.noop,
  759. inverse: options.inverse,
  760. options: options.hash,
  761. declaringView: declaringView,
  762. parent: getParent(declaringView),
  763. _helperName: name,
  764. _helperOptions: {
  765. options: cloneHelperOptions(options),
  766. args: _.clone(args)
  767. }
  768. };
  769. normalizeHTMLAttributeOptions(options.hash);
  770. _.extend(viewOptions, _.pick(options.hash, htmlAttributesToCopy));
  771. // Check to see if we have an existing instance that we can reuse
  772. var instance = _.find(declaringView._previousHelpers, function(child) {
  773. return compareHelperOptions(viewOptions, child);
  774. });
  775. // Create the instance if we don't already have one
  776. if (!instance) {
  777. if (ViewClass.factory) {
  778. instance = ViewClass.factory(args, viewOptions);
  779. if (!instance) {
  780. return '';
  781. }
  782. instance._helperName = viewOptions._helperName;
  783. instance._helperOptions = viewOptions._helperOptions;
  784. } else {
  785. instance = new ViewClass(viewOptions);
  786. }
  787. args.push(instance);
  788. declaringView._addChild(instance);
  789. declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
  790. declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
  791. callback && callback.apply(this, args);
  792. } else {
  793. declaringView._previousHelpers = _.without(declaringView._previousHelpers, instance);
  794. declaringView.children[instance.cid] = instance;
  795. }
  796. var htmlAttributes = _.pick(options.hash, htmlAttributesToCopy);
  797. htmlAttributes[viewPlaceholderAttributeName] = instance.cid;
  798. var expandTokens = options.hash['expand-tokens'];
  799. return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, '', expandTokens ? this : null));
  800. });
  801. var helper = Handlebars.helpers[name];
  802. return helper;
  803. };
  804. Thorax.View.on('append', function(scope, callback) {
  805. (scope || this.$el).find('[' + viewPlaceholderAttributeName + ']').forEach(function(el) {
  806. var placeholderId = el.getAttribute(viewPlaceholderAttributeName),
  807. view = this.children[placeholderId];
  808. if (view) {
  809. //see if the view helper declared an override for the view
  810. //if not, ensure the view has been rendered at least once
  811. if (viewTemplateOverrides[placeholderId]) {
  812. view.render(viewTemplateOverrides[placeholderId]);
  813. delete viewTemplateOverrides[placeholderId];
  814. } else {
  815. view.ensureRendered();
  816. }
  817. $(el).replaceWith(view.el);
  818. callback && callback(view.el);
  819. }
  820. }, this);
  821. });
  822. /**
  823. * Clones the helper options, dropping items that are known to change
  824. * between rendering cycles as appropriate.
  825. */
  826. function cloneHelperOptions(options) {
  827. var ret = _.pick(options, 'fn', 'inverse', 'hash', 'data');
  828. ret.data = _.omit(options.data, 'cid', 'view', 'yield');
  829. return ret;
  830. }
  831. /**
  832. * Checks for basic equality between two sets of parameters for a helper view.
  833. *
  834. * Checked fields include:
  835. * - _helperName
  836. * - All args
  837. * - Hash
  838. * - Data
  839. * - Function and Invert (id based if possible)
  840. *
  841. * This method allows us to determine if the inputs to a given view are the same. If they
  842. * are then we make the assumption that the rendering will be the same (or the child view will
  843. * otherwise rerendering it by monitoring it's parameters as necessary) and reuse the view on
  844. * rerender of the parent view.
  845. */
  846. function compareHelperOptions(a, b) {
  847. function compareValues(a, b) {
  848. return _.every(a, function(value, key) {
  849. return b[key] === value;
  850. });
  851. }
  852. if (a._helperName !== b._helperName) {
  853. return false;
  854. }
  855. a = a._helperOptions;
  856. b = b._helperOptions;
  857. // Implements a first level depth comparison
  858. return a.args.length === b.args.length
  859. && compareValues(a.args, b.args)
  860. && _.isEqual(_.keys(a.options), _.keys(b.options))
  861. && _.every(a.options, function(value, key) {
  862. if (key === 'data' || key === 'hash') {
  863. return compareValues(a.options[key], b.options[key]);
  864. } else if (key === 'fn' || key === 'inverse') {
  865. return b.options[key] === value
  866. || (value && _.has(value, 'program') && ((b.options[key] || {}).program === value.program));
  867. }
  868. return b.options[key] === value;
  869. });
  870. }
  871. ;;
  872. /*global getValue, inheritVars, walkInheritTree */
  873. function dataObject(type, spec) {
  874. spec = inheritVars[type] = _.defaults({
  875. name: '_' + type + 'Events',
  876. event: true
  877. }, spec);
  878. // Add a callback in the view constructor
  879. spec.ctor = function() {
  880. if (this[type]) {
  881. // Need to null this.model/collection so setModel/Collection will
  882. // not treat it as the old model/collection and immediately return
  883. var object = this[type];
  884. this[type] = null;
  885. this[spec.set](object);
  886. }
  887. };
  888. function setObject(dataObject, options) {
  889. var old = this[type],
  890. $el = getValue(this, spec.$el);
  891. if (dataObject === old) {
  892. return this;
  893. }
  894. if (old) {
  895. this.unbindDataObject(old);
  896. }
  897. if (dataObject) {
  898. this[type] = dataObject;
  899. if (spec.loading) {
  900. spec.loading.call(this);
  901. }
  902. this.bindDataObject(type, dataObject, _.extend({}, this.options, options));
  903. $el.attr(spec.cidAttrName, dataObject.cid);
  904. dataObject.trigger('set', dataObject, old);
  905. } else {
  906. this[type] = false;
  907. if (spec.change) {
  908. spec.change.call(this, false);
  909. }
  910. $el.removeAttr(spec.cidAttrName);
  911. }
  912. this.trigger('change:data-object', type, dataObject, old);
  913. return this;
  914. }
  915. Thorax.View.prototype[spec.set] = setObject;
  916. }
  917. _.extend(Thorax.View.prototype, {
  918. bindDataObject: function(type, dataObject, options) {
  919. if (this._boundDataObjectsByCid[dataObject.cid]) {
  920. return false;
  921. }
  922. this._boundDataObjectsByCid[dataObject.cid] = dataObject;
  923. var options = this._modifyDataObjectOptions(dataObject, _.extend({}, inheritVars[type].defaultOptions, options));
  924. this._objectOptionsByCid[dataObject.cid] = options;
  925. bindEvents.call(this, type, dataObject, this.constructor);
  926. bindEvents.call(this, type, dataObject, this);
  927. var spec = inheritVars[type];
  928. spec.bindCallback && spec.bindCallback.call(this, dataObject, options);
  929. if (dataObject.shouldFetch && dataObject.shouldFetch(options)) {
  930. loadObject(dataObject, options);
  931. } else if (inheritVars[type].change) {
  932. // want to trigger built in rendering without triggering event on model
  933. inheritVars[type].change.call(this, dataObject, options);
  934. }
  935. return true;
  936. },
  937. unbindDataObject: function (dataObject) {
  938. if (!this._boundDataObjectsByCid[dataObject.cid]) {
  939. return false;
  940. }
  941. delete this._boundDataObjectsByCid[dataObject.cid];
  942. this.stopListening(dataObject);
  943. delete this._objectOptionsByCid[dataObject.cid];
  944. return true;
  945. },
  946. _modifyDataObjectOptions: function(dataObject, options) {
  947. return options;
  948. }
  949. });
  950. function bindEvents(type, target, source) {
  951. var context = this;
  952. walkInheritTree(source, '_' + type + 'Events', true, function(event) {
  953. // getEventCallback will resolve if it is a string or a method
  954. // and return a method
  955. context.listenTo(target, event[0], _.bind(getEventCallback(event[1], context), event[2] || context));
  956. });
  957. }
  958. function loadObject(dataObject, options) {
  959. if (dataObject.load) {
  960. dataObject.load(function() {
  961. options && options.success && options.success(dataObject);
  962. }, options);
  963. } else {
  964. dataObject.fetch(options);
  965. }
  966. }
  967. function getEventCallback(callback, context) {
  968. if (typeof callback === 'function') {
  969. return callback;
  970. } else {
  971. return context[callback];
  972. }
  973. }
  974. ;;
  975. /*global createRegistryWrapper, dataObject, getValue */
  976. var modelCidAttributeName = 'data-model-cid';
  977. Thorax.Model = Backbone.Model.extend({
  978. isEmpty: function() {
  979. return !this.isPopulated();
  980. },
  981. isPopulated: function() {
  982. // We are populated if we have attributes set
  983. var attributes = _.clone(this.attributes),
  984. defaults = getValue(this, 'defaults') || {};
  985. for (var default_key in defaults) {
  986. if (attributes[default_key] != defaults[default_key]) {
  987. return true;
  988. }
  989. delete attributes[default_key];
  990. }
  991. var keys = _.keys(attributes);
  992. return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
  993. },
  994. shouldFetch: function(options) {
  995. // url() will throw if model has no `urlRoot` and no `collection`
  996. // or has `collection` and `collection` has no `url`
  997. var url;
  998. try {
  999. url = getValue(this, 'url');
  1000. } catch(e) {
  1001. url = false;
  1002. }
  1003. return options.fetch && !!url && !this.isPopulated();
  1004. }
  1005. });
  1006. Thorax.Models = {};
  1007. createRegistryWrapper(Thorax.Model, Thorax.Models);
  1008. dataObject('model', {
  1009. set: 'setModel',
  1010. defaultOptions: {
  1011. render: true,
  1012. fetch: true,
  1013. success: false,
  1014. errors: true
  1015. },
  1016. change: onModelChange,
  1017. $el: '$el',
  1018. cidAttrName: modelCidAttributeName
  1019. });
  1020. function onModelChange(model) {
  1021. var modelOptions = model && this._objectOptionsByCid[model.cid];
  1022. // !modelOptions will be true when setModel(false) is called
  1023. if (!modelOptions || (modelOptions && modelOptions.render)) {
  1024. this.render();
  1025. }
  1026. }
  1027. Thorax.View.on({
  1028. model: {
  1029. error: function(model, errors) {
  1030. if (this._objectOptionsByCid[model.cid].errors) {
  1031. this.trigger('error', errors, model);
  1032. }
  1033. },
  1034. change: function(model) {
  1035. onModelChange.call(this, model);
  1036. }
  1037. }
  1038. });
  1039. $.fn.model = function(view) {
  1040. var $this = $(this),
  1041. modelElement = $this.closest('[' + modelCidAttributeName + ']'),
  1042. modelCid = modelElement && modelElement.attr(modelCidAttributeName);
  1043. if (modelCid) {
  1044. var view = view || $this.view();
  1045. if (view && view.model && view.model.cid === modelCid) {
  1046. return view.model || false;
  1047. }
  1048. var collection = $this.collection(view);
  1049. if (collection) {
  1050. return collection.get(modelCid);
  1051. }
  1052. }
  1053. return false;
  1054. };
  1055. ;;
  1056. /*global createRegistryWrapper, dataObject, getEventCallback, getValue, modelCidAttributeName, viewCidAttributeName */
  1057. var _fetch = Backbone.Collection.prototype.fetch,
  1058. _reset = Backbone.Collection.prototype.reset,
  1059. collectionCidAttributeName = 'data-collection-cid',
  1060. collectionEmptyAttributeName = 'data-collection-empty',
  1061. collectionElementAttributeName = 'data-collection-element',
  1062. ELEMENT_NODE_TYPE = 1;
  1063. Thorax.Collection = Backbone.Collection.extend({
  1064. model: Thorax.Model || Backbone.Model,
  1065. initialize: function() {
  1066. this.cid = _.uniqueId('collection');
  1067. return Backbone.Collection.prototype.initialize.apply(this, arguments);
  1068. },
  1069. isEmpty: function() {
  1070. if (this.length > 0) {
  1071. return false;
  1072. } else {
  1073. return this.length === 0 && this.isPopulated();
  1074. }
  1075. },
  1076. isPopulated: function() {
  1077. return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
  1078. },
  1079. shouldFetch: function(options) {
  1080. return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
  1081. },
  1082. fetch: function(options) {
  1083. options = options || {};
  1084. var success = options.success;
  1085. options.success = function(collection, response) {
  1086. collection._fetched = true;
  1087. success && success(collection, response);
  1088. };
  1089. return _fetch.apply(this, arguments);
  1090. },
  1091. reset: function(models, options) {
  1092. this._fetched = !!models;
  1093. return _reset.call(this, models, options);
  1094. }
  1095. });
  1096. Thorax.Collections = {};
  1097. createRegistryWrapper(Thorax.Collection, Thorax.Collections);
  1098. dataObject('collection', {
  1099. set: 'setCollection',
  1100. bindCallback: onSetCollection,
  1101. defaultOptions: {
  1102. render: true,
  1103. fetch: true,
  1104. success: false,
  1105. errors: true
  1106. },
  1107. change: onCollectionReset,
  1108. $el: 'getCollectionElement',
  1109. cidAttrName: collectionCidAttributeName
  1110. });
  1111. _.extend(Thorax.View.prototype, {
  1112. _collectionSelector: '[' + collectionElementAttributeName + ']',
  1113. //appendItem(model [,index])
  1114. //appendItem(html_string, index)
  1115. //appendItem(view, index)
  1116. appendItem: function(model, index, options) {
  1117. //empty item
  1118. if (!model) {
  1119. return;
  1120. }
  1121. var itemView,
  1122. $el = this.getCollectionElement();
  1123. options = _.defaults(options || {}, {
  1124. filter: true
  1125. });
  1126. //if index argument is a view
  1127. index && index.el && (index = $el.children().indexOf(index.el) + 1);
  1128. //if argument is a view, or html string
  1129. if (model.el || typeof model === 'string') {
  1130. itemView = model;
  1131. model = false;
  1132. } else {
  1133. index = index || this.collection.indexOf(model) || 0;
  1134. itemView = this.renderItem(model, index);
  1135. }
  1136. if (itemView) {
  1137. itemView.cid && this._addChild(itemView);
  1138. //if the renderer's output wasn't contained in a tag, wrap it in a div
  1139. //plain text, or a mixture of top level text nodes and element nodes
  1140. //will get wrapped
  1141. if (typeof itemView === 'string' && !itemView.match(/^\s*</m)) {
  1142. itemView = '<div>' + itemView + '</div>';
  1143. }
  1144. var itemElement = itemView.el ? [itemView.el] : _.filter($(itemView), function(node) {
  1145. //filter out top level whitespace nodes
  1146. return node.nodeType === ELEMENT_NODE_TYPE;
  1147. });
  1148. model && $(itemElement).attr(modelCidAttributeName, model.cid);
  1149. var previousModel = index > 0 ? this.collection.at(index - 1) : false;
  1150. if (!previousModel) {
  1151. $el.prepend(itemElement);
  1152. } else {
  1153. //use last() as appendItem can accept multiple nodes from a template
  1154. var last = $el.find('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
  1155. last.after(itemElement);
  1156. }
  1157. this.trigger('append', null, function(el) {
  1158. el.setAttribute(modelCidAttributeName, model.cid);
  1159. });
  1160. !options.silent && this.trigger('rendered:item', this, this.collection, model, itemElement, index);
  1161. options.filter && applyItemVisiblityFilter.call(this, model);
  1162. }
  1163. return itemView;
  1164. },
  1165. // updateItem only useful if there is no item view, otherwise
  1166. // itemView.render() provides the same functionality
  1167. updateItem: function(model) {
  1168. this.removeItem(model);
  1169. this.appendItem(model);
  1170. },
  1171. removeItem: function(model) {
  1172. var $el = this.getCollectionElement(),
  1173. viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
  1174. if (!viewEl.length) {
  1175. return false;
  1176. }
  1177. viewEl.remove();
  1178. var viewCid = viewEl.attr(viewCidAttributeName),
  1179. child = this.children[viewCid];
  1180. if (child) {
  1181. this._removeChild(child);
  1182. child.destroy();
  1183. }
  1184. return true;
  1185. },
  1186. renderCollection: function() {
  1187. this.ensureRendered();
  1188. if (!this.collectionRenderer) {
  1189. return;
  1190. }
  1191. if (this.collection) {
  1192. if (this.collection.isEmpty()) {
  1193. handleChangeFromNotEmptyToEmpty.call(this);
  1194. } else {
  1195. handleChangeFromEmptyToNotEmpty.call(this);
  1196. this.collection.forEach(function(item, i) {
  1197. this.appendItem(item, i);
  1198. }, this);
  1199. }
  1200. this.trigger('rendered:collection', this, this.collection);
  1201. applyVisibilityFilter.call(this);
  1202. } else {
  1203. handleChangeFromNotEmptyToEmpty.call(this);
  1204. }
  1205. },
  1206. emptyClass: 'empty',
  1207. renderEmpty: function() {
  1208. if (this.emptyView) {
  1209. var viewOptions = {};
  1210. if (this.emptyTemplate) {
  1211. viewOptions.template = this.emptyTemplate;
  1212. }
  1213. var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
  1214. view.ensureRendered();
  1215. return view;
  1216. } else {
  1217. if (!this.emptyTemplate) {
  1218. this.emptyTemplate = Thorax.Util.getTemplate(this.name + '-empty', true);
  1219. }
  1220. return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
  1221. }
  1222. },
  1223. renderItem: function(model, i) {
  1224. if (this.itemView) {
  1225. var viewOptions = {
  1226. model: model
  1227. };
  1228. if (this.itemTemplate) {
  1229. viewOptions.template = this.itemTemplate;
  1230. }
  1231. var view = Thorax.Util.getViewInstance(this.itemView, viewOptions);
  1232. view.ensureRendered();
  1233. return view;
  1234. } else {
  1235. if (!this.itemTemplate) {
  1236. this.itemTemplate = Thorax.Util.getTemplate(this.name + '-item');
  1237. }
  1238. return this.renderTemplate(this.itemTemplate, this.itemContext(model, i));
  1239. }
  1240. },
  1241. itemContext: function(model /*, i */) {
  1242. return model.attributes;
  1243. },
  1244. appendEmpty: function() {
  1245. var $el = this.getCollectionElement();
  1246. $el.empty();
  1247. var emptyContent = this.renderEmpty();
  1248. emptyContent && this.appendItem(emptyContent, 0, {
  1249. silent: true,
  1250. filter: false
  1251. });
  1252. this.trigger('rendered:empty', this, this.collection);
  1253. },
  1254. getCollectionElement: function() {
  1255. var element = this.$(this._collectionSelector);
  1256. return element.length === 0 ? this.$el : element;
  1257. },
  1258. // Events that will only be bound to "this.collection"
  1259. _collectionRenderingEvents: {
  1260. reset: onCollectionReset,
  1261. sort: onCollectionReset,
  1262. filter: function() {
  1263. applyVisibilityFilter.call(this);
  1264. },
  1265. change: function(model) {
  1266. // If we rendered with item views, model changes will be observed
  1267. // by the generated item view but if we rendered with templates
  1268. // then model changes need to be bound as nothing is watching
  1269. !this.itemView && this.updateItem(model);
  1270. applyItemVisiblityFilter.call(this, model);
  1271. },
  1272. add: function(model) {
  1273. var $el = this.getCollectionElement();
  1274. this.collection.length === 1 && $el.length && handleChangeFromEmptyToNotEmpty.call(this);
  1275. if ($el.length) {
  1276. var index = this.collection.indexOf(model);
  1277. this.appendItem(model, index);
  1278. }
  1279. },
  1280. remove: function(model) {
  1281. var $el = this.getCollectionElement();
  1282. this.removeItem(model);
  1283. this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
  1284. }
  1285. }
  1286. });
  1287. Thorax.View.on({
  1288. collection: {
  1289. error: function(collection, message) {
  1290. if (this._objectOptionsByCid[collection.cid].errors) {
  1291. this.trigger('error', message, collection);
  1292. }
  1293. }
  1294. }
  1295. });
  1296. function onCollectionReset(collection) {
  1297. var options = collection && this._objectOptionsByCid[collection.cid];
  1298. // we would want to still render in the case that the
  1299. // collection has transitioned to being falsy
  1300. if (!collection || (options && options.render)) {
  1301. this.renderCollection();
  1302. }
  1303. }
  1304. function onSetCollection(collection) {
  1305. if (this.collectionRenderer && collection) {
  1306. _.each(this._collectionRenderingEvents, function(callback, eventName) {
  1307. // getEventCallback will resolve if it is a string or a method
  1308. // and return a method
  1309. this.listenTo(collection, eventName, getEventCallback(callback, this));
  1310. }, this);
  1311. }
  1312. }
  1313. function applyVisibilityFilter() {
  1314. if (this.itemFilter) {
  1315. this.collection.forEach(function(model) {
  1316. applyItemVisiblityFilter.call(this, model);
  1317. }, this);
  1318. }
  1319. }
  1320. function applyItemVisiblityFilter(model) {
  1321. var $el = this.getCollectionElement();
  1322. this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
  1323. }
  1324. function itemShouldBeVisible(model) {
  1325. return this.itemFilter(model, this.collection.indexOf(model));
  1326. }
  1327. function handleChangeFromEmptyToNotEmpty() {
  1328. var $el = this.getCollectionElement();
  1329. this.emptyClass && $el.removeClass(this.emptyClass);
  1330. $el.removeAttr(collectionEmptyAttributeName);
  1331. $el.empty();
  1332. }
  1333. function handleChangeFromNotEmptyToEmpty() {
  1334. var $el = this.getCollectionElement();
  1335. this.emptyClass && $el.addClass(this.emptyClass);
  1336. $el.attr(collectionEmptyAttributeName, true);
  1337. this.appendEmpty();
  1338. }
  1339. //$(selector).collection() helper
  1340. $.fn.collection = function(view) {
  1341. if (view && view.collection) {
  1342. return view.collection;
  1343. }
  1344. var $this = $(this),
  1345. collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
  1346. collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
  1347. if (collectionCid) {
  1348. view = $this.view();
  1349. if (view) {
  1350. return view.collection;
  1351. }
  1352. }
  1353. return false;
  1354. };
  1355. ;;
  1356. /*global inheritVars */
  1357. inheritVars.model.defaultOptions.populate = true;
  1358. var oldModelChange = inheritVars.model.change;
  1359. inheritVars.model.change = function() {
  1360. oldModelChange.apply(this, arguments);
  1361. // TODO : What can we do to remove this duplication?
  1362. var modelOptions = this.model && this._objectOptionsByCid[this.model.cid];
  1363. if (modelOptions && modelOptions.populate) {
  1364. this.populate(this.model.attributes, modelOptions.populate === true ? {} : modelOptions.populate);
  1365. }
  1366. };
  1367. inheritVars.model.defaultOptions.populate = true;
  1368. _.extend(Thorax.View.prototype, {
  1369. //serializes a form present in the view, returning the serialized data
  1370. //as an object
  1371. //pass {set:false} to not update this.model if present
  1372. //can pass options, callback or event in any order
  1373. serialize: function() {
  1374. var callback, options, event;
  1375. //ignore undefined arguments in case event was null
  1376. for (var i = 0; i < arguments.length; ++i) {
  1377. if (typeof arguments[i] === 'function') {
  1378. callback = arguments[i];
  1379. } else if (typeof arguments[i] === 'object') {
  1380. if ('stopPropagation' in arguments[i] && 'preventDefault' in arguments[i]) {
  1381. event = arguments[i];
  1382. } else {
  1383. options = arguments[i];
  1384. }
  1385. }
  1386. }
  1387. if (event && !this._preventDuplicateSubmission(event)) {
  1388. return;
  1389. }
  1390. options = _.extend({
  1391. set: true,
  1392. validate: true,
  1393. children: true,
  1394. silent: true
  1395. }, options || {});
  1396. var attributes = options.attributes || {};
  1397. //callback has context of element
  1398. var view = this;
  1399. var errors = [];
  1400. eachNamedInput.call(this, options, function() {
  1401. var value = view._getInputValue(this, options, errors);
  1402. if (typeof value !== 'undefined') {
  1403. objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'serialize'}, function(object, key) {
  1404. if (!object[key]) {
  1405. object[key] = value;
  1406. } else if (_.isArray(object[key])) {
  1407. object[key].push(value);
  1408. } else {
  1409. object[key] = [object[key], value];
  1410. }
  1411. });
  1412. }
  1413. });
  1414. this.trigger('serialize', attributes, options);
  1415. if (options.validate) {
  1416. var validateInputErrors = this.validateInput(attributes);
  1417. if (validateInputErrors && validateInputErrors.length) {
  1418. errors = errors.concat(validateInputErrors);
  1419. }
  1420. this.trigger('validate', attributes, errors, options);
  1421. if (errors.length) {
  1422. this.trigger('error', errors);
  1423. return;
  1424. }
  1425. }
  1426. if (options.set && this.model) {
  1427. if (!this.model.set(attributes, {silent: options.silent})) {
  1428. return false;
  1429. }
  1430. }
  1431. callback && callback.call(this, attributes, _.bind(resetSubmitState, this));
  1432. return attributes;
  1433. },
  1434. _preventDuplicateSubmission: function(event, callback) {
  1435. event.preventDefault();
  1436. var form = $(event.target);
  1437. if ((event.target.tagName || '').toLowerCase() !== 'form') {
  1438. // Handle non-submit events by gating on the form
  1439. form = $(event.target).closest('form');
  1440. }
  1441. if (!form.attr('data-submit-wait')) {
  1442. form.attr('data-submit-wait', 'true');
  1443. if (callback) {
  1444. callback.call(this, event);
  1445. }
  1446. return true;
  1447. } else {
  1448. return false;
  1449. }
  1450. },
  1451. //populate a form from the passed attributes or this.model if present
  1452. populate: function(attributes, options) {
  1453. options = _.extend({
  1454. children: true
  1455. }, options || {});
  1456. var value, attributes = attributes || this._getContext();
  1457. //callback has context of element
  1458. eachNamedInput.call(this, options, function() {
  1459. objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'populate'}, function(object, key) {
  1460. if (object && typeof (value = object[key]) !== 'undefined') {
  1461. //will only execute if we have a name that matches the structure in attributes
  1462. if (this.type === 'checkbox' && _.isBoolean(value)) {
  1463. this.checked = value;
  1464. } else if (this.type === 'checkbox' || this.type === 'radio') {
  1465. this.checked = value == this.value;
  1466. } else {
  1467. this.value = value;
  1468. }
  1469. }
  1470. });
  1471. });
  1472. this.trigger('populate', attributes);
  1473. },
  1474. //perform form validation, implemented by child class
  1475. validateInput: function(/* attributes, options, errors */) {},
  1476. _getInputValue: function(input /* , options, errors */) {
  1477. if (input.type === 'checkbox' || input.type === 'radio') {
  1478. if (input.checked) {
  1479. return input.value;
  1480. }
  1481. } else if (input.multiple === true) {
  1482. var values = [];
  1483. $('option', input).each(function() {
  1484. if (this.selected) {
  1485. values.push(this.value);
  1486. }
  1487. });
  1488. return values;
  1489. } else {
  1490. return input.value;
  1491. }
  1492. }
  1493. });
  1494. Thorax.View.on({
  1495. error: function() {
  1496. resetSubmitState.call(this);
  1497. // If we errored with a model we want to reset the content but leave the UI
  1498. // intact. If the user updates the data and serializes any overwritten data
  1499. // will be restored.
  1500. if (this.model && this.model.previousAttributes) {
  1501. this.model.set(this.model.previousAttributes(), {
  1502. silent: true
  1503. });
  1504. }
  1505. },
  1506. deactivated: function() {
  1507. resetSubmitState.call(this);
  1508. }
  1509. });
  1510. function eachNamedInput(options, iterator, context) {
  1511. var i = 0,
  1512. self = this;
  1513. this.$('select,input,textarea', options.root || this.el).each(function() {
  1514. if (!options.children) {
  1515. if (self !== $(this).view({helper: false})) {
  1516. return;
  1517. }
  1518. }
  1519. if (this.type !== 'button' && this.type !== 'cancel' && this.type !== 'submit' && this.name && this.name !== '') {
  1520. iterator.call(context || this, i, this);
  1521. ++i;
  1522. }
  1523. });
  1524. }
  1525. //calls a callback with the correct object fragment and key from a compound name
  1526. function objectAndKeyFromAttributesAndName(attributes, name, options, callback) {
  1527. var key,
  1528. object = attributes,
  1529. keys = name.split('['),
  1530. mode = options.mode;
  1531. for (var i = 0; i < keys.length - 1; ++i) {
  1532. key = keys[i].replace(']', '');
  1533. if (!object[key]) {
  1534. if (mode === 'serialize') {
  1535. object[key] = {};
  1536. } else {
  1537. return callback.call(this, false, key);
  1538. }
  1539. }
  1540. object = object[key];
  1541. }
  1542. key = keys[keys.length - 1].replace(']', '');
  1543. callback.call(this, object, key);
  1544. }
  1545. function resetSubmitState() {
  1546. this.$('form').removeAttr('data-submit-wait');
  1547. }
  1548. ;;
  1549. var layoutCidAttributeName = 'data-layout-cid';
  1550. Thorax.LayoutView = Thorax.View.extend({
  1551. render: function(output) {
  1552. //TODO: fixme, lumbar inserts templates after JS, most of the time this is fine
  1553. //but Application will be created in init.js (unlike most views)
  1554. //so need to put this here so the template will be picked up
  1555. var layoutTemplate;
  1556. if (this.name) {
  1557. layoutTemplate = Thorax.Util.getTemplate(this.name, true);
  1558. }
  1559. //a template is optional in a layout
  1560. if (output || this.template || layoutTemplate) {
  1561. //but if present, it must have embedded an element containing layoutCidAttributeName
  1562. var response = Thorax.View.prototype.render.call(this, output || this.template || layoutTemplate);
  1563. ensureLayoutViewsTargetElement.call(this);
  1564. return response;
  1565. } else {
  1566. ensureLayoutCid.call(this);
  1567. }
  1568. },
  1569. setView: function(view, options) {
  1570. options = _.extend({
  1571. scroll: true,
  1572. destroy: true
  1573. }, options || {});
  1574. if (typeof view === 'string') {
  1575. view = new (Thorax.Util.registryGet(Thorax, 'Views', view, false))();
  1576. }
  1577. this.ensureRendered();
  1578. var oldView = this._view;
  1579. if (view === oldView) {
  1580. return false;
  1581. }
  1582. if (options.destroy && view) {
  1583. view._shouldDestroyOnNextSetView = true;
  1584. }
  1585. this.trigger('change:view:start', view, oldView, options);
  1586. if (oldView) {
  1587. this._removeChild(oldView);
  1588. oldView.$el.remove();
  1589. triggerLifecycleEvent.call(oldView, 'deactivated', options);
  1590. if (oldView._shouldDestroyOnNextSetView) {
  1591. oldView.destroy();
  1592. }
  1593. }
  1594. if (view) {
  1595. triggerLifecycleEvent.call(this, 'activated', options);
  1596. view.trigger('activated', options);
  1597. this._addChild(view);
  1598. this._view = view;
  1599. this._view.appendTo(getLayoutViewsTargetElement.call(this));
  1600. } else {
  1601. this._view = undefined;
  1602. }
  1603. this.trigger('change:view:end', view, oldView, options);
  1604. return view;
  1605. },
  1606. getView: function() {
  1607. return this._view;
  1608. }
  1609. });
  1610. Handlebars.registerHelper('layout', function(options) {
  1611. options.hash[layoutCidAttributeName] = getOptionsData(options).view.cid;
  1612. return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, '', this));
  1613. });
  1614. function triggerLifecycleEvent(eventName, options) {
  1615. options = options || {};
  1616. options.target = this;
  1617. this.trigger(eventName, options);
  1618. _.each(this.children, function(child) {
  1619. child.trigger(eventName, options);
  1620. });
  1621. }
  1622. function ensureLayoutCid() {
  1623. ++this._renderCount;
  1624. //set the layoutCidAttributeName on this.$el if there was no template
  1625. this.$el.attr(layoutCidAttributeName, this.cid);
  1626. }
  1627. function ensureLayoutViewsTargetElement() {
  1628. if (!this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0]) {
  1629. throw new Error('No layout element found in ' + (this.name || this.cid));
  1630. }
  1631. }
  1632. function getLayoutViewsTargetElement() {
  1633. return this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0] || this.el[0] || this.el;
  1634. }
  1635. ;;
  1636. /*global createRegistryWrapper */
  1637. //Router
  1638. function initializeRouter() {
  1639. Backbone.history || (Backbone.history = new Backbone.History());
  1640. Backbone.history.on('route', onRoute, this);
  1641. //router does not have a built in destroy event
  1642. //but ViewController does
  1643. this.on('destroyed', function() {
  1644. Backbone.history.off('route', onRoute, this);
  1645. });
  1646. }
  1647. Thorax.Router = Backbone.Router.extend({
  1648. constructor: function() {
  1649. var response = Thorax.Router.__super__.constructor.apply(this, arguments);
  1650. initializeRouter.call(this);
  1651. return response;
  1652. },
  1653. route: function(route, name, callback) {
  1654. if (!callback) {
  1655. callback = this[name];
  1656. }
  1657. //add a route:before event that is fired before the callback is called
  1658. return Backbone.Router.prototype.route.call(this, route, name, function() {
  1659. this.trigger.apply(this, ['route:before', route, name].concat(Array.prototype.slice.call(arguments)));
  1660. return callback.apply(this, arguments);
  1661. });
  1662. }
  1663. });
  1664. Thorax.Routers = {};
  1665. createRegistryWrapper(Thorax.Router, Thorax.Routers);
  1666. function onRoute(router /* , name */) {
  1667. if (this === router) {
  1668. this.trigger.apply(this, ['route'].concat(Array.prototype.slice.call(arguments, 1)));
  1669. }
  1670. }
  1671. ;;
  1672. Thorax.CollectionHelperView = Thorax.HelperView.extend({
  1673. // Forward render events to the parent
  1674. events: {
  1675. 'rendered:item': forwardRenderEvent('rendered:item'),
  1676. 'rendered:collection': forwardRenderEvent('rendered:collection'),
  1677. 'rendered:empty': forwardRenderEvent('rendered:empty')
  1678. },
  1679. collectionRenderer: true,
  1680. constructor: function(options) {
  1681. _.each(collectionOptionNames, function(viewAttributeName, helperOptionName) {
  1682. if (options.options[helperOptionName]) {
  1683. var value = options.options[helperOptionName];
  1684. if (viewAttributeName === 'itemTemplate' || viewAttributeName === 'emptyTemplate') {
  1685. value = Thorax.Util.getTemplate(value);
  1686. }
  1687. options[viewAttributeName] = value;
  1688. }
  1689. });
  1690. // Handlebars.VM.noop is passed in the handlebars options object as
  1691. // a default for fn and inverse, if a block was present. Need to
  1692. // check to ensure we don't pick the empty / null block up.
  1693. if (!options.itemTemplate && options.template && options.template !== Handlebars.VM.noop) {
  1694. options.itemTemplate = options.template;
  1695. options.template = Handlebars.VM.noop;
  1696. }
  1697. if (!options.emptyTemplate && options.inverse && options.inverse !== Handlebars.VM.noop) {
  1698. options.emptyTemplate = options.inverse;
  1699. options.inverse = Handlebars.VM.noop;
  1700. }
  1701. !options.template && (options.template = Handlebars.VM.noop);
  1702. var response = Thorax.HelperView.call(this, options);
  1703. if (this.parent.name) {
  1704. if (!this.emptyTemplate) {
  1705. this.emptyTemplate = Thorax.Util.getTemplate(this.parent.name + '-empty', true);
  1706. }
  1707. if (!this.itemTemplate) {
  1708. this.itemTemplate = Thorax.Util.getTemplate(this.parent.name + '-item', true);
  1709. }
  1710. }
  1711. return response;
  1712. },
  1713. itemContext: function() {
  1714. return this.parent.itemContext.apply(this.parent, arguments);
  1715. },
  1716. setAsPrimaryCollectionHelper: function() {
  1717. _.each(forwardableProperties, function(propertyName) {
  1718. forwardMissingProperty.call(this, propertyName);
  1719. }, this);
  1720. if (this.parent.itemFilter && !this.itemFilter) {
  1721. this.itemFilter = function() {
  1722. return this.parent.itemFilter.apply(this.parent, arguments);
  1723. };
  1724. }
  1725. }
  1726. });
  1727. var collectionOptionNames = {
  1728. 'item-template': 'itemTemplate',
  1729. 'empty-template': 'emptyTemplate',
  1730. 'item-view': 'itemView',
  1731. 'empty-view': 'emptyView',
  1732. 'empty-class': 'emptyClass'
  1733. };
  1734. function forwardRenderEvent(eventName) {
  1735. return function() {
  1736. var args = _.toArray(arguments);
  1737. args.unshift(eventName);
  1738. this.parent.trigger.apply(this.parent, args);
  1739. }
  1740. }
  1741. var forwardableProperties = [
  1742. 'itemTemplate',
  1743. 'itemView',
  1744. 'emptyTemplate',
  1745. 'emptyView'
  1746. ];
  1747. function forwardMissingProperty(propertyName) {
  1748. var parent = getParent(this);
  1749. if (!this[propertyName]) {
  1750. var prop = parent[propertyName];
  1751. if (prop){
  1752. this[propertyName] = prop;
  1753. }
  1754. }
  1755. }
  1756. Handlebars.registerViewHelper('collection', Thorax.CollectionHelperView, function(collection, view) {
  1757. if (arguments.length === 1) {
  1758. view = collection;
  1759. collection = view.parent.collection;
  1760. collection && view.setAsPrimaryCollectionHelper();
  1761. view.$el.attr(collectionElementAttributeName, 'true');
  1762. // propagate future changes to the parent's collection object
  1763. // to the helper view
  1764. view.listenTo(view.parent, 'change:data-object', function(type, dataObject) {
  1765. if (type === 'collection') {
  1766. view.setAsPrimaryCollectionHelper();
  1767. view.setCollection(dataObject);
  1768. }
  1769. });
  1770. }
  1771. collection && view.setCollection(collection);
  1772. });
  1773. Handlebars.registerHelper('collection-element', function(options) {
  1774. var hash = options.hash;
  1775. normalizeHTMLAttributeOptions(hash);
  1776. hash.tagName = hash.tagName || 'div';
  1777. hash[collectionElementAttributeName] = true;
  1778. return new Handlebars.SafeString(Thorax.Util.tag.call(this, hash, '', this));
  1779. });
  1780. ;;
  1781. Handlebars.registerHelper('empty', function(dataObject, options) {
  1782. if (arguments.length === 1) {
  1783. options = dataObject;
  1784. }
  1785. var view = getOptionsData(options).view;
  1786. if (arguments.length === 1) {
  1787. dataObject = view.model;
  1788. }
  1789. // listeners for the empty helper rather than listeners
  1790. // that are themselves empty
  1791. if (!view._emptyListeners) {
  1792. view._emptyListeners = {};
  1793. }
  1794. // duck type check for collection
  1795. if (dataObject && !view._emptyListeners[dataObject.cid] && dataObject.models && ('length' in dataObject)) {
  1796. view._emptyListeners[dataObject.cid] = true;
  1797. view.listenTo(dataObject, 'remove', function() {
  1798. if (dataObject.length === 0) {
  1799. view.render();
  1800. }
  1801. });
  1802. view.listenTo(dataObject, 'add', function() {
  1803. if (dataObject.length === 1) {
  1804. view.render();
  1805. }
  1806. });
  1807. view.listenTo(dataObject, 'reset', function() {
  1808. view.render();
  1809. });
  1810. }
  1811. return !dataObject || dataObject.isEmpty() ? options.fn(this) : options.inverse(this);
  1812. });
  1813. ;;
  1814. Handlebars.registerHelper('template', function(name, options) {
  1815. var context = _.extend({fn: options && options.fn}, this, options ? options.hash : {});
  1816. var output = getOptionsData(options).view.renderTemplate(name, context);
  1817. return new Handlebars.SafeString(output);
  1818. });
  1819. Handlebars.registerHelper('yield', function(options) {
  1820. return getOptionsData(options).yield && options.data.yield();
  1821. });
  1822. ;;
  1823. Handlebars.registerHelper('url', function(url) {
  1824. var fragment;
  1825. if (arguments.length > 2) {
  1826. fragment = _.map(_.head(arguments, arguments.length - 1), encodeURIComponent).join('/');
  1827. } else {
  1828. var options = arguments[1],
  1829. hash = (options && options.hash) || options;
  1830. if (hash && hash['expand-tokens']) {
  1831. fragment = Thorax.Util.expandToken(url, this);
  1832. } else {
  1833. fragment = url;
  1834. }
  1835. }
  1836. return (Backbone.history._hasPushState ? Backbone.history.options.root : '#') + fragment;
  1837. });
  1838. ;;
  1839. /*global viewTemplateOverrides */
  1840. Handlebars.registerViewHelper('view', {
  1841. factory: function(args, options) {
  1842. var View = args.length >= 1 ? args[0] : Thorax.View;
  1843. return Thorax.Util.getViewInstance(View, options.options);
  1844. },
  1845. callback: function() {
  1846. var instance = arguments[arguments.length-1],
  1847. options = instance._helperOptions.options,
  1848. placeholderId = instance.cid;
  1849. if (options.fn) {
  1850. viewTemplateOverrides[placeholderId] = options.fn;
  1851. }
  1852. }
  1853. });
  1854. ;;
  1855. var callMethodAttributeName = 'data-call-method',
  1856. triggerEventAttributeName = 'data-trigger-event';
  1857. Handlebars.registerHelper('button', function(method, options) {
  1858. if (arguments.length === 1) {
  1859. options = method;
  1860. method = options.hash.method;
  1861. }
  1862. var hash = options.hash,
  1863. expandTokens = hash['expand-tokens'];
  1864. delete hash['expand-tokens'];
  1865. if (!method && !options.hash.trigger) {
  1866. throw new Error("button helper must have a method name as the first argument or a 'trigger', or a 'method' attribute specified.");
  1867. }
  1868. normalizeHTMLAttributeOptions(hash);
  1869. hash.tagName = hash.tagName || 'button';
  1870. hash.trigger && (hash[triggerEventAttributeName] = hash.trigger);
  1871. delete hash.trigger;
  1872. method && (hash[callMethodAttributeName] = method);
  1873. return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
  1874. });
  1875. Handlebars.registerHelper('link', function() {
  1876. var args = _.toArray(arguments),
  1877. options = args.pop(),
  1878. hash = options.hash,
  1879. // url is an array that will be passed to the url helper
  1880. url = args.length === 0 ? [hash.href] : args,
  1881. expandTokens = hash['expand-tokens'];
  1882. delete hash['expand-tokens'];
  1883. if (!url[0] && url[0] !== '') {
  1884. throw new Error("link helper requires an href as the first argument or an 'href' attribute");
  1885. }
  1886. normalizeHTMLAttributeOptions(hash);
  1887. url.push(options);
  1888. hash.href = Handlebars.helpers.url.apply(this, url);
  1889. hash.tagName = hash.tagName || 'a';
  1890. hash.trigger && (hash[triggerEventAttributeName] = options.hash.trigger);
  1891. delete hash.trigger;
  1892. hash[callMethodAttributeName] = '_anchorClick';
  1893. return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
  1894. });
  1895. var clickSelector = '[' + callMethodAttributeName + '], [' + triggerEventAttributeName + ']';
  1896. function handleClick(event) {
  1897. var target = $(event.target),
  1898. view = target.view({helper: false}),
  1899. methodName = target.attr(callMethodAttributeName),
  1900. eventName = target.attr(triggerEventAttributeName),
  1901. methodResponse = false;
  1902. methodName && (methodResponse = view[methodName].call(view, event));
  1903. eventName && view.trigger(eventName, event);
  1904. target.tagName === "A" && methodResponse === false && event.preventDefault();
  1905. }
  1906. var lastClickHandlerEventName;
  1907. function registerClickHandler() {
  1908. unregisterClickHandler();
  1909. lastClickHandlerEventName = Thorax._fastClickEventName || 'click';
  1910. $(document).on(lastClickHandlerEventName, clickSelector, handleClick);
  1911. }
  1912. function unregisterClickHandler() {
  1913. lastClickHandlerEventName && $(document).off(lastClickHandlerEventName, clickSelector, handleClick);
  1914. }
  1915. $(document).ready(function() {
  1916. if (!Thorax._fastClickEventName) {
  1917. registerClickHandler();
  1918. }
  1919. });
  1920. ;;
  1921. var elementPlaceholderAttributeName = 'data-element-tmp';
  1922. Handlebars.registerHelper('element', function(element, options) {
  1923. normalizeHTMLAttributeOptions(options.hash);
  1924. var cid = _.uniqueId('element'),
  1925. declaringView = getOptionsData(options).view,
  1926. htmlAttributes = _.pick(options.hash, htmlAttributesToCopy);
  1927. htmlAttributes[elementPlaceholderAttributeName] = cid;
  1928. declaringView._elementsByCid || (declaringView._elementsByCid = {});
  1929. declaringView._elementsByCid[cid] = element;
  1930. return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes));
  1931. });
  1932. Thorax.View.on('append', function(scope, callback) {
  1933. (scope || this.$el).find('[' + elementPlaceholderAttributeName + ']').forEach(function(el) {
  1934. var $el = $(el),
  1935. cid = $el.attr(elementPlaceholderAttributeName),
  1936. element = this._elementsByCid[cid];
  1937. // A callback function may be specified as the value
  1938. if (_.isFunction(element)) {
  1939. element = element.call(this);
  1940. }
  1941. $el.replaceWith(element);
  1942. callback && callback(element);
  1943. }, this);
  1944. });
  1945. ;;
  1946. Handlebars.registerHelper('super', function(options) {
  1947. var declaringView = getOptionsData(options).view,
  1948. parent = declaringView.constructor && declaringView.constructor.__super__;
  1949. if (parent) {
  1950. var template = parent.template;
  1951. if (!template) {
  1952. if (!parent.name) {
  1953. throw new Error('Cannot use super helper when parent has no name or template.');
  1954. }
  1955. template = Thorax.Util.getTemplate(parent.name, false);
  1956. }
  1957. if (typeof template === 'string') {
  1958. template = Handlebars.compile(template, {data: true});
  1959. }
  1960. return new Handlebars.SafeString(template(this, options));
  1961. } else {
  1962. return '';
  1963. }
  1964. });
  1965. ;;
  1966. /*global collectionOptionNames, inheritVars */
  1967. var loadStart = 'load:start',
  1968. loadEnd = 'load:end',
  1969. rootObject;
  1970. Thorax.setRootObject = function(obj) {
  1971. rootObject = obj;
  1972. };
  1973. Thorax.loadHandler = function(start, end, context) {
  1974. var loadCounter = _.uniqueId();
  1975. return function(message, background, object) {
  1976. var self = context || this;
  1977. self._loadInfo = self._loadInfo || [];
  1978. var loadInfo = self._loadInfo[loadCounter];
  1979. function startLoadTimeout() {
  1980. // If the timeout has been set already but has not triggered yet do nothing
  1981. // Otherwise set a new timeout (either initial or for going from background to
  1982. // non-background loading)
  1983. if (loadInfo.timeout && !loadInfo.run) {
  1984. return;
  1985. }
  1986. var loadingTimeout = self._loadingTimeoutDuration !== undefined ?
  1987. self._loadingTimeoutDuration : Thorax.View.prototype._loadingTimeoutDuration;
  1988. loadInfo.timeout = setTimeout(function() {
  1989. try {
  1990. loadInfo.run = true;
  1991. start.call(self, loadInfo.message, loadInfo.background, loadInfo);
  1992. } catch (e) {
  1993. Thorax.onException('loadStart', e);
  1994. }
  1995. }, loadingTimeout * 1000);
  1996. }
  1997. if (!loadInfo) {
  1998. loadInfo = self._loadInfo[loadCounter] = _.extend({
  1999. events: [],
  2000. timeout: 0,
  2001. message: message,
  2002. background: !!background
  2003. }, Backbone.Events);
  2004. startLoadTimeout();
  2005. } else {
  2006. clearTimeout(loadInfo.endTimeout);
  2007. loadInfo.message = message;
  2008. if (!background && loadInfo.background) {
  2009. loadInfo.background = false;
  2010. startLoadTimeout();
  2011. }
  2012. }
  2013. // Prevent binds to the same object multiple times as this can cause very bad things
  2014. // to happen for the load;load;end;end execution flow.
  2015. if (loadInfo.events.indexOf(object) >= 0) {
  2016. loadInfo.events.push(object);
  2017. return;
  2018. }
  2019. loadInfo.events.push(object);
  2020. object.on(loadEnd, function endCallback() {
  2021. var loadingEndTimeout = self._loadingTimeoutEndDuration;
  2022. if (loadingEndTimeout === void 0) {
  2023. // If we are running on a non-view object pull the default timeout
  2024. loadingEndTimeout = Thorax.View.prototype._loadingTimeoutEndDuration;
  2025. }
  2026. var events = loadInfo.events,
  2027. index = events.indexOf(object);
  2028. if (index >= 0) {
  2029. events.splice(index, 1);
  2030. if (events.indexOf(object) < 0) {
  2031. // Last callback for this particlar object, remove the bind
  2032. object.off(loadEnd, endCallback);
  2033. }
  2034. }
  2035. if (!events.length) {
  2036. clearTimeout(loadInfo.endTimeout);
  2037. loadInfo.endTimeout = setTimeout(function() {
  2038. try {
  2039. if (!events.length) {
  2040. var run = loadInfo.run;
  2041. if (run) {
  2042. // Emit the end behavior, but only if there is a paired start
  2043. end.call(self, loadInfo.background, loadInfo);
  2044. loadInfo.trigger(loadEnd, loadInfo);
  2045. }
  2046. // If stopping make sure we don't run a start
  2047. clearTimeout(loadInfo.timeout);
  2048. loadInfo = self._loadInfo[loadCounter] = undefined;
  2049. }
  2050. } catch (e) {
  2051. Thorax.onException('loadEnd', e);
  2052. }
  2053. }, loadingEndTimeout * 1000);
  2054. }
  2055. });
  2056. };
  2057. };
  2058. /**
  2059. * Helper method for propagating load:start events to other objects.
  2060. *
  2061. * Forwards load:start events that occur on `source` to `dest`.
  2062. */
  2063. Thorax.forwardLoadEvents = function(source, dest, once) {
  2064. function load(message, backgound, object) {
  2065. if (once) {
  2066. source.off(loadStart, load);
  2067. }
  2068. dest.trigger(loadStart, message, backgound, object);
  2069. }
  2070. source.on(loadStart, load);
  2071. return {
  2072. off: function() {
  2073. source.off(loadStart, load);
  2074. }
  2075. };
  2076. };
  2077. //
  2078. // Data load event generation
  2079. //
  2080. /**
  2081. * Mixing for generating load:start and load:end events.
  2082. */
  2083. Thorax.mixinLoadable = function(target, useParent) {
  2084. _.extend(target, {
  2085. //loading config
  2086. _loadingClassName: 'loading',
  2087. _loadingTimeoutDuration: 0.33,
  2088. _loadingTimeoutEndDuration: 0.10,
  2089. // Propagates loading view parameters to the AJAX layer
  2090. onLoadStart: function(message, background, object) {
  2091. var that = useParent ? this.parent : this;
  2092. // Protect against race conditions
  2093. if (!that || !that.el) {
  2094. return;
  2095. }
  2096. if (!that.nonBlockingLoad && !background && rootObject && rootObject !== this) {
  2097. rootObject.trigger(loadStart, message, background, object);
  2098. }
  2099. $(that.el).addClass(that._loadingClassName);
  2100. //used by loading helpers
  2101. if (that._loadingCallbacks) {
  2102. _.each(that._loadingCallbacks, function(callback) {
  2103. callback();
  2104. });
  2105. }
  2106. },
  2107. onLoadEnd: function(/* background, object */) {
  2108. var that = useParent ? this.parent : this;
  2109. // Protect against race conditions
  2110. if (!that || !that.el) {
  2111. return;
  2112. }
  2113. $(that.el).removeClass(that._loadingClassName);
  2114. //used by loading helpers
  2115. if (that._loadingCallbacks) {
  2116. _.each(that._loadingCallbacks, function(callback) {
  2117. callback();
  2118. });
  2119. }
  2120. }
  2121. });
  2122. };
  2123. Thorax.mixinLoadableEvents = function(target, useParent) {
  2124. _.extend(target, {
  2125. loadStart: function(message, background) {
  2126. var that = useParent ? this.parent : this;
  2127. that.trigger(loadStart, message, background, that);
  2128. },
  2129. loadEnd: function() {
  2130. var that = useParent ? this.parent : this;
  2131. that.trigger(loadEnd, that);
  2132. }
  2133. });
  2134. };
  2135. Thorax.mixinLoadable(Thorax.View.prototype);
  2136. Thorax.mixinLoadableEvents(Thorax.View.prototype);
  2137. if (Thorax.HelperView) {
  2138. Thorax.mixinLoadable(Thorax.HelperView.prototype, true);
  2139. Thorax.mixinLoadableEvents(Thorax.HelperView.prototype, true);
  2140. }
  2141. Thorax.sync = function(method, dataObj, options) {
  2142. var self = this,
  2143. complete = options.complete;
  2144. options.complete = function() {
  2145. self._request = undefined;
  2146. self._aborted = false;
  2147. complete && complete.apply(this, arguments);
  2148. };
  2149. this._request = Backbone.sync.apply(this, arguments);
  2150. return this._request;
  2151. };
  2152. function bindToRoute(callback, failback) {
  2153. var fragment = Backbone.history.getFragment(),
  2154. routeChanged = false;
  2155. function routeHandler() {
  2156. if (fragment === Backbone.history.getFragment()) {
  2157. return;
  2158. }
  2159. routeChanged = true;
  2160. res.cancel();
  2161. failback && failback();
  2162. }
  2163. Backbone.history.on('route', routeHandler);
  2164. function finalizer() {
  2165. Backbone.history.off('route', routeHandler);
  2166. if (!routeChanged) {
  2167. callback.apply(this, arguments);
  2168. }
  2169. }
  2170. var res = _.bind(finalizer, this);
  2171. res.cancel = function() {
  2172. Backbone.history.off('route', routeHandler);
  2173. };
  2174. return res;
  2175. }
  2176. function loadData(callback, failback, options) {
  2177. if (this.isPopulated()) {
  2178. return callback(this);
  2179. }
  2180. if (arguments.length === 2 && typeof failback !== 'function' && _.isObject(failback)) {
  2181. options = failback;
  2182. failback = false;
  2183. }
  2184. var self = this,
  2185. routeChanged = false,
  2186. successCallback = bindToRoute(_.bind(callback, self), function() {
  2187. routeChanged = true;
  2188. if (self._request) {
  2189. self._aborted = true;
  2190. self._request.abort();
  2191. }
  2192. failback && failback.call(self, false);
  2193. });
  2194. this.fetch(_.defaults({
  2195. success: successCallback,
  2196. error: failback && function() {
  2197. if (!routeChanged) {
  2198. failback.apply(self, [true].concat(_.toArray(arguments)));
  2199. }
  2200. },
  2201. complete: function() {
  2202. successCallback.cancel();
  2203. }
  2204. }, options));
  2205. }
  2206. function fetchQueue(options, $super) {
  2207. if (options.resetQueue) {
  2208. // WARN: Should ensure that loaders are protected from out of band data
  2209. // when using this option
  2210. this.fetchQueue = undefined;
  2211. }
  2212. if (!this.fetchQueue) {
  2213. // Kick off the request
  2214. this.fetchQueue = [options];
  2215. options = _.defaults({
  2216. success: flushQueue(this, this.fetchQueue, 'success'),
  2217. error: flushQueue(this, this.fetchQueue, 'error'),
  2218. complete: flushQueue(this, this.fetchQueue, 'complete')
  2219. }, options);
  2220. $super.call(this, options);
  2221. } else {
  2222. // Currently fetching. Queue and process once complete
  2223. this.fetchQueue.push(options);
  2224. }
  2225. }
  2226. function flushQueue(self, fetchQueue, handler) {
  2227. return function() {
  2228. var args = arguments;
  2229. // Flush the queue. Executes any callback handlers that
  2230. // may have been passed in the fetch options.
  2231. _.each(fetchQueue, function(options) {
  2232. if (options[handler]) {
  2233. options[handler].apply(this, args);
  2234. }
  2235. }, this);
  2236. // Reset the queue if we are still the active request
  2237. if (self.fetchQueue === fetchQueue) {
  2238. self.fetchQueue = undefined;
  2239. }
  2240. };
  2241. }
  2242. var klasses = [];
  2243. Thorax.Model && klasses.push(Thorax.Model);
  2244. Thorax.Collection && klasses.push(Thorax.Collection);
  2245. _.each(klasses, function(DataClass) {
  2246. var $fetch = DataClass.prototype.fetch;
  2247. Thorax.mixinLoadableEvents(DataClass.prototype, false);
  2248. _.extend(DataClass.prototype, {
  2249. sync: Thorax.sync,
  2250. fetch: function(options) {
  2251. options = options || {};
  2252. var self = this,
  2253. complete = options.complete;
  2254. options.complete = function() {
  2255. complete && complete.apply(this, arguments);
  2256. self.loadEnd();
  2257. };
  2258. self.loadStart(undefined, options.background);
  2259. return fetchQueue.call(this, options || {}, $fetch);
  2260. },
  2261. load: function(callback, failback, options) {
  2262. if (arguments.length === 2 && typeof failback !== 'function') {
  2263. options = failback;
  2264. failback = false;
  2265. }
  2266. options = options || {};
  2267. if (!options.background && !this.isPopulated() && rootObject) {
  2268. // Make sure that the global scope sees the proper load events here
  2269. // if we are loading in standalone mode
  2270. Thorax.forwardLoadEvents(this, rootObject, true);
  2271. }
  2272. loadData.call(this, callback, failback, options);
  2273. }
  2274. });
  2275. });
  2276. Thorax.Util.bindToRoute = bindToRoute;
  2277. if (Thorax.Router) {
  2278. Thorax.Router.bindToRoute = Thorax.Router.prototype.bindToRoute = bindToRoute;
  2279. }
  2280. // Propagates loading view parameters to the AJAX layer
  2281. Thorax.View.prototype._modifyDataObjectOptions = function(dataObject, options) {
  2282. options.ignoreErrors = this.ignoreFetchError;
  2283. options.background = this.nonBlockingLoad;
  2284. return options;
  2285. };
  2286. Thorax.HelperView.prototype._modifyDataObjectOptions = function(dataObject, options) {
  2287. options.ignoreErrors = this.parent.ignoreFetchError;
  2288. options.background = this.parent.nonBlockingLoad;
  2289. return options;
  2290. };
  2291. inheritVars.collection.loading = function() {
  2292. var loadingView = this.loadingView,
  2293. loadingTemplate = this.loadingTemplate,
  2294. loadingPlacement = this.loadingPlacement;
  2295. //add "loading-view" and "loading-template" options to collection helper
  2296. if (loadingView || loadingTemplate) {
  2297. var callback = Thorax.loadHandler(_.bind(function() {
  2298. var item;
  2299. if (this.collection.length === 0) {
  2300. this.$el.empty();
  2301. }
  2302. if (loadingView) {
  2303. var instance = Thorax.Util.getViewInstance(loadingView);
  2304. this._addChild(instance);
  2305. if (loadingTemplate) {
  2306. instance.render(loadingTemplate);
  2307. } else {
  2308. instance.render();
  2309. }
  2310. item = instance;
  2311. } else {
  2312. item = this.renderTemplate(loadingTemplate);
  2313. }
  2314. var index = loadingPlacement
  2315. ? loadingPlacement.call(this)
  2316. : this.collection.length
  2317. ;
  2318. this.appendItem(item, index);
  2319. this.$el.children().eq(index).attr('data-loading-element', this.collection.cid);
  2320. }, this), _.bind(function() {
  2321. this.$el.find('[data-loading-element="' + this.collection.cid + '"]').remove();
  2322. }, this),
  2323. this.collection);
  2324. this.listenTo(this.collection, 'load:start', callback);
  2325. }
  2326. };
  2327. if (typeof collectionOptionNames !== 'undefined') {
  2328. collectionOptionNames['loading-template'] = 'loadingTemplate';
  2329. collectionOptionNames['loading-view'] = 'loadingView';
  2330. collectionOptionNames['loading-placement'] = 'loadingPlacement';
  2331. }
  2332. Thorax.View.on({
  2333. 'load:start': Thorax.loadHandler(
  2334. function(message, background, object) {
  2335. this.onLoadStart(message, background, object);
  2336. },
  2337. function(background, object) {
  2338. this.onLoadEnd(object);
  2339. }),
  2340. collection: {
  2341. 'load:start': function(message, background, object) {
  2342. this.trigger(loadStart, message, background, object);
  2343. }
  2344. },
  2345. model: {
  2346. 'load:start': function(message, background, object) {
  2347. this.trigger(loadStart, message, background, object);
  2348. }
  2349. }
  2350. });
  2351. ;;
  2352. Handlebars.registerViewHelper('loading', function(view) {
  2353. var _render = view.render;
  2354. view.render = function() {
  2355. if (view.parent.$el.hasClass(view.parent._loadingClassName)) {
  2356. return _render.call(this, view.fn);
  2357. } else {
  2358. return _render.call(this, view.inverse);
  2359. }
  2360. };
  2361. var callback = _.bind(view.render, view);
  2362. view.parent._loadingCallbacks = view.parent._loadingCallbacks || [];
  2363. view.parent._loadingCallbacks.push(callback);
  2364. view.on('destroyed', function() {
  2365. view.parent._loadingCallbacks = _.without(view.parent._loadingCallbacks, callback);
  2366. });
  2367. view.render();
  2368. });
  2369. ;;
  2370. var isIE = (/msie [\w.]+/).exec(navigator.userAgent.toLowerCase());
  2371. // IE will lose a reference to the elements if view.el.innerHTML = '';
  2372. // If they are removed one by one the references are not lost.
  2373. // For instance a view's childrens' `el`s will be lost if the view
  2374. // sets it's `el.innerHTML`.
  2375. if (isIE) {
  2376. Thorax.View.on('before:append', function() {
  2377. if (this._renderCount > 0) {
  2378. _.each(this._elementsByCid, function(element) {
  2379. $(element).remove();
  2380. });
  2381. _.each(this.children, function(child) {
  2382. child.$el.remove();
  2383. });
  2384. }
  2385. });
  2386. }
  2387. ;;
  2388. })();