PageRenderTime 10ms CodeModel.GetById 3ms app.highlight 71ms RepoModel.GetById 1ms app.codeStats 1ms

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

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