PageRenderTime 67ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/tbone.js

https://github.com/nvdnkpr/tbone
JavaScript | 1759 lines | 942 code | 131 blank | 686 comment | 209 complexity | 71e475f039d1660f214664e7d34c8681 MD5 | raw file

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

  1. (function () {
  2. /** @const {boolean} */
  3. var TBONE_DEBUG = window['TBONE_DEBUG'];
  4. var tbone = function (arg0, arg1, arg2) {
  5. if (arg0) {
  6. if (typeof arg0 === 'function') {
  7. return autorun(arg0, arg1, arg2);
  8. } else {
  9. return lookup.apply(this, arguments);
  10. }
  11. }
  12. /**
  13. * Does anything make sense to do with no arguments?
  14. */
  15. };
  16. var data = {};
  17. var models = {};
  18. var collections = {};
  19. var templates = {};
  20. var views = {};
  21. /**
  22. * Scheduling priority constants
  23. *
  24. * The scheduler will update views and models in this order:
  25. * 1) synchronous (local) models
  26. * 2) views
  27. * 3) asynchronous (ajax) models
  28. *
  29. * The goals of this ordering are:
  30. * - never render a view based on an outdated model that
  31. * we can update immediately.
  32. * - defer ajax requests until we know that something in the
  33. * UI needs its data.
  34. */
  35. /** @const */
  36. var BASE_PRIORITY_MODEL_SYNC = 3000;
  37. /** @const */
  38. var BASE_PRIORITY_VIEW = 2000;
  39. /** @const */
  40. var BASE_PRIORITY_MODEL_ASYNC = 1000;
  41. /**
  42. * We also use the processQueue to initialize models & views. By adding this delta
  43. * to priorities for initialization, we ensure that initialization happens in the
  44. * same order as execution and that it happens before execution. For example, it
  45. * may be inefficient for a model to reset before a model that it depends on has
  46. * initialized, as dependency chains will not yet be established.
  47. * XXX Does this really matter? Or matter much?
  48. * @const
  49. */
  50. var PRIORITY_INIT_DELTA = 5000;
  51. function identity(x) { return x; }
  52. /** @const */
  53. var noop = identity;
  54. function isfunction (x) {
  55. return typeof x === 'function';
  56. }
  57. function isString(x) {
  58. return typeof x === 'string';
  59. }
  60. /**
  61. * Returns a function that returns the elapsed time.
  62. * @return {function(): Number} Function that returns elapsed time.
  63. */
  64. function timer() {
  65. var start = new Date().getTime();
  66. /**
  67. * Function that returns elapsed time since the outer function was invoked.
  68. * @return {Number} Elapsed time in ms
  69. */
  70. return function () {
  71. return new Date().getTime() - start;
  72. };
  73. }
  74. function warn() {
  75. if (TBONE_DEBUG) {
  76. console.warn.apply(console, arguments);
  77. }
  78. }
  79. function error() {
  80. if (TBONE_DEBUG) {
  81. console.error.apply(console, arguments);
  82. }
  83. }
  84. /** @const */
  85. var ERROR = 1;
  86. /** @const */
  87. var WARN = 2;
  88. /** @const */
  89. var INFO = 3;
  90. /** @const */
  91. var VERBOSE = 4;
  92. var logLevels = {
  93. type: {
  94. },
  95. context: {
  96. },
  97. event: {
  98. },
  99. base: WARN
  100. };
  101. if (TBONE_DEBUG) {
  102. tbone['watchLog'] = function (name, level) {
  103. if (level == null) { level = VERBOSE; }
  104. logLevels.type[name] = VERBOSE;
  105. logLevels.context[name] = VERBOSE;
  106. logLevels.event[name] = VERBOSE;
  107. };
  108. }
  109. var events = [];
  110. var viewRenders = 0;
  111. /**
  112. * Dynamic counter of how many ajax requests are inflight.
  113. * @type {Number}
  114. */
  115. var inflight = 0;
  116. tbone['isReady'] = function () {
  117. return !inflight && !schedulerQueue.length;
  118. };
  119. var logCallbacks = [];
  120. function log () {
  121. if (TBONE_DEBUG) {
  122. for (var i = 0; i < logCallbacks.length; i++) {
  123. logCallbacks[i].apply(this, arguments);
  124. }
  125. }
  126. }
  127. /**
  128. * Log an event. The event is piped to the JS console if the level is less than or equal to the
  129. * matched maximum log level based on the logLevels configuration above.
  130. * @param {Number} level Log level: 1=error, 2=warn, 3=info, 4=verbose
  131. * @param {string|Backbone.Model|Backbone.View|Scope} context What is logging this event
  132. * @param {string} event Short event type string
  133. * @param {string|Object} msg Message string with tokens that will be
  134. * rendered from data. Or just relevant data.
  135. * @param {Object=} data Relevant data
  136. */
  137. function logconsole (level, context, event, msg, data) {
  138. var name = isString(context) ? context : context.name;
  139. var type = (isString(context) ? context :
  140. context.isModel ? 'model' :
  141. context.isView ? 'view' :
  142. context.isScope ? 'scope' : '??');
  143. var threshold = Math.max(logLevels.context[name] || 0,
  144. logLevels.event[event] || 0,
  145. logLevels.type[type] || 0) || logLevels.base;
  146. if (event === 'lookups') {
  147. msg = _.reduce(msg, function(memo, map, id) {
  148. memo[map.__path__] = map;
  149. return memo;
  150. }, {});
  151. }
  152. if (level <= threshold) {
  153. /**
  154. * If a msg is a string, render it as a template with data as the data.
  155. * If msg is not a string, just output the data below.
  156. */
  157. var templated = isString(msg) ? _.template(msg, data || {}) : '';
  158. var includeColon = !!templated || !!msg;
  159. var frame = type === name ? type : (type + ' ' + name);
  160. var message = frame + ' / ' + event + (includeColon ? ': ' : '');
  161. var logfn = console[(level === ERROR ? 'error' : level === WARN ? 'warn' : 'log')];
  162. logfn.call(console, message, templated || msg || '');
  163. }
  164. }
  165. function onLog (cb) {
  166. logCallbacks.push(cb);
  167. }
  168. if (TBONE_DEBUG) {
  169. tbone['onLog'] = onLog;
  170. onLog(logconsole);
  171. }
  172. /**
  173. * Returns the list of unique listeners attached to the specified model/view.
  174. * @param {Backbone.Model|Backbone.View} self
  175. * @return {Array.<Backbone.Model|Backbone.View|Scope>} array of listeners
  176. */
  177. function getListeners(self) {
  178. var listeners = [];
  179. _.each(_.values(self['_callbacks'] || {}), function (ll) {
  180. var curr = ll.next;
  181. while (true) {
  182. if (curr.context) {
  183. listeners.push(curr.context);
  184. curr = curr.next;
  185. } else {
  186. break;
  187. }
  188. }
  189. });
  190. return _.uniq(listeners);
  191. }
  192. //
  193. /**
  194. * Returns true if there is a view that is listening (directly or indirectly)
  195. * to this model. Useful for determining whether the current model should
  196. * be updated (if a model is updated in the forest and nobody is there to
  197. * hear it, then why update it in the first place?)
  198. * @param {Backbone.Model|Backbone.View} self
  199. * @return {Boolean}
  200. */
  201. function hasViewListener(self) {
  202. var todo = [];
  203. var usedModels = {};
  204. todo.push(self);
  205. usedModels[self.name] = true;
  206. while (todo.length) {
  207. var next = todo.pop();
  208. var listeners = getListeners(next);
  209. for (var i = 0; i < listeners.length; i++) {
  210. var listener = listeners[i];
  211. if (listener.isScope) {
  212. // The listener context is the model or view to whom the scope belongs.
  213. // Here, we care about that model/view, not the scope, because that's
  214. // what everyone else might be listening to.
  215. listener = listener.context;
  216. }
  217. // listener might be undefined right now if the scope above didn't have a context.
  218. if (listener) {
  219. if (listener.isView) {
  220. // We found a view that depends on the original model!
  221. return true;
  222. }
  223. // listener could also have been a scope with a context that was neither
  224. // a model nor a view.
  225. if (listener.isModel) {
  226. var name = listener['name'];
  227. if (name && !usedModels[listener.name]) {
  228. todo.push(listener);
  229. usedModels[name] = true;
  230. }
  231. }
  232. }
  233. }
  234. }
  235. return false;
  236. }
  237. /**
  238. * currentParentScope globally tracks the current executing scope, so that subscopes
  239. * created during its execution (i.e. by tbone.autorun) can register themselves as
  240. * subscopes of the parent (this is important for recursive destruction of scopes).
  241. */
  242. var currentParentScope;
  243. /**
  244. * An autobinding function execution scope. See autorun for details.
  245. * @constructor
  246. */
  247. function Scope(fn, context, priority, name, onExecuteCb, onExecuteContext) {
  248. _.extend(this, {
  249. fn: fn,
  250. context: context,
  251. priority: priority,
  252. name: name,
  253. onExecuteCb: onExecuteCb,
  254. onExecuteContext: onExecuteContext,
  255. subScopes: []
  256. });
  257. }
  258. _.extend(Scope.prototype,
  259. /** @lends {Scope.prototype} */ {
  260. /**
  261. * Used to identify that an object is a Scope
  262. * @type {Boolean}
  263. */
  264. isScope: true,
  265. /**
  266. * Queue function execution in the scheduler
  267. */
  268. trigger: function () {
  269. queueExec(this);
  270. },
  271. /**
  272. * Execute the wrapped function, tracking all values referenced through lookup(),
  273. * and binding to those data sources such that the function is re-executed whenever
  274. * those values change. Each execution re-tracks and re-binds all data sources; the
  275. * actual sources bound on each execution may differ depending on what is looked up.
  276. */
  277. execute: function () {
  278. var self = this;
  279. if (!self.destroyed) {
  280. self.unbindAll();
  281. self.destroySubScopes();
  282. // Save our parent's lookups and subscopes. It's like pushing our own values
  283. // onto the top of each stack.
  284. var oldLookups = recentLookups;
  285. this.lookups = recentLookups = {};
  286. var oldParentScope = currentParentScope;
  287. currentParentScope = self;
  288. // ** Call the payload function **
  289. // This function must be synchronous. Anything that is looked up using
  290. // tbone.lookup before this function returns (that is not inside a subscope)
  291. // will get bound below.
  292. self.fn.call(self.context);
  293. _.each(recentLookups, function (propMap) {
  294. var obj = propMap['__obj__'];
  295. if (obj.isCollection) {
  296. /**
  297. * This is not as efficient as it could be.
  298. */
  299. obj.on('add remove reset', self.trigger, self);
  300. } else {
  301. if (propMap['*']) {
  302. obj.on('change', self.trigger, self);
  303. } else {
  304. for (var prop in propMap) {
  305. if (prop !== '__obj__' && prop !== '__path__') {
  306. obj.on('change:' + prop, self.trigger, self);
  307. }
  308. }
  309. }
  310. }
  311. });
  312. // This is intended primarily for diagnostics. onExecute may either be a
  313. // function, or an array with a function and a context to use for the
  314. // function call. In either case, this Scope is passed as the only argument.
  315. if (self.onExecuteCb) {
  316. self.onExecuteCb.call(self.onExecuteContext, this);
  317. }
  318. // Pop our own lookups and parent scope off the stack, restoring them to
  319. // the values we saved above.
  320. recentLookups = oldLookups;
  321. currentParentScope = oldParentScope;
  322. }
  323. },
  324. /**
  325. * For each model which we've bound, tell it to unbind all events where this
  326. * scope is the context of the binding.
  327. */
  328. unbindAll: function () {
  329. var self = this;
  330. _.each(this.lookups || {}, function (propMap) {
  331. propMap['__obj__'].off(null, null, self);
  332. });
  333. },
  334. /**
  335. * Destroy any execution scopes that were creation during execution of this function.
  336. */
  337. destroySubScopes: function () {
  338. _.each(this.subScopes, function (subScope) {
  339. subScope.destroy();
  340. });
  341. this.subScopes = [];
  342. },
  343. /**
  344. * Destroy this scope. Which means to unbind everything, destroy scopes recursively,
  345. * and ignore any execute calls which may already be queued in the scheduler.
  346. */
  347. destroy: function () {
  348. this.destroyed = true;
  349. this.unbindAll();
  350. this.destroySubScopes();
  351. }
  352. });
  353. /**
  354. * tbone.autorun
  355. *
  356. * Wrap a function call with automatic binding for any model properties accessed
  357. * during the function's execution.
  358. *
  359. * Models and views update automatically by wrapping their reset functions with this.
  360. *
  361. * Additionally, this can be used within postRender callbacks to section off a smaller
  362. * block of code to repeat when its own referenced properties are updated, without
  363. * needing to re-render the entire view.
  364. * @param {Function} fn Function to invoke
  365. * @param {Backbone.Model|Backbone.View} context Context to pass on invocation
  366. * @param {number} priority Scheduling priority - higher = sooner
  367. * @param {string} name Name for debugging purposes
  368. * @return {Scope} A new Scope created to wrap this function
  369. */
  370. function autorun(fn, context, priority, name, onExecuteCb, onExecuteContext, detached) {
  371. // Default priority and name if not specified. Priority is important in
  372. // preventing unnecessary refreshes of views/subscopes that may be slated
  373. // for destruction by a parent; the parent should have priority so as
  374. // to execute first.
  375. if (!priority) {
  376. priority = currentParentScope ? currentParentScope.priority - 1 : 0;
  377. }
  378. if (!name) {
  379. name = currentParentScope ? currentParentScope.name + '+' : 'unnamed';
  380. }
  381. // Create a new scope for this function
  382. var scope = new Scope(fn, context, priority, name, onExecuteCb, onExecuteContext);
  383. // If this is a subscope, add it to its parent's list of subscopes.
  384. if (!detached && currentParentScope) {
  385. currentParentScope.subScopes.push(scope);
  386. }
  387. // Run the associated function (and bind associated models)
  388. scope.execute();
  389. // Return the scope object; this is used by BaseView to destroy
  390. // scopes when the associated view is destroyed.
  391. return scope;
  392. }
  393. /**
  394. * Generate and return a unique identifier which we attach to an object.
  395. * The object is typically a view, model, or scope, and is used to compare
  396. * object references for equality using a hash Object for efficiency.
  397. * @param {Object} obj Object to get id from ()
  398. * @return {string} Unique ID assigned to this object
  399. */
  400. function uniqueId(obj) {
  401. return obj['tboneid'] = obj['tboneid'] || nextId++;
  402. }
  403. var nextId = 1;
  404. /**
  405. * List of Scopes to be executed immediately.
  406. * @type {Array.<Scope>}
  407. */
  408. var schedulerQueue = [];
  409. /**
  410. * Flag indicating that the schedulerQueue is unsorted.
  411. * @type {Boolean}
  412. */
  413. var dirty;
  414. /**
  415. * Hash map of all the current Scope uniqueIds that are already
  416. * scheduled for immediate execution.
  417. * @type {Object.<string, Boolean>}
  418. */
  419. var scopesQueued = {};
  420. /**
  421. * Pop the highest priority Scope from the schedulerQueue.
  422. * @return {Scope} Scope to be executed next
  423. */
  424. function pop() {
  425. /**
  426. * The schedulerQueue is lazily sorted using the built-in Array.prototype.sort.
  427. * This is not as theoretically-efficient as standard priority queue algorithms,
  428. * but Array.prototype.sort is fast enough that this should work well enough for
  429. * everyone, hopefully.
  430. */
  431. if (dirty) {
  432. schedulerQueue.sort(function (a, b) {
  433. /**
  434. * TODO for sync models, use dependency graph in addition to priority
  435. * to order execution in such a way as to avoid immediate re-execution.
  436. */
  437. return a.priority - b.priority;
  438. });
  439. dirty = false;
  440. }
  441. return schedulerQueue.pop();
  442. }
  443. /**
  444. * Flag indicating whether a processQueue timer has already been set.
  445. */
  446. var processQueueTimer;
  447. /**
  448. * Queue the specified Scope for execution if it is not already queued.
  449. * @param {Scope} scope
  450. */
  451. function queueExec (scope) {
  452. var contextId = uniqueId(scope);
  453. if (!scopesQueued[contextId]) {
  454. scopesQueued[contextId] = true;
  455. /**
  456. * Push the scope onto the queue of scopes to be executed immediately.
  457. */
  458. schedulerQueue.push(scope);
  459. /**
  460. * Mark the queue as dirty; the priority of the scope we just added
  461. * is not immediately reflected in the queue order.
  462. */
  463. dirty = true;
  464. /**
  465. * If a timer to process the queue is not already set, set one.
  466. */
  467. if (!processQueueTimer && unfrozen) {
  468. processQueueTimer = _.defer(processQueue);
  469. }
  470. }
  471. }
  472. var unfrozen = true;
  473. /**
  474. * Drain the Scope execution queue, in priority order.
  475. */
  476. function processQueue () {
  477. processQueueTimer = null;
  478. var queueProcessTime = timer();
  479. var scope;
  480. while (unfrozen && !!(scope = pop())) {
  481. /**
  482. * Update the scopesQueued map so that this Scope may be requeued.
  483. */
  484. delete scopesQueued[uniqueId(scope)];
  485. var scopeExecTime = timer();
  486. /**
  487. * Execute the scope, and in turn, the wrapped function.
  488. */
  489. scope.execute();
  490. log(VERBOSE, 'scheduler', 'exec', '<%=priority%> <%=duration%>ms <%=name%>', {
  491. 'priority': scope.priority,
  492. 'name': scope.name,
  493. 'duration': scopeExecTime()
  494. });
  495. }
  496. log(VERBOSE, 'scheduler', 'processQueue', 'ran for <%=duration%>ms', {
  497. 'duration': queueProcessTime()
  498. });
  499. log(VERBOSE, 'scheduler', 'viewRenders', 'rendered <%=viewRenders%> total', {
  500. 'viewRenders': viewRenders
  501. });
  502. }
  503. /**
  504. * Drain to the tbone processQueue, processing all scope executes immediately.
  505. * This is useful both for testing and MAYBE also for optimizing responsiveness by
  506. * draining at the end of a keyboard / mouse event handler.
  507. */
  508. tbone['drain'] = function () {
  509. if (processQueueTimer) {
  510. clearTimeout(processQueueTimer);
  511. }
  512. processQueue();
  513. };
  514. tbone['freeze'] = function () {
  515. unfrozen = false;
  516. };
  517. /**
  518. * baseModel
  519. * @constructor
  520. * @extends Backbone.Model
  521. */
  522. var baseModel = Backbone.Model.extend({
  523. isModel: true,
  524. backboneBasePrototype: Backbone.Model.prototype,
  525. /**
  526. * Constructor function to initialize each new model instance.
  527. * @return {[type]}
  528. */
  529. initialize: function () {
  530. var self = this;
  531. uniqueId(self);
  532. var isAsync = self.sleeping = self.isAsync();
  533. var priority = isAsync ? BASE_PRIORITY_MODEL_ASYNC : BASE_PRIORITY_MODEL_SYNC;
  534. /**
  535. * Queue the autorun of update. We want this to happen after the current JS module
  536. * is loaded but before anything else gets updated. We can't do that with setTimeout
  537. * or _.defer because that could possibly fire after processQueue.
  538. */
  539. queueExec({
  540. execute: function () {
  541. self.scope = autorun(self.update, self, priority, 'model_' + self.name,
  542. self.onScopeExecute, self);
  543. },
  544. priority: priority + PRIORITY_INIT_DELTA
  545. });
  546. },
  547. /**
  548. * Indicates whether this function should use the asynchronous or
  549. * synchronous logic.
  550. * @return {Boolean}
  551. */
  552. isAsync: function () {
  553. return !!this['_url'];
  554. },
  555. onScopeExecute: function (scope) {
  556. log(INFO, this, 'lookups', scope.lookups);
  557. },
  558. /**
  559. * Triggers scope re-execution.
  560. */
  561. reset: function () {
  562. if (this.scope) {
  563. this.scope.trigger();
  564. }
  565. },
  566. 'isVisible': function () {
  567. return hasViewListener(this);
  568. },
  569. update: function () {
  570. var self = this;
  571. if (self.isAsync()) {
  572. self.updateAsync();
  573. } else {
  574. self.updateSync();
  575. }
  576. },
  577. updateAsync: function () {
  578. var self = this;
  579. var expirationSeconds = self['expirationSeconds'];
  580. function complete() {
  581. inflight--;
  582. delete self.__xhr;
  583. if (expirationSeconds) {
  584. if (self.expirationTimeout) {
  585. clearTimeout(self.expirationTimeout);
  586. }
  587. self.expirationTimeout = setTimeout(function () {
  588. self.reset();
  589. }, expirationSeconds * 1000);
  590. }
  591. }
  592. var url = self.url();
  593. var lastFetchedUrl = self.fetchedUrl;
  594. self.sleeping = !this['isVisible']();
  595. if (self.sleeping) {
  596. /**
  597. * Regardless of whether url is non-null, this model goes to sleep
  598. * if there's no view listener waiting for data (directly or through
  599. * a chain of other models) from this model.
  600. **/
  601. log(INFO, self, 'sleep');
  602. self.sleeping = true;
  603. } else if (url != null && (expirationSeconds || url !== lastFetchedUrl)) {
  604. /**
  605. * If a defined URL function returns null, it will prevent fetching.
  606. * This can be used e.g. to prevent loading until all required
  607. * parameters are set.
  608. **/
  609. self.fetchedUrl = url;
  610. self.clear();
  611. inflight++;
  612. self.fetch({
  613. 'dataType': 'text',
  614. success: function () {
  615. self['postFetch']();
  616. self.trigger('fetch');
  617. log(INFO, self, 'updated', self.toJSON());
  618. complete();
  619. },
  620. error: function () {
  621. complete();
  622. },
  623. 'beforeSend': function (xhr) {
  624. // If we have an active XHR in flight, we should abort
  625. // it because we don't want that anymore.
  626. if (self.__xhr) {
  627. log(WARN, self, 'abort',
  628. 'aborting obsolete ajax request. old: <%=oldurl%>, new: <%=newurl%>', {
  629. 'oldurl': lastFetchedUrl,
  630. 'newurl': url
  631. });
  632. self.__xhr.abort();
  633. }
  634. self.__xhr = xhr;
  635. xhr['__backbone__'] = true;
  636. },
  637. url: url
  638. });
  639. }
  640. },
  641. updateSync: function () {
  642. var self = this;
  643. // this.state returns the new state, synchronously
  644. var newParams = self['state']();
  645. if (newParams === null) {
  646. log(VERBOSE, self, 'update cancelled');
  647. return;
  648. }
  649. lookup.call(self, '__self__', newParams);
  650. log(INFO, self, 'updated', self.toJSON());
  651. },
  652. 'state': identity,
  653. 'postFetch': noop
  654. });
  655. /**
  656. * Create a new model type.
  657. * @param {string} name Model name
  658. * @param {Backbone.Model|Function=} base Parent model -- or state function of simple sync model
  659. * @param {Object.<string, Object>=} opts Properties to override (optional)
  660. * @return {Backbone.Model}
  661. */
  662. function createModel(name, base, opts) {
  663. if (TBONE_DEBUG && !isString(name)) {
  664. throw 'createModel requires name parameter';
  665. }
  666. /**
  667. * If only a name is provided, this is a passive model. Disable autorun so that this model
  668. * will only be updated by set() calls. This is useful in building simple dynamic data
  669. * sources for other models.
  670. */
  671. if (!base) {
  672. opts = {
  673. initialize: noop
  674. };
  675. base = baseModel;
  676. /**
  677. * If the second parameter is a function, use it as the state function of a simple sync model.
  678. */
  679. } else if (!base['__super__']) {
  680. opts = {
  681. 'state': base
  682. };
  683. base = baseModel;
  684. }
  685. opts = _.extend({
  686. name: name
  687. }, opts || {});
  688. var model = models[name] = base.extend(opts);
  689. var modelPrototype = model.prototype;
  690. _.extend(model, /** @lends {model} */ {
  691. /**
  692. * Create and return an instance of this model using the model name as the instance name.
  693. * @return {Backbone.Model}
  694. */
  695. 'singleton': function () {
  696. return this['make'](name);
  697. },
  698. /**
  699. * Create and return an instance of this model at tbone.data[instanceName].
  700. * @return {Backbone.Model}
  701. */
  702. 'make': function (instanceName) {
  703. var instance = new model();
  704. if (instanceName) {
  705. var nameParts = instanceName.split('.');
  706. var _data = data;
  707. _.each(nameParts.slice(0, nameParts.length - 1), function (part) {
  708. _data = _data[part] = _data[part] || {};
  709. });
  710. _data[nameParts[nameParts.length - 1]] = instance;
  711. }
  712. return instance;
  713. }
  714. });
  715. return model;
  716. }
  717. var baseCollection = Backbone.Collection.extend({
  718. isCollection: true,
  719. backboneBasePrototype: Backbone.Collection.prototype
  720. });
  721. function createCollection(name, model) {
  722. if (TBONE_DEBUG && !isString(name)) {
  723. throw 'createCollection requires name parameter';
  724. }
  725. var opts = {
  726. name: name,
  727. model: model || baseModel
  728. };
  729. var collection = collections[name] = baseCollection.extend(opts);
  730. // XXX this is basically the same as in createModel. Unify.
  731. var collectionPrototype = collection.prototype;
  732. _.extend(collection, /** @lends {collection} */ {
  733. 'singleton': function () {
  734. return this['make'](name);
  735. },
  736. 'make': function (instanceName) {
  737. var instance = new collection();
  738. if (instanceName) {
  739. var nameParts = instanceName.split('.');
  740. var _data = data;
  741. _.each(nameParts.slice(0, nameParts.length - 1), function (part) {
  742. _data = _data[part] = _data[part] || {};
  743. });
  744. _data[nameParts[nameParts.length - 1]] = instance;
  745. }
  746. return instance;
  747. }
  748. });
  749. return collection;
  750. }
  751. var global = window;
  752. var recentLookups;
  753. /**
  754. * "Don't Get Data" - Special flag for lookup to return the model/collection instead
  755. * of calling toJSON() on it.
  756. * @const
  757. */
  758. var DONT_GET_DATA = 1;
  759. /**
  760. * "Iterate Over Models" - Special flag for lookup to return an iterator over the
  761. * models of the collection, enabling iteration over models, which is what we want
  762. * to do when using _.each(collection ...) in a template, as this allows us to
  763. * use model.lookup(...) and properly bind references to the models.
  764. * @const
  765. */
  766. var ITERATE_OVER_MODELS = 2;
  767. /**
  768. * "Extend on set" - instead of replacing an entire object or model's values on
  769. * set, extend that object/model instead.
  770. * @const
  771. */
  772. var EXTEND_ON_SET = 3;
  773. function lookup(flag, query, value) {
  774. var isSet;
  775. var dontGetData = flag === DONT_GET_DATA;
  776. var iterateOverModels = flag === ITERATE_OVER_MODELS;
  777. var extendOnSet = flag === EXTEND_ON_SET;
  778. if (!dontGetData && !iterateOverModels && !extendOnSet) {
  779. /**
  780. * If no flag provided, shift the query and value over. We do it this way instead
  781. * of having flag last so that we can type-check flag and discern optional flags
  782. * from optional values. And flag should only be used internally, anyway.
  783. */
  784. value = query;
  785. query = flag;
  786. flag = null;
  787. /**
  788. * Use arguments.length to switch to set mode in order to properly support
  789. * setting undefined.
  790. */
  791. if (arguments.length === 2) {
  792. isSet = true;
  793. }
  794. } else if (extendOnSet) {
  795. isSet = true;
  796. }
  797. var args = query.split('.');
  798. var setprop;
  799. if (isSet) {
  800. /**
  801. * For set operations, we only want to look up the parent of the property we
  802. * are modifying; pop the final property we're setting from args and save it
  803. * for later.
  804. * @type {string}
  805. */
  806. setprop = args.pop();
  807. }
  808. /**
  809. * If this function was called with a bindable context (i.e. a Model or Collection),
  810. * then use that as the root data object instead of the global tbone.data.
  811. */
  812. var _data = (!this || !this['isBindable']) ? data : this;
  813. var name_parts = [];
  814. var myRecentLookup = {};
  815. var propAfterRecentLookup;
  816. var id;
  817. var arg;
  818. var foundBindable;
  819. if (_data['isBindable']) {
  820. id = uniqueId(_data);
  821. foundBindable = true;
  822. myRecentLookup = (recentLookups && recentLookups[id]) || {
  823. '__obj__': _data
  824. };
  825. if (recentLookups) {
  826. recentLookups[id] = myRecentLookup;
  827. }
  828. }
  829. while ((arg = args.shift()) != null && arg !== '__self__') {
  830. name_parts.push(arg);
  831. if (_data['isBindable']) {
  832. foundBindable = true;
  833. if (_data.isModel) {
  834. _data = _data.get(arg);
  835. } else if (_data.isCollection) {
  836. // XXX should we support .get() for collections? e.g. IDs starting with #?
  837. myRecentLookup[arg] = _data = _data.at(arg);
  838. }
  839. if (!propAfterRecentLookup) {
  840. propAfterRecentLookup = arg;
  841. myRecentLookup[arg] = _data;
  842. }
  843. } else {
  844. _data = _data[arg];
  845. }
  846. if (_data == null) {
  847. /**
  848. * This is not right to do in the case of a deep set where the structure
  849. * is not created yet. We might want to implicitly do a mkdir -p to support
  850. * this, e.g. T('some.deep.random.property.to.set', value)
  851. * -> { some: { deep: { random: { property: { to: { set: value } } } } } }
  852. */
  853. break;
  854. } else if (_data['isBindable']) {
  855. foundBindable = true;
  856. id = uniqueId(_data);
  857. myRecentLookup = (recentLookups && recentLookups[id]) || {
  858. '__obj__': _data,
  859. '__path__': name_parts.join('.') // XXX a model could exist at two paths]
  860. };
  861. if (recentLookups) {
  862. recentLookups[id] = myRecentLookup;
  863. }
  864. propAfterRecentLookup = null;
  865. }
  866. }
  867. /**
  868. * If we haven't found a model / collection in the process of looking something up,
  869. * log an error. A common mistake could be to attempt to read values before models
  870. * are initialized.
  871. **/
  872. if (TBONE_DEBUG && !isSet && !foundBindable) {
  873. log(ERROR, 'lookup', 'no bindable found',
  874. 'No model/collection found while looking up "<%=query%>".', {
  875. query: query
  876. });
  877. }
  878. if (_data) {
  879. if (isSet) {
  880. var currProp = (
  881. query === '__self__' ? _data : // only useful if _data is a model
  882. _data.isModel ? _data.get(setprop) :
  883. _data.isCollection ? _data.at(setprop) :
  884. _data[setprop]);
  885. if (currProp && currProp.isModel) {
  886. /**
  887. * When setting to an entire model, we use different semantics; we want the
  888. * values provided to be set to the model, not replace the model.
  889. */
  890. if (value) {
  891. /**
  892. * Unless extendOnSet is set, remove any properties from the model that
  893. * are not present in the value we're setting it to. Extend-semantics
  894. * are made available to the user via tbone.extend.
  895. */
  896. if (!extendOnSet) {
  897. for (var k in currProp.toJSON()) {
  898. if (value[k] === undefined) {
  899. currProp.unset(k);
  900. }
  901. }
  902. }
  903. currProp.set(value);
  904. } else {
  905. currProp.clear();
  906. }
  907. } else if (currProp !== value) {
  908. if (_data.isModel) {
  909. /**
  910. * Set the value to the top-level model property. Common case.
  911. */
  912. _data.set(setprop, value);
  913. } else if (_data.isCollection) {
  914. // XXX What makes sense to do here?
  915. } else if (_data[setprop] !== value) {
  916. /**
  917. * Set the value to a property on a regular JS object.
  918. */
  919. _data[setprop] = value;
  920. /**
  921. * If we're setting a nested property of a model (or collection?), then
  922. * trigger a change event for the top-level property.
  923. */
  924. if (propAfterRecentLookup) {
  925. myRecentLookup['__obj__'].trigger('change:' + propAfterRecentLookup);
  926. }
  927. }
  928. }
  929. return undefined;
  930. } else if (iterateOverModels && _data.isCollection) {
  931. /**
  932. * If iterateOverModels is set and _data is a collection, return a list of models
  933. * instead of either the collection or a list of model data. This is useful in
  934. * iterating over models while still being able to bind to models individually.
  935. */
  936. myRecentLookup['*'] = _data = _data.models;
  937. } else if (!dontGetData && _data['isBindable']) {
  938. /**
  939. * Unless dontGetData is specified, convert the model/collection to its data.
  940. * This is often what you want to do when getting data from a model, and this
  941. * is what is presented to the user via tbone/lookup.
  942. */
  943. myRecentLookup['*'] = _data = _data.toJSON();
  944. }
  945. }
  946. return _data;
  947. }
  948. function lookupText() {
  949. var value = lookup.apply(this, arguments);
  950. return value != null ? value : '';
  951. }
  952. function toggle(model_and_key) {
  953. lookup(model_and_key, !lookup(model_and_key));
  954. }
  955. function extend(prop, value) {
  956. return lookup.call(this, EXTEND_ON_SET, prop, value);
  957. }
  958. /**
  959. * Convenience function to generate a RegExp from a string. Spaces in the original string
  960. * are re-interpreted to mean a sequence of zero or more whitespace characters.
  961. * @param {String} str
  962. * @param {String} flags
  963. * @return {RegExp}
  964. */
  965. function regexp(str, flags) {
  966. return new RegExp(str.replace(/ /g, '[\\s\\n]*'), flags);
  967. }
  968. /**
  969. * Capture the contents of any/all underscore template blocks.
  970. * @type {RegExp}
  971. * @const
  972. */
  973. var rgxLookup = /<%(=|-|)([\s\S]+?)%>/g;
  974. /**
  975. * Find function declaractions (so that we can detect variables added to the closure scope
  976. * inside a template, as well as start and end of scope).
  977. * @type {RegExp}
  978. * @const
  979. */
  980. var rgxScope = regexp(
  981. 'function \\( ([\\w$_]* (, [\\w$_]+)*) \\)|' +
  982. '(\\{)|' +
  983. '(\\})|' +
  984. '([\\s\\S])', 'g');
  985. /**
  986. * Match function parameters found in the first line of rgxScope.
  987. * @type {RegExp}
  988. * @const
  989. */
  990. var rgxArgs = /[\w$_]+/g;
  991. /**
  992. * When used with string.replace, rgxUnquoted matches unquoted segments with the first group
  993. * and quoted segments with the second group.
  994. * @type {RegExp}
  995. * @const
  996. */
  997. var rgxUnquoted = /([^'"]+)('[^']+'|"[^"]+")?/g;
  998. /**
  999. * Find references that are not subproperty references of something else, e.g. ").hello"
  1000. * @type {RegExp}
  1001. * @const
  1002. */
  1003. var rgxLookupableRef = regexp('(\\. )?(([\\w$_]+)(\\.[\\w$_]+)*)', 'g');
  1004. /**
  1005. * Use to test whether a string is in fact a number literal. We don't want to instrument those.
  1006. * @type {RegExp}
  1007. * @const
  1008. */
  1009. var rgxNumber = /^\d+$/;
  1010. var neverLookup = {};
  1011. _.each(('break case catch continue debugger default delete do else finally for function if in ' +
  1012. 'instanceof new return switch this throw try typeof var void while with ' +
  1013. 'Array Boolean Date Function Iterator Number Object RegExp String ' +
  1014. 'isFinite isNaN parseFloat parseInt Infinity JSON Math NaN undefined true false null ' +
  1015. '$ _ tbone T'
  1016. ).split(' '), function (word) {
  1017. neverLookup[word] = true;
  1018. });
  1019. tbone['dontPatch'] = function (namespace) {
  1020. neverLookup[namespace] = true;
  1021. };
  1022. /**
  1023. * Adds listeners for model value lookups to a template string
  1024. * This allows us to automatically and dynamically bind to change events on the models
  1025. * to auto-refresh this template.
  1026. */
  1027. function withLookupListeners(str, textOp, closureVariables) {
  1028. return str.replace(rgxLookupableRef, function (all, precedingDot, expr, firstArg) {
  1029. if (neverLookup[firstArg] || precedingDot || rgxNumber.test(firstArg)) {
  1030. return all;
  1031. } else {
  1032. if (closureVariables[firstArg] != null) {
  1033. /**
  1034. * If the first part of the expression is a closure-bound variable
  1035. * e.g. from a _.each iterator, try to do a lookup on that (if it's
  1036. * a model). Otherwise, just do a native reference.
  1037. */
  1038. return [
  1039. '(',
  1040. firstArg,
  1041. ' && ',
  1042. firstArg,
  1043. '.isBindable ? ',
  1044. firstArg,
  1045. '.lookup',
  1046. textOp ? 'Text' : '',
  1047. '("',
  1048. expr.slice(firstArg.length + 1),
  1049. '")',
  1050. ' : ',
  1051. expr,
  1052. ')'
  1053. ].join('');
  1054. } else {
  1055. /**
  1056. * Patch the reference to use lookup (or lookupText).
  1057. */
  1058. return [
  1059. 'root.lookup',
  1060. textOp ? 'Text' : '',
  1061. '(' + ITERATE_OVER_MODELS + ', "',
  1062. expr,
  1063. '")'
  1064. ].join('');
  1065. }
  1066. }
  1067. });
  1068. }
  1069. /**
  1070. * Add a template to be used later via render.
  1071. * @param {string} name template name; should match tbone attribute references
  1072. * @param {string} string template as HTML string
  1073. */
  1074. function addTemplate(name, string) {
  1075. templates[name] = string;
  1076. }
  1077. /**
  1078. * Instrument the template for automatic reference binding via tbone.lookup/lookupText.
  1079. * @param {string} string Uninstrumented template as an HTML string
  1080. * @return {function(Object): string}
  1081. */
  1082. function initTemplate(string) {
  1083. /**
  1084. * As we parse through the template, we identify variables defined as function parameters
  1085. * within the current closure scope; if a variable is defined, we instrument references to
  1086. * that variable so that they use that variable as the lookup root, instead of using the
  1087. * root context. We push each new closure scope's variables onto varstack and pop them
  1088. * off when we reach the end of the closure.
  1089. * @type {Array.<Array.<string>>}
  1090. */
  1091. var varstack = [[]];
  1092. /**
  1093. * Hash set of variables that are currently in scope.
  1094. * @type {Object.<string, boolean>}
  1095. */
  1096. var inClosure = {};
  1097. function updateInClosure() {
  1098. /**
  1099. * Rebuild the hash set of variables that are "in closure scope"
  1100. */
  1101. inClosure = _['invert'](_.flatten(varstack));
  1102. }
  1103. updateInClosure();
  1104. /**
  1105. * First, find code blocks within the template.
  1106. */
  1107. var parsed = string.replace(rgxLookup, function (__, textOp, contents) {
  1108. /**
  1109. * List of accumulated instrumentable characters.
  1110. * @type {Array.<string>}
  1111. */
  1112. var cs = [];
  1113. /**
  1114. * Inside the rgxScope replace function, we push unmatched characters one by one onto
  1115. * cs. Whenever we find any other input, we first flush cs by calling cs_parsed.
  1116. * This calls withLookupListeners which does the magic of replacing native JS references
  1117. * with calls to lookup or lookupText where appropriate.
  1118. */
  1119. function cs_parsed() {
  1120. /**
  1121. * Pass the accumulated string to withLookupListeners, replacing variable
  1122. * references with calls to lookup.
  1123. */
  1124. var instrumented = withLookupListeners(cs.join(''), textOp, inClosure);
  1125. cs = [];
  1126. return instrumented;
  1127. }
  1128. /**
  1129. * Find unquoted segments within the code block. Pass quoted segments through unmodified.
  1130. */
  1131. var newContents = contents.replace(rgxUnquoted, function (__, unquoted, quoted) {
  1132. /**
  1133. * Process the unquoted segments, taking note of variables added in closure scope.
  1134. * We should not lookup-patch variables that are defined in a closure (e.g. as the
  1135. * looping variable of a _.each).
  1136. */
  1137. return unquoted.replace(rgxScope, function (all, args, __, openScope, closeScope, c) {
  1138. if (c) {
  1139. /**
  1140. * Push a single character onto cs to be parsed in cs_parsed. Obviously, not
  1141. * the most efficient mechanism possible.
  1142. */
  1143. cs.push(c);
  1144. return '';
  1145. }
  1146. if (openScope) {
  1147. /**
  1148. * We found a new function declaration; add a new closure scope to the stack.
  1149. */
  1150. varstack.push([]);
  1151. } else if (args) {
  1152. /**
  1153. * We found an argument list for this function; add each of the arguments to
  1154. * the closure scope at the top of the stack (added above).
  1155. */
  1156. args.replace(rgxArgs, function (arg) {
  1157. varstack[varstack.length - 1].push(arg);
  1158. });
  1159. } else if (closeScope) {
  1160. /**
  1161. * We found the closing brace for a closure scope. Pop it off the stack to
  1162. * reflect that any variables attached to it are no longer in scope.
  1163. */
  1164. varstack.pop();
  1165. }
  1166. updateInClosure();
  1167. /**
  1168. * Flush cs, and in addition to that, return the function/variables/brace that we
  1169. * just found.
  1170. */
  1171. return cs_parsed() + all;
  1172. }) + cs_parsed() + (quoted || '');
  1173. }) + cs_parsed();
  1174. return '<%' + textOp + newContents + '%>';
  1175. });
  1176. /**
  1177. * Pass the template to _.template. It will create a function that takes a single "root"
  1178. * parameter. On render, we'll pass either a model/collection or tbone itself as the root.
  1179. * @type {Function}
  1180. */
  1181. var fn = _.template(parsed, null, { 'variable': 'root' });
  1182. /**
  1183. * For debugging purposes, save a copy of the parsed template for reference.
  1184. * @type {string}
  1185. */
  1186. fn.parsed = parsed;
  1187. return fn;
  1188. }
  1189. function renderTemplate(id, root) {
  1190. var template = templates[id];
  1191. if (!template) {
  1192. error('Could not find template ' + id);
  1193. return '';
  1194. }
  1195. if (typeof template === 'string') {
  1196. template = templates[id] = initTemplate(template);
  1197. }
  1198. return template(root);
  1199. }
  1200. var baseView = Backbone.View.extend({
  1201. isView: true,
  1202. initialize: function (opts) {
  1203. var self = this;
  1204. uniqueId(self);
  1205. _.extend(self, opts);
  1206. self.priority = self.parentView ? self.parentView.priority - 1 : BASE_PRIORITY_VIEW;
  1207. self.scope = autorun(self.render, self, self.priority, 'view_' + self.name,
  1208. self.onScopeExecute, self, true);
  1209. },
  1210. onScopeExecute: function (scope) {
  1211. log(INFO, this, 'lookups', scope.lookups);
  1212. },
  1213. /**
  1214. * View.destroy
  1215. *
  1216. * Destroys this view, removing all bindings and sub-views (recursively).
  1217. */
  1218. destroy: function (destroyRoot) {
  1219. var self = this;
  1220. log(VERBOSE, self, 'destroy', 'due to re-render of ' + destroyRoot.name);
  1221. self.destroyed = true;
  1222. self.scope.destroy();
  1223. _.each(self.subViews || [], function (view) {
  1224. view.destroy(self);
  1225. });
  1226. self['destroyDOM'](self.$el);
  1227. },
  1228. /**
  1229. * View.render
  1230. *
  1231. * This function is called at View init, and again whenever any model properties that this View
  1232. * depended on are changed.
  1233. */
  1234. render: function () {
  1235. var self = this;
  1236. // This view may get a reset call at the same instant that another
  1237. // view gets created to replace it.
  1238. if (!self.destroyed) {
  1239. /**
  1240. * Move all this view's children to another temporary DOM element. This will be used as the
  1241. * pseudo-parent element for the destroyDOM call.
  1242. */
  1243. if (self.templateId) {
  1244. /**
  1245. * If the DOM fragment to be removed has an active (focused) element, we attempt
  1246. * to restore that focus after refreshing this DOM fragment. We also attempt
  1247. * to restore the selection start/end, which only works in Webkit/Gecko right
  1248. * now; see the URL below for possible IE compatibility.
  1249. */
  1250. var activeElement = document.activeElement;
  1251. var activeElementSelector, activeElementIndex, selectionStart, selectionEnd;
  1252. if (_.contains($(activeElement).parents(), self.el)) {
  1253. // XXX this could be improved to pick up on IDs/classes/attributes or something?
  1254. activeElementSelector = 'input';
  1255. activeElementIndex = _.indexOf(self.$(activeElementSelector), activeElement);
  1256. // XXX for IE compatibility, this might work:
  1257. // http://the-stickman.com/web-development/javascript/ ...
  1258. // finding-selection-cursor-position-in-a-textarea-in-internet-explorer/
  1259. selectionStart = activeElement.selectionStart;
  1260. selectionEnd = activeElement.selectionEnd;
  1261. }
  1262. var $old = $('<div>').append(this.$el.children());
  1263. var newHtml = renderTemplate(self.templateId, self.root());
  1264. log(INFO, self, 'newhtml', newHtml);
  1265. self.$el.html(newHtml);
  1266. /**
  1267. * Execute the "fragment ready" callback.
  1268. */
  1269. self['ready']();
  1270. self['postReady']();
  1271. /**
  1272. * (Re-)create sub-views for each descendent element with a tbone attribute.
  1273. * On re-renders, the pre-existing list of sub-views is passed to render, which
  1274. * attempts to pair already-rendered views with matching elements in this view's
  1275. * newly re-rendered template. Matching views are transferred to the new DOM
  1276. * hierarchy without disruption.
  1277. */
  1278. var oldSubViews = self.subViews || [];
  1279. self.subViews = render(self.$('[tbone]'), self, oldSubViews);
  1280. var obsoleteSubViews = _.difference(oldSubViews, self.subViews);
  1281. /**
  1282. * Destroy all of the sub-views that were not reused.
  1283. */
  1284. _.each(obsoleteSubViews, function (view) {
  1285. view.destroy(self);
  1286. });
  1287. /**
  1288. * Call destroyDOM with the the pseudo-parent created above. This DOM fragment contains all
  1289. * of the previously-rendered (if any) DOM structure of this view and subviews, minus any
  1290. * subviews that are being reused (which have already been moved to the new parent).
  1291. */
  1292. self['destroyDOM']($old);
  1293. /**
  1294. * If we saved it above, restore the active element focus and selection.
  1295. */
  1296. if (activeElementSelector) {
  1297. var newActiveElement = self.$(activeElementSelector)[activeElementIndex];
  1298. $(newActiveElement).focus();
  1299. if (selectionStart != null && selectionEnd != null) {
  1300. newActiveElement.selectionStart = selectionStart;
  1301. newActiveElement.selectionEnd = selectionEnd;
  1302. }
  1303. }
  1304. } else {
  1305. self['ready']();
  1306. self['postReady']();
  1307. }
  1308. self['postRender']();
  1309. viewRenders++;
  1310. }
  1311. },
  1312. /**
  1313. * View.ready
  1314. *
  1315. * The "template-ready" callback. This is the restricted tbone equivalent of document-ready.
  1316. * It is the recommended means of adding interactivity/data/whatever to Views.
  1317. *
  1318. * At the moment this callback is executed, subviews are neither rendered nor are they
  1319. * attached to the DOM fragment. If you need to interact with subviews, use postRender.
  1320. */
  1321. 'ready': noop,
  1322. /**
  1323. * View.postReady
  1324. *
  1325. * This is the same as ready, except that it executes after ready. The typical use case is
  1326. * to override this in your base template to provide automatic application-wide helpers,
  1327. * such as activating a tooltip library, and to use View.ready for specific view logic.
  1328. */
  1329. 'postReady': noop,
  1330. /**
  1331. * View.postRender
  1332. *
  1333. * The "fragment-updated" callback. This is executed whenever this view is re-rendered,
  1334. * and after all sub-views (recursively) have rendered.
  1335. *
  1336. * Note that because we optimistically re-use sub-views, this may be called multiple times
  1337. * with the same sub-view DOM fragments. Ensure that anything you do to DOM elements in
  1338. * sub-views is idempotent.
  1339. */
  1340. 'postRender': noop,
  1341. /**
  1342. * View.destroyDOM
  1343. *
  1344. * The "document-destroy" callback. Use this to do cleanup on removal of old HTML, e.g.
  1345. * destroying associated tooltips.
  1346. *

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