You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2014/08/18 11:33:00 UTC
[2/2] fauxton commit: updated refs/heads/master to 2283352
Refactored Layout and View, upgraded to latest LM
The Layout object was a simple wrapper over the Layout constructor
instead of extending the constructor itself. I've patched this to make
it more consistent and removed duplicative methods. I've also removed
the tests for `removeView` since that is now a built-in method.
The latest LM contains bugfixes and render performance updates that
unforuntately have to be opt-out. Areas in the source or tests that are
expecting synchronous renders must be patched to work asynchronously to
take advantage.
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/07cce5a6
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/07cce5a6
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/07cce5a6
Branch: refs/heads/master
Commit: 07cce5a61d281b61ebcf3fa9ee77a72d7ca52247
Parents: a57188c
Author: Tim Branyen <ti...@tabdeveloper.com>
Authored: Fri Aug 1 10:18:13 2014 -0400
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Aug 18 11:30:50 2014 +0200
----------------------------------------------------------------------
app/core/base.js | 4 +
app/core/layout.js | 77 ++------
app/core/tests/layoutSpec.js | 38 +---
assets/js/plugins/backbone.layoutmanager.js | 240 ++++++++++++++++++-----
4 files changed, 217 insertions(+), 142 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/07cce5a6/app/core/base.js
----------------------------------------------------------------------
diff --git a/app/core/base.js b/app/core/base.js
index 15499b3..9fbb7a0 100644
--- a/app/core/base.js
+++ b/app/core/base.js
@@ -62,6 +62,10 @@ function(Backbone, LayoutManager) {
manage: true,
disableLoader: false,
+ // Either tests or source are expecting synchronous renders, so disable
+ // asynchronous rendering improvements.
+ useRAF: false,
+
forceRender: function () {
this.hasRendered = false;
}
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/07cce5a6/app/core/layout.js
----------------------------------------------------------------------
diff --git a/app/core/layout.js b/app/core/layout.js
index ff339c7..0a45e62 100644
--- a/app/core/layout.js
+++ b/app/core/layout.js
@@ -10,82 +10,31 @@
// License for the specific language governing permissions and limitations under
// the License.
-define([
- "backbone",
- "plugins/backbone.layoutmanager"
-], function(Backbone) {
+define(function(require, exports, module) {
+ var Backbone = require("backbone");
+ var LayoutManager = require("plugins/backbone.layoutmanager");
- // A wrapper of the main Backbone.layoutmanager
- // Allows the main layout of the page to be changed by any plugin.
- var Layout = function () {
- this.layout = new Backbone.Layout({
- template: "templates/layouts/with_sidebar",
- });
+ var Layout = Backbone.Layout.extend({
+ template: "templates/layouts/with_sidebar",
- this.layoutViews = {};
- this.el = this.layout.el;
- };
-
- Layout.configure = function (options) {
- Backbone.Layout.configure(options);
- };
-
- // creatings the dashboard object same way backbone does
- _.extend(Layout.prototype, {
- render: function () {
- return this.layout.render();
- },
+ // Either tests or source are expecting synchronous renders, so disable
+ // asynchronous rendering improvements.
+ useRAF: false,
setTemplate: function(template) {
if (template.prefix){
- this.layout.template = template.prefix + template.name;
+ this.template = template.prefix + template.name;
} else{
- this.layout.template = "templates/layouts/" + template;
+ this.template = "templates/layouts/" + template;
}
+
// If we're changing layouts all bets are off, so kill off all the
// existing views in the layout.
- _.each(this.layoutViews, function(view){view.remove();});
- this.layoutViews = {};
+ this.removeView();
this.render();
- },
-
- setView: function(selector, view, keep) {
- this.layout.setView(selector, view, false);
-
- if (!keep) {
- this.layoutViews[selector] = view;
- }
-
- return view;
- },
-
- renderView: function(selector) {
- var view = this.layoutViews[selector];
- if (!view) {
- return false;
- } else {
- return view.render();
- }
- },
-
- removeView: function (selector) {
- var view = this.layout.getView(selector);
-
- if (!view) {
- return false;
- }
-
- view.remove();
-
- if (this.layoutViews[selector]) {
- delete this.layoutViews[selector];
- }
-
- return true;
}
-
});
- return Layout;
+ module.exports = Layout;
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/07cce5a6/app/core/tests/layoutSpec.js
----------------------------------------------------------------------
diff --git a/app/core/tests/layoutSpec.js b/app/core/tests/layoutSpec.js
index 40b1947..2b87173 100644
--- a/app/core/tests/layoutSpec.js
+++ b/app/core/tests/layoutSpec.js
@@ -27,24 +27,20 @@ define([
it("Should set template without prefix", function () {
layout.setTemplate('myTemplate');
- assert.equal(layout.layout.template, 'templates/layouts/myTemplate');
+ assert.equal(layout.template, 'templates/layouts/myTemplate');
});
it("Should set template with prefix", function () {
layout.setTemplate({name: 'myTemplate', prefix: 'myPrefix/'});
- assert.equal(layout.layout.template, 'myPrefix/myTemplate');
+ assert.equal(layout.template, 'myPrefix/myTemplate');
});
it("Should remove old views", function () {
- var view = {
- remove: function () {}
- };
+ var view = new FauxtonAPI.Layout();
- layout.layoutViews = {
- 'selector': view
- };
+ layout.setView('selector', view);
var mockRemove = sinon.spy(view, 'remove');
layout.setTemplate('myTemplate');
@@ -62,31 +58,5 @@ define([
});
});
-
- describe('#renderView', function () {
-
- it('Should render existing view', function () {
- var view = new Backbone.View();
- var mockRender = sinon.spy(view, 'render');
- layout.layoutViews = {
- '#selector': view
- };
-
- var out = layout.renderView('#selector');
-
- assert.ok(mockRender.calledOnce);
- });
-
- it('Should return false for non-existing view', function () {
- var view = new Backbone.View();
- layout.layoutViews = {
- 'selector': view
- };
-
- var out = layout.renderView('wrongSelector');
- assert.notOk(out, 'No view found');
- });
- });
-
});
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/07cce5a6/assets/js/plugins/backbone.layoutmanager.js
----------------------------------------------------------------------
diff --git a/assets/js/plugins/backbone.layoutmanager.js b/assets/js/plugins/backbone.layoutmanager.js
index c5d4a80..e20bf5a 100644
--- a/assets/js/plugins/backbone.layoutmanager.js
+++ b/assets/js/plugins/backbone.layoutmanager.js
@@ -1,22 +1,35 @@
/*!
- * backbone.layoutmanager.js v0.9.4
+ * backbone.layoutmanager.js v0.9.5
* Copyright 2013, Tim Branyen (@tbranyen)
* backbone.layoutmanager.js may be freely distributed under the MIT license.
*/
(function(window, factory) {
"use strict";
- var Backbone = window.Backbone;
// AMD. Register as an anonymous module. Wrap in function so we have access
// to root via `this`.
if (typeof define === "function" && define.amd) {
- return define(["backbone", "underscore", "jquery"], function() {
+ define(["backbone", "underscore", "jquery"], function() {
return factory.apply(window, arguments);
});
}
+ // Node. Does not work with strict CommonJS, but only CommonJS-like
+ // environments that support module.exports, like Node.
+ else if (typeof exports === "object") {
+ var Backbone = require("backbone");
+ var _ = require("underscore");
+ // In a browserify build, since this is the entry point, Backbone.$
+ // is not bound. Ensure that it is.
+ Backbone.$ = Backbone.$ || require("jquery");
+
+ module.exports = factory.call(window, Backbone, _, Backbone.$);
+ }
+
// Browser globals.
- Backbone.Layout = factory.call(window, Backbone, window._, Backbone.$);
+ else {
+ factory.call(window, window.Backbone, window._, window.Backbone.$);
+ }
}(typeof global === "object" ? global : this, function (Backbone, _, $) {
"use strict";
@@ -236,20 +249,33 @@ var LayoutManager = Backbone.View.extend({
return this.__manager__.renderDeferred.promise();
},
+ // Proxy `then` for easier invocation.
+ then: function() {
+ return this.promise().then.apply(this, arguments);
+ },
+
// Sometimes it's desirable to only render the child views under the parent.
// This is typical for a layout that does not change. This method will
- // iterate over the child Views and aggregate all child render promises and
- // return the parent View. The internal `promise()` method will return the
- // aggregate promise that resolves once all children have completed their
- // render.
- renderViews: function() {
+ // iterate over the provided views or delegate to `getViews` to fetch child
+ // Views and aggregate all render promises and return the parent View.
+ // The internal `promise()` method will return the aggregate promise that
+ // resolves once all children have completed their render.
+ renderViews: function(views) {
var root = this;
var manager = root.__manager__;
var newDeferred = root.deferred();
+ // If the caller provided an array of views then render those, otherwise
+ // delegate to getViews.
+ if (views && _.isArray(views)) {
+ views = _.chain(views);
+ } else {
+ views = root.getViews(views);
+ }
+
// Collect all promises from rendering the child views and wait till they
// all complete.
- var promises = root.getViews().map(function(view) {
+ var promises = views.map(function(view) {
return view.render().__manager__.renderDeferred;
}).value();
@@ -392,16 +418,12 @@ var LayoutManager = Backbone.View.extend({
// 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 (view.hasRendered) {
- // Apply the partial.
- view.partial(root.$el, view.$el, root.__manager__, manager);
+ // Ensure remove is called only when swapping in a new view (when the
+ // view is the same, it does not need to be removed or cleaned up).
+ if (root.getView(name) !== view) {
+ root.removeView(name);
}
- // Ensure remove is called when swapping View's.
- root.removeView(name);
-
// Assign to main views object and return for chainability.
return root.views[selector] = view;
}
@@ -450,7 +472,6 @@ var LayoutManager = Backbone.View.extend({
// Triggered once the render has succeeded.
function resolve() {
- var next;
// Insert all subViews into the parent at once.
_.each(root.views, function(views, selector) {
@@ -465,8 +486,7 @@ var LayoutManager = Backbone.View.extend({
if (parent && !manager.insertedViaFragment) {
if (!root.contains(parent.el, root.el)) {
// Apply the partial using parent's html() method.
- parent.partial(parent.$el, root.$el, rentManager,
- manager);
+ parent.partial(parent.$el, root.$el, rentManager, manager);
}
}
@@ -475,21 +495,24 @@ var LayoutManager = Backbone.View.extend({
// Set this View as successfully rendered.
root.hasRendered = true;
+ manager.renderInProgress = false;
+
+ // Clear triggeredByRAF flag.
+ delete manager.triggeredByRAF;
// Only process the queue if it exists.
- if (next = manager.queue.shift()) {
+ if (manager.queue && manager.queue.length) {
// 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();
+ (manager.queue.shift())();
} 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.
+ // Reusable function for triggering the afterRender callback and event.
function completeRender() {
var console = window.console;
var afterRender = root.afterRender;
@@ -520,10 +543,10 @@ var LayoutManager = Backbone.View.extend({
// If the parent is currently rendering, wait until it has completed
// until calling the nested View's `afterRender`.
- if (rentManager && rentManager.queue) {
+ if (rentManager && (rentManager.renderInProgress || rentManager.queue)) {
// Wait until the parent View has finished rendering, which could be
// asynchronous, and trigger afterRender on this View once it has
- // compeleted.
+ // completed.
parent.once("afterRender", completeRender);
} else {
// This View and its parent have both rendered.
@@ -574,23 +597,21 @@ var LayoutManager = Backbone.View.extend({
});
}
- // 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 = [];
+ // Mark this render as in progress. This will prevent
+ // afterRender from being fired until the entire chain has rendered.
+ manager.renderInProgress = true;
- // This the first `render`, preceeding the `queue` so render
- // immediately.
- actuallyRender(root, def);
- }
+ // Start the render.
+ // Register this request & cancel any that conflict.
+ root._registerWithRAF(actuallyRender, def);
// Put the deferred inside of the `__manager__` object, since we don't want
// end users accessing this directly anymore in favor of the `afterRender`
// event. So instead of doing `render().then(...` do
// `render().once("afterRender", ...`.
- root.__manager__.renderDeferred = def;
+ // FIXME: I think we need to move back to promises so that we don't
+ // miss events, regardless of sync/async (useRAF setting)
+ manager.renderDeferred = def;
// Return the actual View for chainability purposes.
return root;
@@ -603,6 +624,75 @@ var LayoutManager = Backbone.View.extend({
// Call the original remove function.
return this._remove.apply(this, arguments);
+ },
+
+ // Register a view render with RAF.
+ _registerWithRAF: function(callback, deferred) {
+ var root = this;
+ var manager = root.__manager__;
+ var rentManager = manager.parent && manager.parent.__manager__;
+
+ // Allow RAF processing to be shut off using `useRAF`:false.
+ if (this.useRAF === false) {
+ if (manager.queue) {
+ aPush.call(manager.queue, callback);
+ } else {
+ manager.queue = [];
+ callback();
+ }
+ return;
+ }
+
+ // Keep track of all deferreds so we can resolve them.
+ manager.deferreds = manager.deferreds || [];
+ manager.deferreds.push(deferred);
+
+ // Schedule resolving all deferreds that are waiting.
+ deferred.done(resolveDeferreds);
+
+ // Cancel any other renders on this view that are queued to execute.
+ this._cancelQueuedRAFRender();
+
+ // Trigger immediately if the parent was triggered by RAF.
+ // The flag propagates downward so this view's children are also
+ // rendered immediately.
+ if (rentManager && rentManager.triggeredByRAF) {
+ return finish();
+ }
+
+ // Register this request with requestAnimationFrame.
+ manager.rafID = root.requestAnimationFrame(finish);
+
+ function finish() {
+ // Remove this ID as it is no longer valid.
+ manager.rafID = null;
+
+ // Set flag (will propagate to children) so they render
+ // without waiting for RAF.
+ manager.triggeredByRAF = true;
+
+ // Call original cb.
+ callback();
+ }
+
+ // Resolve all deferreds that were cancelled previously, if any.
+ // This allows the user to bind callbacks to any render callback,
+ // even if it was cancelled above.
+ function resolveDeferreds() {
+ for (var i = 0; i < manager.deferreds.length; i++){
+ manager.deferreds[i].resolveWith(root, [root]);
+ }
+ manager.deferreds = [];
+ }
+ },
+
+ // Cancel any queued render requests.
+ _cancelQueuedRAFRender: function() {
+ var root = this;
+ var manager = root.__manager__;
+ if (manager.rafID != null) {
+ root.cancelAnimationFrame(manager.rafID);
+ }
}
},
@@ -652,6 +742,9 @@ var LayoutManager = Backbone.View.extend({
// Remove the View completely.
view.$el.remove();
+ // Cancel any pending renders, if present.
+ view._cancelQueuedRAFRender();
+
// Bail out early if no parent exists.
if (!manager.parent) { return; }
@@ -735,12 +828,18 @@ var LayoutManager = Backbone.View.extend({
if (options.suppressWarnings === true) {
Backbone.View.prototype.suppressWarnings = true;
}
+
+ // Allow global configuration of `useRAF`.
+ if (options.useRAF === false) {
+ Backbone.View.prototype.useRAF = false;
+ }
},
// Configure a View to work with the LayoutManager plugin.
setupView: function(views, options) {
- // Don't break the options object (passed into Backbone.View#initialize).
- options = options || {};
+ // Ensure that options is always an object, and clone it so that
+ // changes to the original object don't screw up this view.
+ options = _.extend({}, options);
// Set up all Views passed.
_.each(aConcat.call([], views), function(view) {
@@ -819,14 +918,14 @@ var LayoutManager = Backbone.View.extend({
}
});
-LayoutManager.VERSION = "0.9.4";
+LayoutManager.VERSION = "0.9.5";
// Expose through Backbone object.
Backbone.Layout = LayoutManager;
// Override _configure to provide extra functionality that is necessary in
// order for the render function reference to be bound during initialize.
-Backbone.View = function(options) {
+Backbone.View.prototype.constructor = function(options) {
var noel;
// Ensure options is always an object.
@@ -854,6 +953,8 @@ Backbone.View = function(options) {
ViewConstructor.apply(this, arguments);
};
+Backbone.View = Backbone.View.prototype.constructor;
+
// Copy over the extend method.
Backbone.View.extend = ViewConstructor.extend;
@@ -865,6 +966,10 @@ var defaultOptions = {
// Prefix template/layout paths.
prefix: "",
+ // Use requestAnimationFrame to queue up view rendering and cancel
+ // repeat requests. Leave on for better performance.
+ useRAF: true,
+
// Can be used to supply a different deferred implementation.
deferred: function() {
return $.Deferred();
@@ -878,7 +983,7 @@ var defaultOptions = {
// By default, render using underscore's templating and trim output.
renderTemplate: function(template, context) {
- return trim(template(context));
+ return trim(template.call(this, context));
},
// By default, pass model attributes to the templates
@@ -964,7 +1069,54 @@ var defaultOptions = {
// A method to determine if a View contains another.
contains: function(parent, child) {
return $.contains(parent, child);
- }
+ },
+
+ // Based on:
+ // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+ // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and
+ // Tino Zijdel.
+ requestAnimationFrame: (function() {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ var requestAnimationFrame = window.requestAnimationFrame;
+
+ for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
+ requestAnimationFrame = window[vendors[i] + "RequestAnimationFrame"];
+ }
+
+ if (!requestAnimationFrame){
+ requestAnimationFrame = function(callback) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function() {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ }
+
+ return _.bind(requestAnimationFrame, window);
+ })(),
+
+ cancelAnimationFrame: (function() {
+ var vendors = ["ms", "moz", "webkit", "o"];
+ var cancelAnimationFrame = window.cancelAnimationFrame;
+
+ for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
+ cancelAnimationFrame =
+ window[vendors[i] + "CancelAnimationFrame"] ||
+ window[vendors[i] + "CancelRequestAnimationFrame"];
+ }
+
+ if (!cancelAnimationFrame) {
+ cancelAnimationFrame = function(id) {
+ clearTimeout(id);
+ };
+ }
+
+ return _.bind(cancelAnimationFrame, window);
+ })()
};
// Extend LayoutManager with default options.