diff options
author | Garren Smith <garren.smith@gmail.com> | 2014-01-08 15:26:12 +0200 |
---|---|---|
committer | Garren Smith <garren.smith@gmail.com> | 2014-01-29 16:53:46 +0200 |
commit | 96be583d8566536b0356296d4f9b01164e31f4fc (patch) | |
tree | f67e98718799a55bfe24fb2e496b18482b286ef4 | |
parent | 5c9f9a9f056ca71c516ddd7eb9ab32f8eb01dc12 (diff) | |
download | couchdb-96be583d8566536b0356296d4f9b01164e31f4fc.tar.gz |
Fauxton: Split up api.js
Split up api to core/*. To seperate out the Framework from the UI.
Move tests to fit with this refactor.
Change addons/Fauxton with work with api extraction.
31 files changed, 1101 insertions, 875 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index ba160b9cd..0f5c49153 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -111,14 +111,22 @@ FAUXTON_FILES = \ fauxton/app/addons/verifyinstall/routes.js \ fauxton/app/addons/verifyinstall/templates/main.html \ fauxton/app/addons/verifyinstall/assets/less/verifyinstall.less \ - fauxton/app/api.js \ + fauxton/app/core/api.js \ + fauxton/app/core/auth.js \ + fauxton/app/core/base.js \ + fauxton/app/core/couchdbSession.js \ + fauxton/app/core/layout.js \ + fauxton/app/core/routeObject.js \ + fauxton/app/core/router.js \ + fauxton/app/core/utils.js \ + fauxton/app/core/tests/layoutSpec.js \ + fauxton/app/core/tests/routeObjectSpec.js \ fauxton/app/app.js \ fauxton/app/config.js \ fauxton/app/helpers.js \ fauxton/app/initialize.js.underscore \ fauxton/app/load_addons.js.underscore \ fauxton/app/main.js \ - fauxton/app/utils.js \ fauxton/app/addons/databases/base.js \ fauxton/app/addons/databases/resources.js \ fauxton/app/addons/databases/routes.js \ @@ -131,11 +139,13 @@ FAUXTON_FILES = \ fauxton/app/addons/fauxton/base.js \ fauxton/app/addons/fauxton/components.js \ fauxton/app/addons/fauxton/layout.js \ + fauxton/app/addons/fauxton/resizeColumns.js \ + fauxton/app/addons/fauxton/tests/baseSpec.js \ + fauxton/app/addons/fauxton/tests/navbarSpec.js \ + fauxton/app/addons/fauxton/tests/paginateSpec.js \ fauxton/app/addons/pouchdb/base.js \ fauxton/app/addons/pouchdb/pouch.collate.js \ fauxton/app/addons/pouchdb/pouchdb.mapreduce.js \ - fauxton/app/resizeColumns.js \ - fauxton/app/router.js \ fauxton/app/addons/databases/templates/item.html \ fauxton/app/addons/databases/templates/list.html \ fauxton/app/addons/databases/templates/newdatabase.html \ @@ -309,10 +319,6 @@ FAUXTON_FILES = \ fauxton/tasks/couchserver.js \ fauxton/tasks/fauxton.js \ fauxton/tasks/helper.js \ - fauxton/test/core/layoutSpec.js \ - fauxton/test/core/navbarSpec.js \ - fauxton/test/core/paginateSpec.js \ - fauxton/test/core/routeObjectSpec.js \ fauxton/test/mocha/chai.js \ fauxton/test/mocha/mocha.css \ fauxton/test/mocha/mocha.js \ diff --git a/src/fauxton/Gruntfile.js b/src/fauxton/Gruntfile.js index 32065b67d..2c5a24906 100644 --- a/src/fauxton/Gruntfile.js +++ b/src/fauxton/Gruntfile.js @@ -357,7 +357,7 @@ module.exports = function(grunt) { mochaSetup: { default: { - files: { src: helper.watchFiles(['[Ss]pec.js'], ['./test/core/**/*[Ss]pec.js', './app/**/*[Ss]pec.js'])}, + files: { src: helper.watchFiles(['[Ss]pec.js'], ['./app/**/*[Ss]pec.js'])}, template: 'test/test.config.underscore', config: './app/config.js' } @@ -424,7 +424,7 @@ module.exports = function(grunt) { grunt.registerTask('lint', ['clean', 'jshint']); grunt.registerTask('test', ['lint', 'dependencies', 'test_inline']); // lighter weight test task for use inside dev/watch - grunt.registerTask('test_inline', ['mochaSetup','jst', 'concat:test_config_js', 'mocha_phantomjs']); + grunt.registerTask('test_inline', ['mochaSetup','jst', 'concat:test_config_js','mocha_phantomjs']); // Fetch dependencies (from git or local dir), lint them and make load_addons grunt.registerTask('dependencies', ['get_deps', 'gen_load_addons:default']); // build templates, js and css diff --git a/src/fauxton/app/addons/activetasks/tests/viewsSpec.js b/src/fauxton/app/addons/activetasks/tests/viewsSpec.js index 395b60a79..13c904974 100644 --- a/src/fauxton/app/addons/activetasks/tests/viewsSpec.js +++ b/src/fauxton/app/addons/activetasks/tests/viewsSpec.js @@ -132,8 +132,5 @@ define([ }); }); - - - }); }); diff --git a/src/fauxton/app/addons/auth/base.js b/src/fauxton/app/addons/auth/base.js index 9f9a33214..7f69a7f66 100644 --- a/src/fauxton/app/addons/auth/base.js +++ b/src/fauxton/app/addons/auth/base.js @@ -20,6 +20,7 @@ function(app, FauxtonAPI, Auth) { Auth.session = new Auth.Session(); FauxtonAPI.setSession(Auth.session); + app.session = Auth.session; Auth.initialize = function() { Auth.navLink = new Auth.NavLink({model: Auth.session}); diff --git a/src/fauxton/app/addons/auth/resources.js b/src/fauxton/app/addons/auth/resources.js index 970d55bb9..321d3026d 100644 --- a/src/fauxton/app/addons/auth/resources.js +++ b/src/fauxton/app/addons/auth/resources.js @@ -12,10 +12,11 @@ define([ "app", - "api" + "api", + "core/CouchdbSession" ], -function (app, FauxtonAPI) { +function (app, FauxtonAPI, CouchdbSession) { var Auth = new FauxtonAPI.addon(); @@ -46,7 +47,7 @@ function (app, FauxtonAPI) { } }); - Auth.Session = FauxtonAPI.Session.extend({ + Auth.Session = CouchdbSession.Session.extend({ url: app.host + '/_session', initialize: function (options) { diff --git a/src/fauxton/app/addons/documents/views.js b/src/fauxton/app/addons/documents/views.js index 95163557a..aeb5983b1 100644 --- a/src/fauxton/app/addons/documents/views.js +++ b/src/fauxton/app/addons/documents/views.js @@ -21,7 +21,7 @@ define([ "addons/pouchdb/base", // Libs - "resizeColumns", + "addons/Fauxton/resizeColumns", // Plugins "plugins/beautify", @@ -720,11 +720,11 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum }, cleanup: function () { - //if (!this.pagination) { return; } - this.pagination.remove(); - //this.pagination = null; this.allDocsNumber.remove(); _.each(this.rows, function (row) {row.remove();}); + + if (!this.pagination) { return; } + this.pagination.remove(); }, beforeRender: function() { diff --git a/src/fauxton/app/addons/fauxton/base.js b/src/fauxton/app/addons/fauxton/base.js index 1811e845f..35babb5f6 100644 --- a/src/fauxton/app/addons/fauxton/base.js +++ b/src/fauxton/app/addons/fauxton/base.js @@ -12,18 +12,86 @@ define([ "app", - // Libs - "backbone", - "resizeColumns", + "api", + "addons/fauxton/resizeColumns" ], -function(app, Backbone, resizeColumns) { +function(app, FauxtonAPI, resizeColumns) { - //resizeAnimation - app.resizeColumns = new resizeColumns({}); - app.resizeColumns.onResizeHandler(); + var Fauxton = FauxtonAPI.addon(); + FauxtonAPI.addNotification = function (options) { + options = _.extend({ + msg: "Notification Event Triggered!", + type: "info", + selector: "#global-notifications" + }, options); - var Fauxton = {}; + var view = new Fauxton.Notification(options); + return view.renderNotification(); + }; + + FauxtonAPI.UUID = FauxtonAPI.Model.extend({ + initialize: function(options) { + options = _.extend({count: 1}, options); + this.count = options.count; + }, + + url: function() { + return app.host + "/_uuids?count=" + this.count; + }, + + next: function() { + return this.get("uuids").pop(); + } + }); + + + Fauxton.initialize = function () { + app.footer = new Fauxton.Footer({el: "#footer-content"}), + app.navBar = new Fauxton.NavBar(); + app.apiBar = new Fauxton.ApiBar(); + + FauxtonAPI.when.apply(null, app.footer.establish()).done(function() { + FauxtonAPI.masterLayout.setView("#primary-navbar", app.navBar, true); + FauxtonAPI.masterLayout.setView("#api-navbar", app.apiBar, true); + app.navBar.render(); + app.apiBar.render(); + + app.footer.render(); + }); + + FauxtonAPI.masterLayout.navBar = app.navBar; + FauxtonAPI.masterLayout.apiBar = app.apiBar; + + FauxtonAPI.RouteObject.on('beforeFullRender', function (routeObject) { + $('#primary-navbar li').removeClass('active'); + + if (routeObject.selectedHeader) { + app.selectedHeader = routeObject.selectedHeader; + $('#primary-navbar li[data-nav-name="' + routeObject.selectedHeader + '"]').addClass('active'); + } + }); + + FauxtonAPI.RouteObject.on('beforeEstablish', function (routeObject) { + FauxtonAPI.masterLayout.removeView('#breadcrumbs'); + var crumbs = routeObject.get('crumbs'); + + if (crumbs.length) { + FauxtonAPI.masterLayout.setView('#breadcrumbs', new Fauxton.Breadcrumbs({ + crumbs: crumbs + }), true).render(); + } + }); + + FauxtonAPI.RouteObject.on('renderComplete', function (routeObject) { + var masterLayout = FauxtonAPI.masterLayout; + if (routeObject.get('apiUrl')){ + masterLayout.apiBar.update(routeObject.get('apiUrl')); + } else { + masterLayout.apiBar.hide(); + } + }); + }; Fauxton.Breadcrumbs = Backbone.View.extend({ template: "templates/fauxton/breadcrumbs", @@ -41,7 +109,9 @@ function(app, Backbone, resizeColumns) { }); Fauxton.VersionInfo = Backbone.Model.extend({ - url: app.host + url: function () { + return app.host; + } }); // TODO: this View should extend from FauxtonApi.View. @@ -76,6 +146,16 @@ function(app, Backbone, resizeColumns) { bottomNavLinks: [], footerNavLinks: [], + initialize: function () { + _.bindAll(this); + //resizeAnimation + this.resizeColumns = new resizeColumns({}); + this.resizeColumns.onResizeHandler(); + + FauxtonAPI.extensions.on('add:navbar:addHeaderLink', this.addLink); + FauxtonAPI.extensions.on('removeItem:navbar:addHeaderLink', this.removeLink); + }, + serialize: function() { return { navLinks: this.navLinks, @@ -124,7 +204,6 @@ function(app, Backbone, resizeColumns) { }, afterRender: function(){ - $('#primary-navbar li[data-nav-name="' + app.selectedHeader + '"]').addClass('active'); var menuOpen = true; @@ -141,21 +220,22 @@ function(app, Backbone, resizeColumns) { function toggleMenu(){ $selectorList.toggleClass('closeMenu'); menuOpen = $selectorList.hasClass('closeMenu'); - app.resizeColumns.onResizeHandler(); + this.resizeColumns.onResizeHandler(); } - + + var that = this; $('#primary-navbar').on("click", ".nav a", function(){ if (!($selectorList.hasClass('closeMenu'))){ setTimeout( function(){ $selectorList.addClass('closeMenu'); - app.resizeColumns.onResizeHandler(); + that.resizeColumns.onResizeHandler(); },3000); } }); - app.resizeColumns.initialize(); + this.resizeColumns.initialize(); }, beforeRender: function () { @@ -265,6 +345,5 @@ function(app, Backbone, resizeColumns) { } }); - return Fauxton; }); diff --git a/src/fauxton/app/addons/fauxton/components.js b/src/fauxton/app/addons/fauxton/components.js index edde428eb..c26fc963f 100644 --- a/src/fauxton/app/addons/fauxton/components.js +++ b/src/fauxton/app/addons/fauxton/components.js @@ -25,10 +25,11 @@ define([ // Libs "api", "ace_configuration", + "spin" ], -function(app, FauxtonAPI, ace) { - var Components = app.module(); +function(app, FauxtonAPI, ace, spin) { + var Components = FauxtonAPI.addon(); Components.Pagination = FauxtonAPI.View.extend({ template: "templates/fauxton/pagination", @@ -124,7 +125,7 @@ function(app, FauxtonAPI, ace) { pageEnd: function () { if (this.collection.length < this.pageLimit()) { - return this.collection.length; + return (this.previousParams.length * this.pageLimit()) + this.collection.length; } return (this.previousParams.length * this.pageLimit()) + this.pageLimit(); @@ -235,6 +236,8 @@ function(app, FauxtonAPI, ace) { this.theme = options.theme || 'crimson_editor'; this.couchJSHINT = options.couchJSHINT; this.edited = false; + + _.bindAll(this); }, afterRender: function () { @@ -295,13 +298,14 @@ function(app, FauxtonAPI, ace) { }, removeIncorrectAnnotations: function () { - var editor = this.editor; + var editor = this.editor, + isIgnorableError = this.isIgnorableError; - this.editor.getSession().on("changeAnnotation", function(){ + this.editor.getSession().on("changeAnnotation", function () { var annotations = editor.getSession().getAnnotations(); var newAnnotations = _.reduce(annotations, function (annotations, error) { - if (!FauxtonAPI.isIgnorableError(error.raw)) { + if (!isIgnorableError(error.raw)) { annotations.push(error); } return annotations; @@ -338,12 +342,94 @@ function(app, FauxtonAPI, ace) { var errors = this.getAnnotations(); // By default CouchDB view functions don't pass lint return _.every(errors, function(error) { - return FauxtonAPI.isIgnorableError(error.raw); + return this.isIgnorableError(error.raw); },this); + }, + + // List of JSHINT errors to ignore + // Gets around problem of anonymous functions not being a valid statement + excludedViewErrors: [ + "Missing name in function declaration.", + "['{a}'] is better written in dot notation." + ], + + isIgnorableError: function(msg) { + return _.contains(this.excludedViewErrors, msg); } }); + //need to make this into a backbone view... + var routeObjectSpinner; + FauxtonAPI.RouteObject.on('beforeEstablish', function (routeObject) { + if (!routeObject.disableLoader){ + var opts = { + lines: 16, // The number of lines to draw + length: 8, // The length of each line + width: 4, // The line thickness + radius: 12, // The radius of the inner circle + color: '#333', // #rbg or #rrggbb + speed: 1, // Rounds per second + trail: 10, // Afterglow percentage + shadow: false // Whether to render a shadow + }; + + if (!$('.spinner').length) { + $('<div class="spinner"></div>') + .appendTo('#app-container'); + } + + routeObjectSpinner = new Spinner(opts).spin(); + $('.spinner').append(routeObjectSpinner.el); + } + }); + + var removeRouteObjectSpinner = function () { + if (routeObjectSpinner) { + routeObjectSpinner.stop(); + $('.spinner').remove(); + } + }; + + var removeViewSpinner = function () { + if (viewSpinner){ + viewSpinner.stop(); + $('.spinner').remove(); + } + }; + + var viewSpinner; + FauxtonAPI.RouteObject.on('beforeRender', function (routeObject, view, selector) { + removeRouteObjectSpinner(); + + if (!view.disableLoader){ + var opts = { + lines: 16, // The number of lines to draw + length: 8, // The length of each line + width: 4, // The line thickness + radius: 12, // The radius of the inner circle + color: '#333', // #rbg or #rrggbb + speed: 1, // Rounds per second + trail: 10, // Afterglow percentage + shadow: false // Whether to render a shadow + }; + + viewSpinner = new Spinner(opts).spin(); + $('<div class="spinner"></div>') + .appendTo(selector) + .append(viewSpinner.el); + } + }); + + FauxtonAPI.RouteObject.on('afterRender', function (routeObject, view, selector) { + removeViewSpinner(); + }); + + FauxtonAPI.RouteObject.on('viewHasRendered', function () { + removeViewSpinner(); + removeRouteObjectSpinner(); + }); + return Components; }); diff --git a/src/fauxton/app/resizeColumns.js b/src/fauxton/app/addons/fauxton/resizeColumns.js index bb5076742..abfcd2f63 100644 --- a/src/fauxton/app/resizeColumns.js +++ b/src/fauxton/app/addons/fauxton/resizeColumns.js @@ -18,10 +18,10 @@ // "purely functional" helper system. define([ - "utils" + "api" ], -function(utils) { +function(FauxtonAPI) { var Resize = function(options){ this.options = options; @@ -42,8 +42,8 @@ function(utils) { var that = this; //add throttler :) this.lazyLayout = _.debounce(that.onResizeHandler, 300).bind(this); - utils.addWindowResize(this.lazyLayout,"animation"); - utils.initWindowResize(); + FauxtonAPI.utils.addWindowResize(this.lazyLayout,"animation"); + FauxtonAPI.utils.initWindowResize(); this.onResizeHandler(); }, updateOptions:function(options){ @@ -52,7 +52,7 @@ function(utils) { this.options.selectorElements = options.selectorElements || ".window-resizeable"; }, turnOff:function(){ - utils.removeWindowResize("animation"); + FauxtonAPI.utils.removeWindowResize("animation"); }, cleanupCallback: function(){ this.callback = null; diff --git a/src/fauxton/app/addons/fauxton/tests/baseSpec.js b/src/fauxton/app/addons/fauxton/tests/baseSpec.js new file mode 100644 index 000000000..7df4dfc9d --- /dev/null +++ b/src/fauxton/app/addons/fauxton/tests/baseSpec.js @@ -0,0 +1,79 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +define([ + 'testUtils', + 'api', + 'addons/fauxton/base', + "backbone" +], function (testUtils, FauxtonAPI, Base) { + var assert = testUtils.assert; + + + describe('Fauxton RouteObject:beforeEstablish', function () { + var TestRouteObject, testRouteObject, mockLayout, _layout; + + before(function () { + Base.initialize(); + _layout = FauxtonAPI.masterLayout; + }); + + beforeEach(function () { + TestRouteObject = FauxtonAPI.RouteObject.extend({ + crumbs: ['mycrumbs'] + }); + + testRouteObject = new TestRouteObject(); + var apiBar = {}; + apiBar.hide = sinon.spy(); + var setViewSpy = sinon.stub(); + setViewSpy.returns({ + render: function () {} + }); + + // Need to find a better way of doing this + mockLayout = { + setTemplate: sinon.spy(), + clearBreadcrumbs: sinon.spy(), + setView: setViewSpy, + renderView: sinon.spy(), + removeView: sinon.spy(), + hooks: [], + setBreadcrumbs: sinon.spy(), + apiBar: apiBar, + layout: { + setView: function () {} + } + }; + + + }); + + after(function () { + FauxtonAPI.masterLayout = _layout; + }); + + it('Should clear breadcrumbs', function () { + FauxtonAPI.masterLayout = mockLayout; + testRouteObject.renderWith('the-route', mockLayout, 'args'); + assert.ok(mockLayout.removeView.calledWith('#breadcrumbs'), 'Clear Breadcrumbs called'); + }); + + it('Should set breadcrumbs when breadcrumbs exist', function () { + FauxtonAPI.masterLayout = mockLayout; + testRouteObject.renderWith('the-route', mockLayout, 'args'); + assert.ok(mockLayout.setView.calledOnce, 'Set Breadcrumbs was called'); + }); + + }); + + +}); diff --git a/src/fauxton/test/core/navbarSpec.js b/src/fauxton/app/addons/fauxton/tests/navbarSpec.js index 3eca6f6ce..3eca6f6ce 100644 --- a/src/fauxton/test/core/navbarSpec.js +++ b/src/fauxton/app/addons/fauxton/tests/navbarSpec.js diff --git a/src/fauxton/test/core/paginateSpec.js b/src/fauxton/app/addons/fauxton/tests/paginateSpec.js index d05b322a6..535e26f4a 100644 --- a/src/fauxton/test/core/paginateSpec.js +++ b/src/fauxton/app/addons/fauxton/tests/paginateSpec.js @@ -10,12 +10,12 @@ // License for the specific language governing permissions and limitations under // the License. define([ - 'api', + 'app', 'addons/fauxton/components', 'addons/documents/resources', 'testUtils', - 'app' -], function (FauxtonAPI, Views, Models, testUtils, app) { + 'api' +], function (app, Views, Models, testUtils, FauxtonAPI) { var assert = testUtils.assert, ViewSandbox = testUtils.ViewSandbox; @@ -23,10 +23,6 @@ define([ describe('IndexPaginate', function () { var viewSandbox, paginate, collection, navigateMock; beforeEach(function () { - app.router = { - navigate: function () {} - }; - collection = new Models.IndexCollection([{ id:'myId1', doc: 'num1' diff --git a/src/fauxton/app/api.js b/src/fauxton/app/api.js deleted file mode 100644 index 9ac895eb5..000000000 --- a/src/fauxton/app/api.js +++ /dev/null @@ -1,593 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -define([ - "app", - - // Modules - "addons/fauxton/base", - "spin" -], - -function(app, Fauxton) { - var FauxtonAPI = app.module(); - - FauxtonAPI.moduleExtensions = { - Routes: { - } - }; - - FauxtonAPI.addonExtensions = { - initialize: function() {} - }; - - // List of JSHINT errors to ignore - // Gets around problem of anonymous functions not being a valid statement - FauxtonAPI.excludedViewErrors = [ - "Missing name in function declaration.", - "['{a}'] is better written in dot notation." - ]; - - FauxtonAPI.isIgnorableError = function(msg) { - return _.contains(FauxtonAPI.excludedViewErrors, msg); - }; - - FauxtonAPI.View = Backbone.View.extend({ - // This should return an array of promises, an empty array, or null - establish: function() { - return null; - }, - - loaderClassname: 'loader', - - disableLoader: false, - - forceRender: function () { - this.hasRendered = false; - } - }); - - FauxtonAPI.navigate = function(url, _opts) { - var options = _.extend({trigger: true}, _opts ); - app.router.navigate(url,options); - }; - - FauxtonAPI.beforeUnload = function () { - app.router.beforeUnload.apply(app.router, arguments); - }; - - FauxtonAPI.removeBeforeUnload = function () { - app.router.removeBeforeUnload.apply(app.router, arguments); - }; - - FauxtonAPI.addHeaderLink = function(link) { - app.masterLayout.navBar.addLink(link); - }; - - FauxtonAPI.removeHeaderLink = function(link) { - app.masterLayout.navBar.removeLink(link); - }; - - FauxtonAPI.Deferred = function() { - return $.Deferred(); - }; - - FauxtonAPI.when = function (deferreds) { - if (deferreds instanceof Array) { - return $.when.apply(null, deferreds); - } - - return $.when(deferreds); - }; - - FauxtonAPI.addRoute = function(route) { - app.router.route(route.route, route.name, route.callback); - }; - - FauxtonAPI.triggerRouteEvent = function (routeEvent, args) { - app.router.triggerRouteEvent("route:"+routeEvent, args); - }; - - FauxtonAPI.module = function(extra) { - return app.module(_.extend(FauxtonAPI.moduleExtensions, extra)); - }; - - FauxtonAPI.addon = function(extra) { - return FauxtonAPI.module(FauxtonAPI.addonExtensions, extra); - }; - - FauxtonAPI.addNotification = function(options) { - options = _.extend({ - msg: "Notification Event Triggered!", - type: "info", - selector: "#global-notifications" - }, options); - var view = new Fauxton.Notification(options); - - return view.renderNotification(); - }; - - FauxtonAPI.UUID = Backbone.Model.extend({ - initialize: function(options) { - options = _.extend({count: 1}, options); - this.count = options.count; - }, - - url: function() { - return app.host + "/_uuids?count=" + this.count; - }, - - next: function() { - return this.get("uuids").pop(); - } - }); - - FauxtonAPI.Session = Backbone.Model.extend({ - url: app.host + '/_session', - - user: function () { - var userCtx = this.get('userCtx'); - - if (!userCtx || !userCtx.name) { return null; } - - return { - name: userCtx.name, - roles: userCtx.roles - }; - }, - - fetchOnce: function (opt) { - var options = _.extend({}, opt); - - if (!this._deferred || this._deferred.state() === "rejected" || options.forceFetch ) { - this._deferred = this.fetch(); - } - - return this._deferred; - }, - - fetchUser: function (opt) { - var that = this, - currentUser = this.user(); - - return this.fetchOnce(opt).then(function () { - var user = that.user(); - - // Notify anyone listening on these events that either a user has changed - // or current user is the same - if (currentUser !== user) { - that.trigger('session:userChanged'); - } else { - that.trigger('session:userFetched'); - } - - // this will return the user as a value to all function that calls done on this - // eg. session.fetchUser().done(user) { .. do something with user ..} - return user; - }); - } - }); - - FauxtonAPI.setSession = function (newSession) { - app.session = FauxtonAPI.session = newSession; - return FauxtonAPI.session.fetchUser(); - }; - - FauxtonAPI.setSession(new FauxtonAPI.Session()); - - // This is not exposed externally as it should not need to be accessed or overridden - var Auth = function (options) { - this._options = options; - this.initialize.apply(this, arguments); - }; - - // Piggy-back on Backbone's self-propagating extend function, - Auth.extend = Backbone.Model.extend; - - _.extend(Auth.prototype, Backbone.Events, { - authDeniedCb: function() {}, - - initialize: function() { - var that = this; - }, - - authHandlerCb : function (roles) { - var deferred = $.Deferred(); - deferred.resolve(); - return deferred; - }, - - registerAuth: function (authHandlerCb) { - this.authHandlerCb = authHandlerCb; - }, - - registerAuthDenied: function (authDeniedCb) { - this.authDeniedCb = authDeniedCb; - }, - - checkAccess: function (roles) { - var requiredRoles = roles || [], - that = this; - - return FauxtonAPI.session.fetchUser().then(function (user) { - return FauxtonAPI.when(that.authHandlerCb(FauxtonAPI.session, requiredRoles)); - }); - } - }); - - FauxtonAPI.auth = new Auth(); - - FauxtonAPI.RouteObject = function(options) { - this._options = options; - - this._configure(options || {}); - this.initialize.apply(this, arguments); - this.addEvents(); - }; - - var broadcaster = {}; - _.extend(broadcaster, Backbone.Events); - - FauxtonAPI.RouteObject.on = function (eventName, fn) { - broadcaster.on(eventName, fn); - }; - - /* How Route Object events work - To listen to a specific route objects events: - - myRouteObject = FauxtonAPI.RouteObject.extend({ - events: { - "beforeRender": "beforeRenderEvent" - }, - - beforeRenderEvent: function (view, selector) { - console.log('Hey, beforeRenderEvent triggered',arguments); - }, - }); - - It is also possible to listen to events triggered from all Routeobjects. - This is great for more general things like adding loaders, hooks. - - FauxtonAPI.RouteObject.on('beforeRender', function (routeObject, view, selector) { - console.log('hey, this will trigger when any routeobject renders a view'); - }); - - Current Events to subscribe to: - * beforeFullRender -- before a full render is being done - * beforeEstablish -- before the routeobject calls establish - * AfterEstablish -- after the routeobject has run establish - * beforeRender -- before a view is rendered - * afterRender -- a view is finished being rendered - * renderComplete -- all rendering is complete - - */ - - // Piggy-back on Backbone's self-propagating extend function - FauxtonAPI.RouteObject.extend = Backbone.Model.extend; - - var routeObjectOptions = ["views", "routes", "events", "roles", "crumbs", "layout", "apiUrl", "establish"]; - - _.extend(FauxtonAPI.RouteObject.prototype, Backbone.Events, { - // Should these be default vals or empty funcs? - views: {}, - routes: {}, - events: {}, - crumbs: [], - layout: "with_sidebar", - apiUrl: null, - disableLoader: false, - loaderClassname: 'loader', - renderedState: false, - establish: function() {}, - route: function() {}, - roles: [], - _promises: [], - initialize: function() {} - }, { - - renderWith: function(route, masterLayout, args) { - var routeObject = this, - triggerBroadcast = _.bind(this.triggerBroadcast, this); - - // Only want to redo the template if its a full render - if (!this.renderedState) { - masterLayout.setTemplate(this.layout); - triggerBroadcast('beforeFullRender'); - $('#primary-navbar li').removeClass('active'); - - if (this.selectedHeader) { - app.selectedHeader = this.selectedHeader; - $('#primary-navbar li[data-nav-name="' + this.selectedHeader + '"]').addClass('active'); - } - } - - masterLayout.clearBreadcrumbs(); - var crumbs = this.get('crumbs'); - - if (crumbs.length) { - masterLayout.setBreadcrumbs(new Fauxton.Breadcrumbs({ - crumbs: crumbs - })); - } - - triggerBroadcast('beforeEstablish'); - var establishPromise = this.establish(); - this.addPromise(establishPromise); - FauxtonAPI.when(establishPromise).then(function(resp) { - triggerBroadcast('afterEstablish'); - _.each(routeObject.getViews(), function(view, selector) { - if(view.hasRendered) { - triggerBroadcast('viewHasRendered', view, selector); - return; - } - - triggerBroadcast('beforeRender', view, selector); - var viewPromise = view.establish(); - routeObject.addPromise(viewPromise); - FauxtonAPI.when(viewPromise).then(function(resp) { - masterLayout.setView(selector, view); - - masterLayout.renderView(selector); - triggerBroadcast('afterRender', view, selector); - }, function(resp) { - view.establishError = { - error: true, - reason: resp - }; - - if (resp && resp.responseText) { - var errorText = JSON.parse(resp.responseText).reason; - FauxtonAPI.addNotification({ - msg: 'An Error occurred: ' + errorText, - type: 'error', - clear: true - }); - } - - masterLayout.renderView(selector); - }); - - }); - }.bind(this), function (resp) { - if (!resp || !resp.responseText) { return; } - FauxtonAPI.addNotification({ - msg: 'An Error occurred' + JSON.parse(resp.responseText).reason, - type: 'error', - clear: true - }); - }); - - if (this.get('apiUrl')){ - masterLayout.apiBar.update(this.get('apiUrl')); - } else { - masterLayout.apiBar.hide(); - } - - // Track that we've done a full initial render - this.renderedState = true; - triggerBroadcast('renderComplete'); - }, - - triggerBroadcast: function (eventName) { - var args = Array.prototype.slice.call(arguments); - this.trigger.apply(this, args); - - args.splice(0,1, eventName, this); - broadcaster.trigger.apply(broadcaster, args); - }, - - get: function(key) { - return _.isFunction(this[key]) ? this[key]() : this[key]; - }, - - addEvents: function(events) { - events = events || this.get('events'); - _.each(events, function(method, event) { - if (!_.isFunction(method) && !_.isFunction(this[method])) { - throw new Error("Invalid method: "+method); - } - method = _.isFunction(method) ? method : this[method]; - - this.on(event, method); - }, this); - }, - - _configure: function(options) { - _.each(_.intersection(_.keys(options), routeObjectOptions), function(key) { - this[key] = options[key]; - }, this); - }, - - getView: function(selector) { - return this.views[selector]; - }, - - setView: function(selector, view) { - this.views[selector] = view; - return view; - }, - - getViews: function() { - return this.views; - }, - - removeViews: function () { - _.each(this.views, function (view, selector) { - view.remove(); - delete this.views[selector]; - }, this); - }, - - addPromise: function (promise) { - if (_.isEmpty(promise)) { return; } - - if (_.isArray(promise)) { - return _.each(promise, function (p) { - this._promises.push(p); - }, this); - } - - this._promises.push(promise); - }, - - cleanup: function () { - this.removeViews(); - this.rejectPromises(); - }, - - rejectPromises: function () { - _.each(this._promises, function (promise) { - if (promise.state() === "resolved") { return; } - if (promise.abort) { - return promise.abort("Route change"); - } - - promise.reject(); - }, this); - - this._promises = []; - }, - - getRouteUrls: function () { - return _.keys(this.get('routes')); - }, - - hasRoute: function (route) { - if (this.get('routes')[route]) { - return true; - } - return false; - }, - - routeCallback: function (route, args) { - var routes = this.get('routes'), - routeObj = routes[route], - routeCallback; - - if (typeof routeObj === 'object') { - routeCallback = this[routeObj.route]; - } else { - routeCallback = this[routeObj]; - } - - routeCallback.apply(this, args); - }, - - getRouteRoles: function (routeUrl) { - var route = this.get('routes')[routeUrl]; - - if ((typeof route === 'object') && route.roles) { - return route.roles; - } - - return this.roles; - } - - }); - - // We could look at moving the spinner code out to its own module - var routeObjectSpinner; - FauxtonAPI.RouteObject.on('beforeEstablish', function (routeObject) { - if (!routeObject.disableLoader){ - var opts = { - lines: 16, // The number of lines to draw - length: 8, // The length of each line - width: 4, // The line thickness - radius: 12, // The radius of the inner circle - color: '#333', // #rbg or #rrggbb - speed: 1, // Rounds per second - trail: 10, // Afterglow percentage - shadow: false // Whether to render a shadow - }; - - if (!$('.spinner').length) { - $('<div class="spinner"></div>') - .appendTo('#app-container'); - } - - routeObjectSpinner = new Spinner(opts).spin(); - $('.spinner').append(routeObjectSpinner.el); - } - }); - - var removeRouteObjectSpinner = function () { - if (routeObjectSpinner) { - routeObjectSpinner.stop(); - $('.spinner').remove(); - } - }; - - var removeViewSpinner = function () { - if (viewSpinner){ - viewSpinner.stop(); - $('.spinner').remove(); - } - }; - - var viewSpinner; - FauxtonAPI.RouteObject.on('beforeRender', function (routeObject, view, selector) { - removeRouteObjectSpinner(); - - if (!view.disableLoader){ - var opts = { - lines: 16, // The number of lines to draw - length: 8, // The length of each line - width: 4, // The line thickness - radius: 12, // The radius of the inner circle - color: '#333', // #rbg or #rrggbb - speed: 1, // Rounds per second - trail: 10, // Afterglow percentage - shadow: false // Whether to render a shadow - }; - - viewSpinner = new Spinner(opts).spin(); - $('<div class="spinner"></div>') - .appendTo(selector) - .append(viewSpinner.el); - } - }); - - FauxtonAPI.RouteObject.on('afterRender', function (routeObject, view, selector) { - removeViewSpinner(); - }); - - FauxtonAPI.RouteObject.on('viewHasRendered', function () { - removeViewSpinner(); - removeRouteObjectSpinner(); - }); - - var extensions = _.extend({}, Backbone.Events); - // Can look at a remove function later. - FauxtonAPI.registerExtension = function (name, view) { - if (!extensions[name]) { - extensions[name] = []; - } - - extensions.trigger('add:' + name, view); - extensions[name].push(view); - }; - - FauxtonAPI.getExtensions = function (name) { - var views = extensions[name]; - - if (!views) { - views = []; - } - - return views; - }; - - FauxtonAPI.extensions = extensions; - - app.fauxtonAPI = FauxtonAPI; - return app.fauxtonAPI; -}); diff --git a/src/fauxton/app/app.js b/src/fauxton/app/app.js index 5325f77fe..521ad6c84 100644 --- a/src/fauxton/app/app.js +++ b/src/fauxton/app/app.js @@ -21,77 +21,38 @@ define([ "bootstrap", "helpers", - "utils", + "core/utils", // Modules - "resizeColumns", - - // Plugins. + "core/api", + "core/couchdbsession", + // Plugins. "plugins/backbone.layoutmanager", "plugins/jquery.form" ], -function(app, $, _, Backbone, Bootstrap, Helpers, Utils, resizeColumns) { - - // Make sure we have a console.log +function(app, $, _, Backbone, Bootstrap, Helpers, Utils, FauxtonAPI, Couchdb) { + // Make sure we have a console.log if (typeof console == "undefined") { console = { - log: function(){} + log: function(){}, + trace: function(){}, + debug: function(){} }; } // Provide a global location to place configuration settings and module // creation also mix in Backbone.Events - _.extend(app, Backbone.Events, { + _.extend(app, { utils: Utils, - - renderView: function(baseView, selector, view, options, callback) { - baseView.setView(selector, new view(options)).render().then(callback); - }, - - // Create a custom object with a nested Views object. - module: function(additionalProps) { - return _.extend({ Views: {} }, additionalProps); - }, - - // Thanks to: http://stackoverflow.com/a/2880929 - getParams: function(queryString) { - if (queryString) { - // I think this could be combined into one if - if (queryString.substring(0,1) === "?") { - queryString = queryString.substring(1); - } else if (queryString.indexOf('?') > -1) { - queryString = queryString.split('?')[1]; - } - } - var hash = window.location.hash.split('?')[1]; - queryString = queryString || hash || window.location.search.substring(1); - var match, - urlParams = {}, - pl = /\+/g, // Regex for replacing addition symbol with a space - search = /([^&=]+)=?([^&]*)/g, - decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, - query = queryString; - - if (queryString) { - while ((match = search.exec(query))) { - urlParams[decode(match[1])] = decode(match[2]); - } - } - - return urlParams; - } + getParams: FauxtonAPI.utils.getParams }); - //resizeAnimation - app.resizeColumns = new resizeColumns({}); - app.resizeColumns.onResizeHandler(); - // Localize or create a new JavaScript Template object. var JST = window.JST = window.JST || {}; // Configure LayoutManager with Backbone Boilerplate defaults. - Backbone.Layout.configure({ + FauxtonAPI.Layout.configure({ // Allow LayoutManager to augment Backbone.View.prototype. manage: true, @@ -123,6 +84,28 @@ function(app, $, _, Backbone, Bootstrap, Helpers, Utils, resizeColumns) { } }); + FauxtonAPI.setSession(new Couchdb.Session()); + + // Define your master router on the application namespace and trigger all + // navigation from this instance. + FauxtonAPI.config({ + el: "#app-container", + masterLayout: new FauxtonAPI.Layout(), + + addHeaderLink: function(link) { + FauxtonAPI.registerExtension('navbar:addHeaderLink', link); + }, + + removeHeaderLink: function(link) { + FauxtonAPI.removeExtensionItem('navbar:addHeaderLink', link, function (item) { + if (item.title === link.title) { + return true; + } + + return false; + }); + } + }); return app; }); diff --git a/src/fauxton/app/config.js b/src/fauxton/app/config.js index 057523bb0..98be9c6a8 100644 --- a/src/fauxton/app/config.js +++ b/src/fauxton/app/config.js @@ -25,7 +25,7 @@ require.config({ jquery: "../assets/js/libs/jquery", lodash: "../assets/js/libs/lodash", backbone: "../assets/js/libs/backbone", - "backbone.layoutmanger": "../assets/js/plugins/backbone.layoutmanager", + "backbone.layoutmanager": "../assets/js/plugins/backbone.layoutmanager", bootstrap: "../assets/js/libs/bootstrap", spin: "../assets/js/libs/spin.min", d3: "../assets/js/libs/d3", @@ -37,7 +37,8 @@ require.config({ map: { "*": { - 'underscore': 'lodash' + 'underscore': 'lodash', + 'api':'core/api' } }, diff --git a/src/fauxton/app/core/api.js b/src/fauxton/app/core/api.js new file mode 100644 index 000000000..1b21dca62 --- /dev/null +++ b/src/fauxton/app/core/api.js @@ -0,0 +1,53 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + "core/base", + "core/layout", + "core/router", + "core/routeObject", + "core/utils" +], + +function(FauxtonAPI, Layout, Router, RouteObject, utils) { + FauxtonAPI = _.extend(FauxtonAPI, { + Layout: Layout, + Router: Router, + RouteObject: RouteObject, + utils: utils + }); + + FauxtonAPI.navigate = function(url, _opts) { + var options = _.extend({trigger: true}, _opts ); + FauxtonAPI.router.navigate(url,options); + }; + + FauxtonAPI.beforeUnload = function () { + FauxtonAPI.router.beforeUnload.apply(FauxtonAPI.router, arguments); + }; + + FauxtonAPI.removeBeforeUnload = function () { + FauxtonAPI.router.removeBeforeUnload.apply(FauxtonAPI.router, arguments); + }; + + FauxtonAPI.addRoute = function(route) { + FauxtonAPI.router.route(route.route, route.name, route.callback); + }; + + FauxtonAPI.triggerRouteEvent = function (routeEvent, args) { + FauxtonAPI.router.triggerRouteEvent("route:"+routeEvent, args); + }; + + + return FauxtonAPI; +}); + diff --git a/src/fauxton/app/core/auth.js b/src/fauxton/app/core/auth.js new file mode 100644 index 000000000..15cf5667f --- /dev/null +++ b/src/fauxton/app/core/auth.js @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + "core/base", + "backbone" +], +function(FauxtonAPI, Backbone) { + + // This is not exposed externally as it should not need to be accessed or overridden + var Auth = function (options) { + this._options = options; + this.initialize.apply(this, arguments); + }; + + // Piggy-back on Backbone's self-propagating extend function, + Auth.extend = Backbone.Model.extend; + + _.extend(Auth.prototype, Backbone.Events, { + authDeniedCb: function() {}, + + initialize: function() { + var that = this; + }, + + authHandlerCb : function (roles) { + var deferred = $.Deferred(); + deferred.resolve(); + return deferred; + }, + + registerAuth: function (authHandlerCb) { + this.authHandlerCb = authHandlerCb; + }, + + registerAuthDenied: function (authDeniedCb) { + this.authDeniedCb = authDeniedCb; + }, + + checkAccess: function (roles) { + var requiredRoles = roles || [], + that = this; + + if (!FauxtonAPI.session) { + throw new Error("Fauxton.session is not configured."); + } + + return FauxtonAPI.session.fetchUser().then(function (user) { + return FauxtonAPI.when(that.authHandlerCb(FauxtonAPI.session, requiredRoles)); + }); + } + }); + +// FauxtonAPI.auth = new Auth(); + + return Auth; +}); + diff --git a/src/fauxton/app/core/base.js b/src/fauxton/app/core/base.js new file mode 100644 index 000000000..55a8d876d --- /dev/null +++ b/src/fauxton/app/core/base.js @@ -0,0 +1,137 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + "backbone" +], + +function(Backbone) { + var FauxtonAPI = { + //add default objects + router: { + navigate: function () {} + }, + + masterLayout: {}, + + addNotification: function () {}, + + config: function (options) { + return _.extend(this, options); + } + }; + + FauxtonAPI.Deferred = function() { + return $.Deferred(); + }; + + FauxtonAPI.when = function (deferreds) { + if (deferreds instanceof Array) { + return $.when.apply(null, deferreds); + } + + return $.when(deferreds); + }; + + FauxtonAPI.addonExtensions = { + initialize: function() {}, + RouteObjects: {}, + Views: {} + }; + + FauxtonAPI.addon = function(extra) { + return _.extend(_.clone(FauxtonAPI.addonExtensions), extra); + }; + + FauxtonAPI.View = Backbone.View.extend({ + // This should return an array of promises, an empty array, or null + establish: function() { + return null; + }, + + loaderClassname: 'loader', + + disableLoader: false, + + forceRender: function () { + this.hasRendered = false; + } + }); + + FauxtonAPI.Model = Backbone.Model.extend({ + fetchOnce: function (opt) { + var options = _.extend({}, opt); + + if (!this._deferred || this._deferred.state() === "rejected" || options.forceFetch ) { + this._deferred = this.fetch(); + } + + return this._deferred; + } + }); + + var extensions = _.extend({}, Backbone.Events); + // Can look at a remove function later. + FauxtonAPI.registerExtension = function (name, view) { + if (!extensions[name]) { + extensions[name] = []; + } + + extensions.trigger('add:' + name, view); + extensions[name].push(view); + }; + + FauxtonAPI.unRegisterExtension = function (name) { + var views = extensions[name]; + + if (!views) { return; } + extensions.trigger('remove:' + name, views); + delete extensions[name]; + }; + + FauxtonAPI.getExtensions = function (name) { + var views = extensions[name]; + + if (!views) { + views = []; + } + + return views; + }; + + FauxtonAPI.removeExtensionItem = function (name, view, cb) { + var views = extensions[name]; + if (!views) { return; } + + var _cb = arguments[arguments.length -1]; + if (_.isObject(view) && !cb) { + _cb = function (item) { return _.isEqual(item, view);}; + } + + views = _.filter(views, function (item) { + return !_cb(item); + }); + + extensions[name] = views; + extensions.trigger('removeItem:' + name, view); + }; + + FauxtonAPI.extensions = extensions; + + FauxtonAPI.setSession = function (newSession) { + FauxtonAPI.session = newSession; + return FauxtonAPI.session.fetchUser(); + }; + + return FauxtonAPI; +}); + diff --git a/src/fauxton/app/core/couchdbSession.js b/src/fauxton/app/core/couchdbSession.js new file mode 100644 index 000000000..93bfd8ac6 --- /dev/null +++ b/src/fauxton/app/core/couchdbSession.js @@ -0,0 +1,54 @@ +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + "core/base" +], +function (FauxtonAPI) { + var CouchdbSession = { + Session: FauxtonAPI.Model.extend({ + url: '/_session', + + user: function () { + var userCtx = this.get('userCtx'); + + if (!userCtx || !userCtx.name) { return null; } + + return { + name: userCtx.name, + roles: userCtx.roles + }; + }, + + fetchUser: function (opt) { + var that = this, + currentUser = this.user(); + + return this.fetchOnce(opt).then(function () { + var user = that.user(); + + // Notify anyone listening on these events that either a user has changed + // or current user is the same + if (currentUser !== user) { + that.trigger('session:userChanged'); + } else { + that.trigger('session:userFetched'); + } + + // this will return the user as a value to all function that calls done on this + // eg. session.fetchUser().done(user) { .. do something with user ..} + return user; + }); + } + }) + }; + + return CouchdbSession; +}); diff --git a/src/fauxton/app/addons/fauxton/layout.js b/src/fauxton/app/core/layout.js index 14222414b..ff339c72c 100644 --- a/src/fauxton/app/addons/fauxton/layout.js +++ b/src/fauxton/app/core/layout.js @@ -10,38 +10,26 @@ // License for the specific language governing permissions and limitations under // the License. -define(["backbone"], - -function(Backbone) { +define([ + "backbone", + "plugins/backbone.layoutmanager" +], function(Backbone) { // A wrapper of the main Backbone.layoutmanager // Allows the main layout of the page to be changed by any plugin. - // Exposes the different views: - // navBar -> the top navigation bar - // dashboardContent -> Main display view - // breadcrumbs -> Breadcrumbs navigation section - var Layout = function (navBar, apiBar) { - this.navBar = navBar; - this.apiBar = apiBar; - + var Layout = function () { this.layout = new Backbone.Layout({ template: "templates/layouts/with_sidebar", - - views: { - "#primary-navbar": this.navBar, - "#api-navbar": this.apiBar - }, - afterRender: function(){ - - } }); this.layoutViews = {}; - //this.hooks = {}; - 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 () { @@ -61,25 +49,14 @@ function(Backbone) { this.render(); }, - setTabs: function(view){ - // TODO: Not sure I like this - seems fragile/repetitive - this.tabs = this.layout.setView("#tabs", view); - this.tabs.render(); - }, - - setBreadcrumbs: function(view) { - this.breadcrumbs = this.layout.setView("#breadcrumbs", view); - this.breadcrumbs.render(); - }, - - clearBreadcrumbs: function () { - if (!this.breadcrumbs) {return ;} + setView: function(selector, view, keep) { + this.layout.setView(selector, view, false); - this.breadcrumbs.remove(); - }, + if (!keep) { + this.layoutViews[selector] = view; + } - setView: function(selector, view) { - this.layoutViews[selector] = this.layout.setView(selector, view, false); + return view; }, renderView: function(selector) { @@ -89,6 +66,22 @@ function(Backbone) { } 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; } }); diff --git a/src/fauxton/app/core/routeObject.js b/src/fauxton/app/core/routeObject.js new file mode 100644 index 000000000..f3b8672e0 --- /dev/null +++ b/src/fauxton/app/core/routeObject.js @@ -0,0 +1,296 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + "core/base", + "backbone" +], +function(FauxtonAPI, Backbone) { + + var RouteObject = function(options) { + this._options = options; + + this._configure(options || {}); + this.initialize.apply(this, arguments); + this.addEvents(); + }; + + var broadcaster = {}; + _.extend(broadcaster, Backbone.Events); + + RouteObject.on = function (eventName, fn) { + broadcaster.on(eventName, fn); + }; + + /* How Route Object events work + To listen to a specific route objects events: + + myRouteObject = FauxtonAPI.RouteObject.extend({ + events: { + "beforeRender": "beforeRenderEvent" + }, + + beforeRenderEvent: function (view, selector) { + console.log('Hey, beforeRenderEvent triggered',arguments); + }, + }); + + It is also possible to listen to events triggered from all Routeobjects. + This is great for more general things like adding loaders, hooks. + + FauxtonAPI.RouteObject.on('beforeRender', function (routeObject, view, selector) { + console.log('hey, this will trigger when any routeobject renders a view'); + }); + + Current Events to subscribe to: + * beforeFullRender -- before a full render is being done + * beforeEstablish -- before the routeobject calls establish + * AfterEstablish -- after the routeobject has run establish + * beforeRender -- before a view is rendered + * afterRender -- a view is finished being rendered + * renderComplete -- all rendering is complete + + */ + + // Piggy-back on Backbone's self-propagating extend function + RouteObject.extend = Backbone.Model.extend; + + var routeObjectOptions = ["views", "routes", "events", "roles", "crumbs", "layout", "apiUrl", "establish"]; + + _.extend(RouteObject.prototype, Backbone.Events, { + // Should these be default vals or empty funcs? + views: {}, + routes: {}, + events: {}, + crumbs: [], + layout: "with_sidebar", + apiUrl: null, + disableLoader: false, + loaderClassname: 'loader', + renderedState: false, + establish: function() {}, + route: function() {}, + roles: [], + _promises: [], + initialize: function() {} + }, { + + renderWith: function(route, masterLayout, args) { + //set the options for this render + var options = { + masterLayout: masterLayout, + route: route, + args: args + }; + + this.setTemplateOnFullRender(masterLayout); + + this.triggerBroadcast('beforeEstablish'); + + var renderAllViews = _.bind(this.renderAllViews, this, options), + establishError = _.bind(this.establishError, this), + renderComplete = _.bind(this.renderComplete, this), + promise = this.establish(); + + this.callEstablish(promise) + .then(renderAllViews, establishError) + .then(renderComplete); + }, + + setTemplateOnFullRender: function(masterLayout){ + // Only want to redo the template if its a full render + if (!this.renderedState) { + masterLayout.setTemplate(this.layout); + this.triggerBroadcast('beforeFullRender'); + } + }, + + callEstablish: function(establishPromise) { + this.addPromise(establishPromise); + return FauxtonAPI.when(establishPromise); + }, + + renderAllViews: function(options, resp){ + var routeObject = this, + renderView = _.bind(this.renderView, this, routeObject, options); + + this.triggerBroadcast('afterEstablish'); + + var promises = _.map(routeObject.getViews(), renderView, this); + return FauxtonAPI.when(promises); + }, + + renderView: function(routeObject, options, view, selector) { + var viewInfo = { + view: view, + selector: selector, + masterLayout: options.masterLayout + }; + + var renderViewOnLayout = _.bind(this.renderViewOnLayout, this, viewInfo); + + if(view.hasRendered) { + this.triggerBroadcast('viewHasRendered', view, selector); + return; + } + + this.triggerBroadcast('beforeRender', view, selector); + + return this.callEstablish(view.establish()).then(renderViewOnLayout, this.establishError); + }, + + renderViewOnLayout: function(viewInfo, resp, xhr){ + var masterLayout = viewInfo.masterLayout; + + masterLayout.setView(viewInfo.selector, viewInfo.view); + masterLayout.renderView(viewInfo.selector); + + this.triggerBroadcast('afterRender', viewInfo.view, viewInfo.selector); + }, + + establishError: function(resp){ + if (!resp || !resp.responseText) { return; } + FauxtonAPI.addNotification({ + msg: 'An Error occurred' + JSON.parse(resp.responseText).reason, + type: 'error', + clear: true + }); + }, + + renderComplete: function () { + // Track that we've done a full initial render + this.setRenderedState(true); + this.triggerBroadcast('renderComplete'); + }, + + setRenderedState: function(bool){ + this.renderedState = bool; + }, + + triggerBroadcast: function (eventName) { + var args = Array.prototype.slice.call(arguments); + this.trigger.apply(this, args); + + args.splice(0,1, eventName, this); + broadcaster.trigger.apply(broadcaster, args); + }, + + get: function(key) { + return _.isFunction(this[key]) ? this[key]() : this[key]; + }, + + addEvents: function(events) { + events = events || this.get('events'); + _.each(events, function(method, event) { + if (!_.isFunction(method) && !_.isFunction(this[method])) { + throw new Error("Invalid method: "+method); + } + method = _.isFunction(method) ? method : this[method]; + + this.on(event, method); + }, this); + }, + + _configure: function(options) { + _.each(_.intersection(_.keys(options), routeObjectOptions), function(key) { + this[key] = options[key]; + }, this); + }, + + getView: function(selector) { + return this.views[selector]; + }, + + setView: function(selector, view) { + this.views[selector] = view; + return view; + }, + + getViews: function() { + return this.views; + }, + + removeViews: function () { + _.each(this.views, function (view, selector) { + view.remove(); + delete this.views[selector]; + }, this); + }, + + addPromise: function (promise) { + if (_.isEmpty(promise)) { return; } + + if (!_.isArray(promise)) { + return this._promises.push(promise); + } + + _.each(promise, function (p) { + this._promises.push(p); + }, this); + }, + + cleanup: function () { + this.removeViews(); + this.rejectPromises(); + }, + + rejectPromises: function () { + _.each(this._promises, function (promise) { + if (promise.state() === "resolved") { return; } + if (promise.abort) { + return promise.abort("Route change"); + } + + promise.reject(); + }, this); + + this._promises = []; + }, + + getRouteUrls: function () { + return _.keys(this.get('routes')); + }, + + hasRoute: function (route) { + if (this.get('routes')[route]) { + return true; + } + return false; + }, + + routeCallback: function (route, args) { + var routes = this.get('routes'), + routeObj = routes[route], + routeCallback; + + if (typeof routeObj === 'object') { + routeCallback = this[routeObj.route]; + } else { + routeCallback = this[routeObj]; + } + + routeCallback.apply(this, args); + }, + + getRouteRoles: function (routeUrl) { + var route = this.get('routes')[routeUrl]; + + if ((typeof route === 'object') && route.roles) { + return route.roles; + } + + return this.roles; + } + + }); + return RouteObject; +}); diff --git a/src/fauxton/app/router.js b/src/fauxton/app/core/router.js index 89c60cf33..cc1ca4f64 100644 --- a/src/fauxton/app/router.js +++ b/src/fauxton/app/core/router.js @@ -11,32 +11,16 @@ // the License. define([ - // Load require for use in nested requiring - // as per the note in: http://requirejs.org/docs/api.html#multiversion - "require", - - // Application. - "app", - - // Initialize application - "initialize", - - // Load Fauxton API - "api", - - // Modules - "addons/fauxton/base", - // Layout - "addons/fauxton/layout", - - "load_addons" + "core/base", + "core/auth", + "backbone" ], -function(req, app, Initialize, FauxtonAPI, Fauxton, Layout, LoadAddons) { +function(FauxtonAPI, Auth, Backbone) { var beforeUnloads = {}; - var Router = app.router = Backbone.Router.extend({ + var Router = Backbone.Router.extend({ routes: {}, beforeUnload: function (name, fn) { @@ -64,14 +48,14 @@ function(req, app, Initialize, FauxtonAPI, Fauxton, Layout, LoadAddons) { addModuleRouteObject: function(RouteObject) { var that = this; - var masterLayout = this.masterLayout, + var masterLayout = FauxtonAPI.masterLayout, routeUrls = RouteObject.prototype.getRouteUrls(); _.each(routeUrls, function(route) { this.route(route, route.toString(), function() { var args = Array.prototype.slice.call(arguments), roles = RouteObject.prototype.getRouteRoles(route), - authPromise = app.auth.checkAccess(roles); + authPromise = FauxtonAPI.auth.checkAccess(roles); authPromise.then(function () { if (!that.activeRouteObject || !that.activeRouteObject.hasRoute(route)) { @@ -92,8 +76,8 @@ function(req, app, Initialize, FauxtonAPI, Fauxton, Layout, LoadAddons) { }, this); }, - setModuleRoutes: function() { - _.each(LoadAddons.addons, function(module) { + setModuleRoutes: function(addons) { + _.each(addons, function(module) { if (module){ module.initialize(); // This is pure routes the addon provides @@ -104,39 +88,26 @@ function(req, app, Initialize, FauxtonAPI, Fauxton, Layout, LoadAddons) { }, this); }, - initialize: function() { - //TODO: It would be nice to handle this with a router - this.navBar = app.navBar = new Fauxton.NavBar(); - this.apiBar = app.apiBar = new Fauxton.ApiBar(); - this.auth = app.auth = FauxtonAPI.auth; - app.session = FauxtonAPI.session; - - app.masterLayout = this.masterLayout = new Layout(this.navBar, this.apiBar); - app.footer = new Fauxton.Footer({el: "#footer-content"}); - + initialize: function(addons) { + this.addons = addons; + this.auth = FauxtonAPI.auth = new Auth(); // NOTE: This must be below creation of the layout // FauxtonAPI header links and others depend on existence of the layout - //this.setAddonHooks(); - this.setModuleRoutes(); - - $("#app-container").html(this.masterLayout.el); - this.masterLayout.render(); + this.setModuleRoutes(addons); - // TODO: move this to a proper Fauxton.View - $.when.apply(null, app.footer.establish()).done(function() { - app.footer.render(); - }); + $(FauxtonAPI.el).html(FauxtonAPI.masterLayout.el); + FauxtonAPI.masterLayout.render(); }, triggerRouteEvent: function(event, args) { if (this.activeRouteObject) { var eventArgs = [event].concat(args); this.activeRouteObject.trigger.apply(this.activeRouteObject, eventArgs ); - this.activeRouteObject.renderWith(eventArgs, this.masterLayout, args); + this.activeRouteObject.renderWith(eventArgs, FauxtonAPI.masterLayout, args); } } }); return Router; - }); + diff --git a/src/fauxton/test/core/layoutSpec.js b/src/fauxton/app/core/tests/layoutSpec.js index 4167100d7..b58966bc8 100644 --- a/src/fauxton/test/core/layoutSpec.js +++ b/src/fauxton/app/core/tests/layoutSpec.js @@ -10,18 +10,16 @@ // License for the specific language governing permissions and limitations under // the License. define([ - 'addons/fauxton/layout', - 'testUtils' -], function (Layout, testUtils) { + 'api', + 'testUtils' +], function (FauxtonAPI, testUtils) { var assert = testUtils.assert; describe("Faxuton Layout", function () { var layout; beforeEach(function () { - var navBar = new Backbone.View(); - var apiBar = new Backbone.View(); - layout = new Layout(navBar, apiBar); + layout = new FauxtonAPI.Layout(); }); describe('#setTemplate', function () { diff --git a/src/fauxton/test/core/routeObjectSpec.js b/src/fauxton/app/core/tests/routeObjectSpec.js index 987d5b7e4..2fca94d45 100644 --- a/src/fauxton/test/core/routeObjectSpec.js +++ b/src/fauxton/app/core/tests/routeObjectSpec.js @@ -57,16 +57,7 @@ define([ assert.ok(mockLayout.setTemplate.calledOnce, 'SetTemplate not meant to be called'); }); - it('Should clear breadcrumbs', function () { - testRouteObject.renderWith('the-route', mockLayout, 'args'); - assert.ok(mockLayout.clearBreadcrumbs.calledOnce, 'Clear Breadcrumbs called'); - }); - - it('Should set breadcrumbs when breadcrumbs exist', function () { - testRouteObject.renderWith('the-route', mockLayout, 'args'); - assert.ok(mockLayout.setBreadcrumbs.calledOnce, 'Set Breadcrumbs was called'); - }); - + it("Should call establish of routeObject", function () { var establishSpy = sinon.spy(testRouteObject,"establish"); diff --git a/src/fauxton/app/core/utils.js b/src/fauxton/app/core/utils.js new file mode 100644 index 000000000..44945e846 --- /dev/null +++ b/src/fauxton/app/core/utils.js @@ -0,0 +1,94 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + + +// This file creates a set of helper functions that will be loaded for all html +// templates. These functions should be self contained and not rely on any +// external dependencies as they are loaded prior to the application. We may +// want to change this later, but for now this should be thought of as a +// "purely functional" helper system. + + +define([ + "jquery", + "lodash" +], + +function($, _ ) { + + var onWindowResize = {}; + + var utils = { + // Thanks to: http://stackoverflow.com/a/2880929 + getParams: function(queryString) { + if (queryString) { + // I think this could be combined into one if + if (queryString.substring(0,1) === "?") { + queryString = queryString.substring(1); + } else if (queryString.indexOf('?') > -1) { + queryString = queryString.split('?')[1]; + } + } + var hash = window.location.hash.split('?')[1]; + queryString = queryString || hash || window.location.search.substring(1); + var match, + urlParams = {}, + pl = /\+/g, // Regex for replacing addition symbol with a space + search = /([^&=]+)=?([^&]*)/g, + decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, + query = queryString; + + if (queryString) { + while ((match = search.exec(query))) { + urlParams[decode(match[1])] = decode(match[2]); + } + } + + return urlParams; + }, + + addWindowResize: function(fun, key){ + onWindowResize[key]=fun; + // You shouldn't need to call it here. Just define it at startup and each time it will loop + // through all the functions in the hash. + //app.initWindowResize(); + }, + + removeWindowResize: function(key){ + delete onWindowResize[key]; + utils.initWindowResize(); + }, + + initWindowResize: function(){ + //when calling this it should be overriding what was called previously + window.onresize = function(e) { + // could do this instead of the above for loop + _.each(onWindowResize, function (fn) { + fn(); + }); + }; + }, + + removeSpecialCharacters: function(name){ + return name.replace(/[^\w\s]/gi,""); + }, + + safeURLName: function(name){ + var testName = name || ""; + var checkforBad = testName.match(/[\$\-/_,+-]/g); + return (checkforBad !== null)?encodeURIComponent(name):name; + } + }; + + return utils; +}); + diff --git a/src/fauxton/app/main.js b/src/fauxton/app/main.js index 6fe999102..9df15c5d9 100644 --- a/src/fauxton/app/main.js +++ b/src/fauxton/app/main.js @@ -13,19 +13,18 @@ require([ // Application. "app", - - // Main Router. - "router" + "api", + "load_addons" ], -function(app, Router) { +function(app, FauxtonAPI, LoadAddons) { - // Define your master router on the application namespace and trigger all - // navigation from this instance. - app.router = new Router(); + app.addons = LoadAddons.addons; + FauxtonAPI.router = app.router = new FauxtonAPI.Router(app.addons); // Trigger the initial route and enable HTML5 History API support, set the // root folder to '/' by default. Change in app.js. Backbone.history.start({ pushState: false, root: app.root }); + // All navigation that is relative should be passed through the navigate // method, to be processed by the router. If the link has a `data-bypass` // attribute, bypass the delegation completely. diff --git a/src/fauxton/app/utils.js b/src/fauxton/app/utils.js deleted file mode 100644 index ded7dace4..000000000 --- a/src/fauxton/app/utils.js +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - - -// This file creates a set of helper functions that will be loaded for all html -// templates. These functions should be self contained and not rely on any -// external dependencies as they are loaded prior to the application. We may -// want to change this later, but for now this should be thought of as a -// "purely functional" helper system. - - -define([ - "jquery", - "lodash" -], - -function($, _ ) { - - var utils = {}; - - var onWindowResize = {}; - - utils.addWindowResize = function(fun, key){ - onWindowResize[key]=fun; - // You shouldn't need to call it here. Just define it at startup and each time it will loop - // through all the functions in the hash. - //app.initWindowResize(); - }; - - utils.removeWindowResize = function(key){ - delete onWindowResize[key]; - utils.initWindowResize(); - }; - - utils.initWindowResize = function(){ - //when calling this it should be overriding what was called previously - window.onresize = function(e) { - // could do this instead of the above for loop - _.each(onWindowResize, function (fn) { - fn(); - }); - }; - }; - - utils.removeSpecialCharacters = function(name){ - return name.replace(/[^\w\s]/gi,""); - }; - - utils.safeURLName = function(name){ - var testName = name || ""; - var checkforBad = testName.match(/[\$\-/_,+-]/g); - return (checkforBad !== null)?encodeURIComponent(name):name; - }; - - return utils; -}); - diff --git a/src/fauxton/settings.json.default b/src/fauxton/settings.json.default index cb09eb2e1..e817b79a3 100644 --- a/src/fauxton/settings.json.default +++ b/src/fauxton/settings.json.default @@ -1,5 +1,6 @@ { "deps": [ + { "name": "fauxton" }, { "name": "databases" }, { "name": "documents" }, { "name": "pouchdb" }, diff --git a/src/fauxton/tasks/couchserver.js b/src/fauxton/tasks/couchserver.js index 5ccbfe11e..21c2fcb23 100644 --- a/src/fauxton/tasks/couchserver.js +++ b/src/fauxton/tasks/couchserver.js @@ -61,7 +61,7 @@ module.exports = function (grunt) { // server js from app directory filePath = path.join(app_dir, url.replace('/_utils/fauxton/','')); } else if (!!url.match(/testrunner/)) { - var testSetup = grunt.util.spawn({cmd: 'grunt', grunt: true, args: ['mochaSetup']}, function (error, result, code) {/* log.writeln(String(result));*/ }); + var testSetup = grunt.util.spawn({cmd: 'grunt', grunt: true, args: ['test_inline']}, function (error, result, code) {/* log.writeln(String(result));*/ }); testSetup.stdout.pipe(process.stdout); testSetup.stderr.pipe(process.stderr); filePath = path.join('./test/runner.html'); diff --git a/src/fauxton/test/mocha/testUtils.js b/src/fauxton/test/mocha/testUtils.js index f9643e8df..2c418f915 100644 --- a/src/fauxton/test/mocha/testUtils.js +++ b/src/fauxton/test/mocha/testUtils.js @@ -11,11 +11,12 @@ // the License. define([ + "api", "chai", "sinon-chai", "underscore" ], -function(chai, sinonChai) { +function(FauxtonAPI,chai, sinonChai) { chai.use(sinonChai); var ViewSandbox = function () { diff --git a/src/fauxton/test/test.config.underscore b/src/fauxton/test/test.config.underscore index dda16f275..5cebe7837 100644 --- a/src/fauxton/test/test.config.underscore +++ b/src/fauxton/test/test.config.underscore @@ -5,6 +5,7 @@ require.config( ); require([ + "app", <% _.each(testFiles, function (test) {%> '../<%= test %>', <% }) %> |