/ajax/libs/backbone.layoutmanager/0.8.4/backbone.layoutmanager.js
JavaScript | 843 lines | 425 code | 150 blank | 268 comment | 92 complexity | ea328be9423bcc61dfd6d567fb5cc8fb MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
- /*!
- * backbone.layoutmanager.js v0.8.4
- * Copyright 2013, Tim Branyen (@tbranyen)
- * backbone.layoutmanager.js may be freely distributed under the MIT license.
- */
- (function(window) {
- "use strict";
- // Hoisted, referenced at the bottom of the source. This caches a list of all
- // LayoutManager options at definition time.
- var keys;
- // Localize global dependency references.
- var Backbone = window.Backbone;
- var _ = window._;
- var $ = window.$;
- // Maintain references to the two `Backbone.View` functions that are
- // overwritten so that they can be proxied.
- var _configure = Backbone.View.prototype._configure;
- var render = Backbone.View.prototype.render;
- // Cache these methods for performance.
- var aPush = Array.prototype.push;
- var aConcat = Array.prototype.concat;
- var aSplice = Array.prototype.splice;
- // LayoutManager is a wrapper around a `Backbone.View`.
- var LayoutManager = Backbone.View.extend({
- // This named function allows for significantly easier debugging.
- constructor: function Layout(options) {
- // Options may not always be passed to the constructor, this ensures it is
- // always an object.
- options = options || {};
- // Grant this View superpowers.
- LayoutManager.setupView(this, options);
- // Have Backbone set up the rest of this View.
- Backbone.View.call(this, options);
- },
- // Shorthand to `setView` function with the `insert` flag set.
- insertView: function(selector, view) {
- // If the `view` argument exists, then a selector was passed in. This code
- // path will forward the selector on to `setView`.
- if (view) {
- return this.setView(selector, view, true);
- }
- // If no `view` argument is defined, then assume the first argument is the
- // View, somewhat now confusingly named `selector`.
- return this.setView(selector, true);
- },
- // Iterate over an object and ensure every value is wrapped in an array to
- // ensure they will be inserted, then pass that object to `setViews`.
- insertViews: function(views) {
- // If an array of views was passed it should be inserted into the
- // root view. Much like calling insertView without a selector.
- if (_.isArray(views)) {
- return this.setViews({ "": views });
- }
- _.each(views, function(view, selector) {
- views[selector] = _.isArray(view) ? view : [view];
- });
- return this.setViews(views);
- },
- // Returns the View that matches the `getViews` filter function.
- getView: function(fn) {
- // If `getView` is invoked with undefined as the first argument, then the
- // second argument will be used instead. This is to allow
- // `getViews(undefined, fn)` to work as `getViews(fn)`. Useful for when
- // you are allowing an optional selector.
- if (typeof fn !== "function" && typeof fn !== "string") {
- fn = arguments[1];
- }
- return this.getViews(fn).first().value();
- },
- // Provide a filter function to get a flattened array of all the subviews.
- // If the filter function is omitted it will return all subviews. If a
- // String is passed instead, it will return the Views for that selector.
- getViews: function(fn) {
- // Generate an array of all top level (no deeply nested) Views flattened.
- var views = _.chain(this.views).map(function(view) {
- return _.isArray(view) ? view : [view];
- }, this).flatten().value();
- // If the filter argument is a String, then return a chained Version of the
- // elements.
- if (typeof fn === "string") {
- return _.chain([this.views[fn]]).flatten();
- }
- // If the argument passed is an Object, then pass it to `_.where`.
- if (typeof fn === "object") {
- return _.chain([_.where(views, fn)]).flatten();
- }
- // If a filter function is provided, run it on all Views and return a
- // wrapped chain. Otherwise, simply return a wrapped chain of all Views.
- return _.chain(typeof fn === "function" ? _.filter(views, fn) : views);
- },
- // Use this to remove Views, internally uses `getViews` so you can pass the
- // same argument here as you would to that method.
- removeView: function(fn) {
- // Allow an optional selector or function to find the right model and
- // remove nested Views based off the results of the selector or filter.
- return this.getViews(fn).each(function(nestedView) {
- nestedView.remove();
- });
- },
- // This takes in a partial name and view instance and assigns them to
- // the internal collection of views. If a view is not a LayoutManager
- // instance, then mix in the LayoutManager prototype. This ensures
- // all Views can be used successfully.
- //
- // Must definitely wrap any render method passed in or defaults to a
- // typical render function `return layout(this).render()`.
- setView: function(name, view, insert) {
- var manager, existing, options;
- // Parent view, the one you are setting a View on.
- var root = this;
- // If no name was passed, use an empty string and shift all arguments.
- if (typeof name !== "string") {
- insert = view;
- view = name;
- name = "";
- }
- // If the parent views object doesn't exist... create it.
- this.views = this.views || {};
- // Shorthand the `__manager__` property.
- manager = view.__manager__;
- // Shorthand the View that potentially already exists.
- existing = this.views[name];
- // If the View has not been properly set up, throw an Error message
- // indicating that the View needs `manage: true` set.
- if (!manager) {
- throw new Error("Please set `View#manage` property with selector '" +
- name + "' to `true`.");
- }
- // Assign options.
- options = view.getAllOptions();
- // Add reference to the parentView.
- manager.parent = root;
- // Add reference to the placement selector used.
- manager.selector = name;
- // Set up event bubbling, inspired by Backbone.ViewMaster. Do not bubble
- // internal events that are triggered.
- view.on("all", function(name) {
- if (name !== "beforeRender" && name !== "afterRender") {
- root.trigger.apply(root, arguments);
- }
- }, view);
- // Code path is less complex for Views that are not being inserted. Simply
- // remove existing Views and bail out with the assignment.
- if (!insert) {
- // If the View we are adding has already been rendered, simply inject it
- // into the parent.
- if (manager.hasRendered) {
- // Apply the partial.
- options.partial(root.$el, view.$el, root.__manager__, manager);
- }
- // Ensure remove is called when swapping View's.
- if (existing) {
- // If the views are an array, iterate and remove each individually.
- _.each(aConcat.call([], existing), function(nestedView) {
- nestedView.remove();
- });
- }
- // Assign to main views object and return for chainability.
- return this.views[name] = view;
- }
- // Ensure this.views[name] is an array and push this View to the end.
- this.views[name] = aConcat.call([], existing || [], view);
- // Put the view into `insert` mode.
- manager.insert = true;
- return view;
- },
- // Allows the setting of multiple views instead of a single view.
- setViews: function(views) {
- // Iterate over all the views and use the View's view method to assign.
- _.each(views, function(view, name) {
- // If the view is an array put all views into insert mode.
- if (_.isArray(view)) {
- return _.each(view, function(view) {
- this.insertView(name, view);
- }, this);
- }
- // Assign each view using the view function.
- this.setView(name, view);
- }, this);
- // Allow for chaining
- return this;
- },
- // By default this should find all nested views and render them into
- // the this.el and call done once all of them have successfully been
- // resolved.
- //
- // This function returns a promise that can be chained to determine
- // once all subviews and main view have been rendered into the view.el.
- render: function() {
- var root = this;
- var options = root.getAllOptions();
- var manager = root.__manager__;
- var parent = manager.parent;
- var rentManager = parent && parent.__manager__;
- var def = options.deferred();
- // Triggered once the render has succeeded.
- function resolve() {
- var next, afterRender;
- // If there is a parent, attach.
- if (parent) {
- if (!options.contains(parent.el, root.el)) {
- // Apply the partial.
- options.partial(parent.$el, root.$el, rentManager, manager);
- }
- }
- // Ensure events are always correctly bound after rendering.
- root.delegateEvents();
- // Set this View as successfully rendered.
- manager.hasRendered = true;
- // Only process the queue if it exists.
- if (next = manager.queue.shift()) {
- // Ensure that the next render is only called after all other
- // `done` handlers have completed. This will prevent `render`
- // callbacks from firing out of order.
- next();
- } else {
- // Once the queue is depleted, remove it, the render process has
- // completed.
- delete manager.queue;
- }
- // Reusable function for triggering the afterRender callback and event
- // and setting the hasRendered flag.
- function completeRender() {
- var afterRender = options.afterRender;
- if (afterRender) {
- afterRender.call(root, root);
- }
- // Always emit an afterRender event.
- root.trigger("afterRender", root);
- }
- // If the parent is currently rendering, wait until it has completed
- // until calling the nested View's `afterRender`.
- if (rentManager && rentManager.queue) {
- // Wait until the parent View has finished rendering, which could be
- // asynchronous, and trigger afterRender on this View once it has
- // compeleted.
- parent.once("afterRender", completeRender);
- } else {
- // This View and its parent have both rendered.
- completeRender();
- }
- return def.resolveWith(root, [root]);
- }
- // Actually facilitate a render.
- function actuallyRender() {
- var options = root.getAllOptions();
- var manager = root.__manager__;
- var parent = manager.parent;
- var rentManager = parent && parent.__manager__;
- // The `_viewRender` method is broken out to abstract away from having
- // too much code in `actuallyRender`.
- root._render(LayoutManager._viewRender, options).done(function() {
- // If there are no children to worry about, complete the render
- // instantly.
- if (!_.keys(root.views).length) {
- return resolve();
- }
- // Create a list of promises to wait on until rendering is done.
- // Since this method will run on all children as well, its sufficient
- // for a full hierarchical.
- var promises = _.map(root.views, function(view) {
- var insert = _.isArray(view);
- // If items are being inserted, they will be in a non-zero length
- // Array.
- if (insert && view.length) {
- // Schedule each view to be rendered in order and return a promise
- // representing the result of the final rendering.
- return _.reduce(view.slice(1), function(prevRender, view) {
- return prevRender.then(function() {
- return view.render();
- });
- // The first view should be rendered immediately, and the resulting
- // promise used to initialize the reduction.
- }, view[0].render());
- }
- // Only return the fetch deferred, resolve the main deferred after
- // the element has been attached to it's parent.
- return !insert ? view.render() : view;
- });
- // Once all nested Views have been rendered, resolve this View's
- // deferred.
- options.when(promises).done(resolve);
- });
- }
- // Another render is currently happening if there is an existing queue, so
- // push a closure to render later into the queue.
- if (manager.queue) {
- aPush.call(manager.queue, actuallyRender);
- } else {
- manager.queue = [];
- // This the first `render`, preceeding the `queue` so render
- // immediately.
- actuallyRender(root, def);
- }
- // Add the View to the deferred so that `view.render().view.el` is
- // possible.
- def.view = root;
- // This is the promise that determines if the `render` function has
- // completed or not.
- return def;
- },
- // Ensure the cleanup function is called whenever remove is called.
- remove: function() {
- // Force remove itself from its parent.
- LayoutManager._removeView(this, true);
- // Call the original remove function.
- return this._remove.apply(this, arguments);
- },
- // Merge instance and global options.
- getAllOptions: function() {
- // Instance overrides take precedence, fallback to prototype options.
- return _.extend({}, this, LayoutManager.prototype.options, this.options);
- }
- },
- {
- // Clearable cache.
- _cache: {},
- // Creates a deferred and returns a function to call when finished.
- _makeAsync: function(options, done) {
- var handler = options.deferred();
- // Used to handle asynchronous renders.
- handler.async = function() {
- handler._isAsync = true;
- return done;
- };
- return handler;
- },
- // This gets passed to all _render methods. The `root` value here is passed
- // from the `manage(this).render()` line in the `_render` function
- _viewRender: function(root, options) {
- var url, contents, fetchAsync, renderedEl;
- var manager = root.__manager__;
- // This function is responsible for pairing the rendered template into
- // the DOM element.
- function applyTemplate(rendered) {
- // Actually put the rendered contents into the element.
- if (rendered) {
- // If no container is specified, we must replace the content.
- if (manager.noel) {
- // Hold a reference to created element as replaceWith doesn't return new el.
- renderedEl = root.$el.html(rendered).children();
- root.$el.replaceWith(renderedEl);
- // Don't delegate events here - we'll do that in resolve()
- root.setElement(renderedEl, false);
- } else {
- options.html(root.$el, rendered);
- }
- }
- // Resolve only after fetch and render have succeeded.
- fetchAsync.resolveWith(root, [root]);
- }
- // Once the template is successfully fetched, use its contents to proceed.
- // Context argument is first, since it is bound for partial application
- // reasons.
- function done(context, contents) {
- // Store the rendered template someplace so it can be re-assignable.
- var rendered;
- // This allows the `render` method to be asynchronous as well as `fetch`.
- var renderAsync = LayoutManager._makeAsync(options, function(rendered) {
- applyTemplate(rendered);
- });
- // Ensure the cache is up-to-date.
- LayoutManager.cache(url, contents);
- // Render the View into the el property.
- if (contents) {
- rendered = options.render.call(renderAsync, contents, context);
- }
- // If the function was synchronous, continue execution.
- if (!renderAsync._isAsync) {
- applyTemplate(rendered);
- }
- }
- return {
- // This `render` function is what gets called inside of the View render,
- // when `manage(this).render` is called. Returns a promise that can be
- // used to know when the element has been rendered into its parent.
- render: function() {
- var context = root.serialize || options.serialize;
- var template = root.template || options.template;
- // If data is a function, immediately call it.
- if (_.isFunction(context)) {
- context = context.call(root);
- }
- // This allows for `var done = this.async()` and then `done(contents)`.
- fetchAsync = LayoutManager._makeAsync(options, function(contents) {
- done(context, contents);
- });
- // Set the url to the prefix + the view's template property.
- if (typeof template === "string") {
- url = options.prefix + template;
- }
- // Check if contents are already cached and if they are, simply process
- // the template with the correct data.
- if (contents = LayoutManager.cache(url)) {
- done(context, contents, url);
- return fetchAsync;
- }
- // Fetch layout and template contents.
- if (typeof template === "string") {
- contents = options.fetch.call(fetchAsync, options.prefix + template);
- // If the template is already a function, simply call it.
- } else if (typeof template === "function") {
- contents = template;
- // If its not a string and not undefined, pass the value to `fetch`.
- } else if (template != null) {
- contents = options.fetch.call(fetchAsync, template);
- }
- // If the function was synchronous, continue execution.
- if (!fetchAsync._isAsync) {
- done(context, contents);
- }
- return fetchAsync;
- }
- };
- },
- // Remove all nested Views.
- _removeViews: function(root, force) {
- var views;
- // Shift arguments around.
- if (typeof root === "boolean") {
- force = root;
- root = this;
- }
- // Allow removeView to be called on instances.
- root = root || this;
- // Iterate over all of the nested View's and remove.
- root.getViews().each(function(view) {
- // Force doesn't care about if a View has rendered or not.
- if (view.__manager__.hasRendered || force) {
- LayoutManager._removeView(view, force);
- }
- });
- },
- // Remove a single nested View.
- _removeView: function(view, force) {
- var parentViews;
- // Shorthand the manager for easier access.
- var manager = view.__manager__;
- // Test for keep.
- var keep = typeof view.keep === "boolean" ? view.keep : view.options.keep;
- // Only remove views that do not have `keep` attribute set, unless the
- // View is in `insert` mode and the force flag is set.
- if (!keep && (manager.insert === true || force)) {
- // Clean out the events.
- LayoutManager.cleanViews(view);
- // Since we are removing this view, force subviews to remove
- view._removeViews(true);
- // Remove the View completely.
- view.$el.remove();
- // Bail out early if no parent exists.
- if (!manager.parent) { return; }
- // Assign (if they exist) the sibling Views to a property.
- parentViews = manager.parent.views[manager.selector];
- // If this is an array of items remove items that are not marked to
- // keep.
- if (_.isArray(parentViews)) {
- // Remove duplicate Views.
- return _.each(_.clone(parentViews), function(view, i) {
- // If the managers match, splice off this View.
- if (view && view.__manager__ === manager) {
- aSplice.call(parentViews, i, 1);
- }
- });
- }
- // Otherwise delete the parent selector.
- delete manager.parent.views[manager.selector];
- }
- },
- // Cache templates into LayoutManager._cache.
- cache: function(path, contents) {
- // If template path is found in the cache, return the contents.
- if (path in this._cache && contents == null) {
- return this._cache[path];
- // Ensure path and contents aren't undefined.
- } else if (path != null && contents != null) {
- return this._cache[path] = contents;
- }
- // If the template is not in the cache, return undefined.
- },
- // Accept either a single view or an array of views to clean of all DOM
- // events internal model and collection references and all Backbone.Events.
- cleanViews: function(views) {
- // Clear out all existing views.
- _.each(aConcat.call([], views), function(view) {
- // Remove all custom events attached to this View.
- view.unbind();
- // Automatically unbind `model`.
- if (view.model instanceof Backbone.Model) {
- view.model.off(null, null, view);
- }
- // Automatically unbind `collection`.
- if (view.collection instanceof Backbone.Collection) {
- view.collection.off(null, null, view);
- }
- // Automatically unbind events bound to this View.
- view.stopListening();
- // If a custom cleanup method was provided on the view, call it after
- // the initial cleanup is done
- _.result(view, "cleanup");
- });
- },
- // This static method allows for global configuration of LayoutManager.
- configure: function(options) {
- _.extend(LayoutManager.prototype.options, options);
- // Allow LayoutManager to manage Backbone.View.prototype.
- if (options.manage) {
- Backbone.View.prototype.manage = true;
- }
- // Disable the element globally.
- if (options.el === false) {
- Backbone.View.prototype.el = false;
- }
- },
- // Configure a View to work with the LayoutManager plugin.
- setupView: function(views, options) {
- // Set up all Views passed.
- _.each(aConcat.call([], views), function(view) {
- // If the View has already been setup, no need to do it again.
- if (view.__manager__) {
- return;
- }
- var views, declaredViews, viewOptions;
- var proto = LayoutManager.prototype;
- var viewOverrides = _.pick(view, keys);
- // Ensure necessary properties are set.
- _.defaults(view, {
- // Ensure a view always has a views object.
- views: {},
- // Internal state object used to store whether or not a View has been
- // taken over by layout manager and if it has been rendered into the DOM.
- __manager__: {},
- // Add the ability to remove all Views.
- _removeViews: LayoutManager._removeViews,
- // Add the ability to remove itself.
- _removeView: LayoutManager._removeView
- // Mix in all LayoutManager prototype properties as well.
- }, LayoutManager.prototype);
- // Extend the options with the prototype and passed options.
- options = view.options = _.defaults(options || {}, view.options,
- proto.options);
- // Ensure view events are properly copied over.
- viewOptions = _.pick(options, aConcat.call(["events"],
- _.values(options.events)));
- // Merge the View options into the View.
- _.extend(view, viewOptions);
- // If the View still has the Backbone.View#render method, remove it. Don't
- // want it accidentally overriding the LM render.
- if (viewOverrides.render === LayoutManager.prototype.render ||
- viewOverrides.render === Backbone.View.prototype.render) {
- delete viewOverrides.render;
- }
- // Pick out the specific properties that can be dynamically added at
- // runtime and ensure they are available on the view object.
- _.extend(options, viewOverrides);
- // By default the original Remove function is the Backbone.View one.
- view._remove = Backbone.View.prototype.remove;
- // Always use this render function when using LayoutManager.
- view._render = function(manage, options) {
- // Keep the view consistent between callbacks and deferreds.
- var view = this;
- // Shorthand the manager.
- var manager = view.__manager__;
- // Cache these properties.
- var beforeRender = options.beforeRender;
- // Ensure all nested Views are properly scrubbed if re-rendering.
- if (manager.hasRendered) {
- this._removeViews();
- }
- // If a beforeRender function is defined, call it.
- if (beforeRender) {
- beforeRender.call(this, this);
- }
- // Always emit a beforeRender event.
- this.trigger("beforeRender", this);
- // Render!
- return manage(this, options).render();
- };
- // Ensure the render is always set correctly.
- view.render = LayoutManager.prototype.render;
- // If the user provided their own remove override, use that instead of the
- // default.
- if (view.remove !== proto.remove) {
- view._remove = view.remove;
- view.remove = proto.remove;
- }
- // Normalize views to exist on either instance or options, default to
- // options.
- views = options.views || view.views;
- // Set the internal views, only if selectors have been provided.
- if (_.keys(views).length) {
- // Keep original object declared containing Views.
- declaredViews = views;
- // Reset the property to avoid duplication or overwritting.
- view.views = {};
- // Set the declared Views.
- view.setViews(declaredViews);
- }
- // If a template is passed use that instead.
- if (view.options.template) {
- view.options.template = options.template;
- // Ensure the template is mapped over.
- } else if (view.template) {
- options.template = view.template;
- // Remove it from the instance.
- delete view.template;
- }
- });
- }
- });
- // Convenience assignment to make creating Layout's slightly shorter.
- Backbone.Layout = LayoutManager;
- // Tack on the version.
- LayoutManager.VERSION = "0.8.4";
- // Override _configure to provide extra functionality that is necessary in
- // order for the render function reference to be bound during initialize.
- Backbone.View.prototype._configure = function(options) {
- var noel, retVal;
- // Remove the container element provided by Backbone.
- if ("el" in options ? options.el === false : this.el === false) {
- noel = true;
- }
- // Run the original _configure.
- retVal = _configure.apply(this, arguments);
- // If manage is set, do it!
- if (options.manage || this.manage) {
- // Set up this View.
- LayoutManager.setupView(this);
- }
- // Assign the `noel` property once we're sure the View we're working with is
- // mangaed by LayoutManager.
- if (this.__manager__) {
- this.__manager__.noel = noel;
- }
- // Act like nothing happened.
- return retVal;
- };
- // Default configuration options; designed to be overriden.
- LayoutManager.prototype.options = {
- // Prefix template/layout paths.
- prefix: "",
- // Can be used to supply a different deferred implementation.
- deferred: function() {
- return $.Deferred();
- },
- // Fetch is passed a path and is expected to return template contents as a
- // function or string.
- fetch: function(path) {
- return _.template($(path).html());
- },
- // This is the most common way you will want to partially apply a view into
- // a layout.
- partial: function($root, $el, rentManager, manager) {
- // If selector is specified, attempt to find it.
- if (manager.selector) {
- if (rentManager.noel) {
- var $filtered = $root.filter(manager.selector);
- $root = $filtered.length ? $filtered : $root.find(manager.selector);
- } else {
- $root = $root.find(manager.selector);
- }
- }
- // Use the insert method if insert argument is true.
- if (manager.insert) {
- this.insert($root, $el);
- } else {
- this.html($root, $el);
- }
- },
- // Override this with a custom HTML method, passed a root element and content
- // (a jQuery collection or a string) to replace the innerHTML with.
- html: function($root, content) {
- $root.html(content);
- },
- // Very similar to HTML except this one will appendChild by default.
- insert: function($root, $el) {
- $root.append($el);
- },
- // Return a deferred for when all promises resolve/reject.
- when: function(promises) {
- return $.when.apply(null, promises);
- },
- // By default, render using underscore's templating.
- render: function(template, context) {
- return template(context);
- },
- // A method to determine if a View contains another.
- contains: function(parent, child) {
- return $.contains(parent, child);
- }
- };
- // Maintain a list of the keys at define time.
- keys = _.keys(LayoutManager.prototype.options);
- })(typeof global === "object" ? global : this);