changeset 69:0adac3bc079e

Client refactoring: - Correctly clean-up Backbone views. - Remove some coupling to the router. - Remove duplication for view hosts. - Added ability to revert a page to a previous revision.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 12 Feb 2013 20:55:13 -0800
parents 4cb946982fca
children acc615617fdf
files static/js/wikked/app.js static/js/wikked/models.js static/js/wikked/view-manager.js static/js/wikked/views.js static/tpl/revision-page.html
diffstat 5 files changed, 157 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/static/js/wikked/app.js	Tue Feb 12 20:52:58 2013 -0800
+++ b/static/js/wikked/app.js	Tue Feb 12 20:55:13 2013 -0800
@@ -10,10 +10,55 @@
         ],
     function($, _, Backbone, Views, Models) {
 
+    var exports = {};
+
+    /**
+     * View manager.
+     */
+    var ViewManager = exports.ViewManager = function(el) {
+        this.initialize.apply(this, arguments);
+    };
+    _.extend(ViewManager.prototype, {
+        initialize: function(el) {
+            this.el = el;
+        },
+        _currentView: false,
+        switchView: function(view, autoFetch) {
+            if (this._currentView) {
+                this._currentView.dispose();
+                this._currentView = false;
+            }
+
+            if (view) {
+                this._currentView = view;
+                this.el.html(view.el);
+                if (autoFetch || autoFetch === undefined) {
+                    view.model.fetch();
+                }
+            }
+
+            return this;
+        }
+    });
+
     /**
      * Main router.
      */
     var AppRouter = Backbone.Router.extend({
+        initialize: function(options) {
+            this.viewManager = options ? options.viewManager : undefined;
+            if (!this.viewManager) {
+                this.viewManager = new ViewManager($('#app'));
+            }
+
+            var $router = this;
+            Backbone.View.prototype.navigate = function(url, options) {
+                $router.navigate(url, options);
+            };
+            Backbone.Model.prototype.navigate = function(url, options) {
+                $router.navigate(url, options);
+            };
+        },
         routes: {
             'read/*path':           "readPage",
             '':                     "readMainPage",
@@ -33,14 +78,12 @@
             path_clean = this.stripQuery(path);
             no_redirect = this.getQueryVariable('no_redirect', path);
             var view = new Views.PageReadView({
-                el: $('#app'),
                 model: new Models.PageReadModel({ path: path_clean })
             });
             if (no_redirect) {
                 view.model.set('no_redirect', true);
             }
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/read/' + path);
         },
         readMainPage: function() {
@@ -48,60 +91,48 @@
         },
         editPage: function(path) {
             var view = new Views.PageEditView({
-                el: $('#app'),
                 model: new Models.PageEditModel({ path: path })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/edit/' + path);
         },
         showPageHistory: function(path) {
             var view = new Views.PageHistoryView({
-                el: $('#app'),
                 model: new Models.PageHistoryModel({ path: path })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/changes/' + path);
         },
         showIncomingLinks: function(path) {
             var view = new Views.IncomingLinksView({
-                el: $('#app'),
                 model: new Models.IncomingLinksModel({ path: path })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/inlinks/' + path);
         },
         readPageRevision: function(path, rev) {
             var view = new Views.PageRevisionView({
-                el: $('#app'),
                 rev: rev,
                 model: new Models.PageRevisionModel({ path: path, rev: rev })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/revision/' + path + '/' + rev);
         },
         showDiffWithPrevious: function(path, rev) {
             var view = new Views.PageDiffView({
-                el: $('#app'),
                 rev1: rev,
                 model: new Models.PageDiffModel({ path: path, rev1: rev })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/diff/c/' + path + '/' + rev);
         },
         showDiff: function(path, rev1, rev2) {
             var view = new Views.PageDiffView({
-                el: $('#app'),
                 rev1: rev1,
                 rev2: rev2,
                 model: new Models.PageDiffModel({ path: path, rev1: rev1, rev2: rev2 })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2);
         },
         showSearchResults: function(query) {
@@ -109,20 +140,16 @@
                 query = this.getQueryVariable('q');
             }
             var view = new Views.WikiSearchView({
-                el: $('#app'),
                 model: new Models.WikiSearchModel()
             });
-            view.model.setApp(this);
-            view.model.execute(query);
+            this.viewManager.switchView(view);
             this.navigate('/search/' + query);
         },
         showLogin: function() {
             var view = new Views.LoginView({
-                el: $('#app'),
                 model: new Models.LoginModel()
             });
-            view.model.setApp(this);
-            view.render();
+            this.viewManager.switchView(view);
             this.navigate('/login');
         },
         doLogout: function() {
@@ -137,11 +164,9 @@
         },
         showSpecialPages: function() {
             var view = new Views.SpecialPagesView({
-                el: $('#app'),
                 model: new Models.SpecialPagesModel()
             });
-            view.model.setApp(this);
-            view.render();
+            this.viewManager.switchView(view);
             this.navigate('/special');
         },
         showSpecialPage: function(page) {
@@ -159,11 +184,9 @@
                 return;
             }
             var view = new viewType({
-                el: $('#app'),
                 model: new Models.GenericSpecialPageModel({ page: page })
             });
-            view.model.setApp(this);
-            view.model.fetch();
+            this.viewManager.switchView(view);
             this.navigate('/special/' + page);
         },
         stripQuery: function(url) {
--- a/static/js/wikked/models.js	Tue Feb 12 20:52:58 2013 -0800
+++ b/static/js/wikked/models.js	Tue Feb 12 20:55:13 2013 -0800
@@ -48,7 +48,7 @@
                 });
         },
         doSearch: function(form) {
-            this.app.navigate('/search/' + $(form.q).val(), { trigger: true });
+            this.navigate('/search/' + $(form.q).val(), { trigger: true });
         },
         _onChangePath: function(path) {
             this.set({
@@ -81,7 +81,7 @@
         defaults: function() {
             return {
                 url_extras: [
-                    { name: 'Home', url: '/' },
+                    { name: 'Home', url: '/#/' },
                     { name: 'Special Pages', url: '/#/special' }
                 ]
             };
@@ -106,7 +106,7 @@
             var $model = this;
             $.post('/api/user/login', $(form).serialize())
                 .success(function() {
-                    $model.app.navigate('/', { trigger: true });
+                    $model.navigate('/', { trigger: true });
                 })
                 .error(function() {
                     $model.set('has_error', true);
@@ -227,16 +227,17 @@
             this.footer.addExtraUrl('JSON', function() { return '/api/read/' + model.id; });
         },
         _onChange: function() {
-            // Handle redirects.
-            if (this.getMeta('redirect') && !this.get('no_redirect')) {
+            if (this.getMeta('redirect') && 
+                !this.get('no_redirect') &&
+                !this.get('redirected_from')) {
+                // Handle redirects.
                 var oldPath = this.get('path');
-                this.set('path', this.getMeta('redirect'));
-                this.fetch({
-                    success: function(model) {
-                        model.set('redirected_from', oldPath);
-                    }
+                this.set({
+                    'path': this.getMeta('redirect'),
+                    'redirected_from': oldPath
                 });
-                this.app.navigate('/read/' + this.getMeta('redirect'), { replace: true });
+                this.fetch();
+                this.navigate('/read/' + this.getMeta('redirect'), { replace: true, trigger: false });
             }
         }
     });
@@ -252,9 +253,10 @@
         doEdit: function(form) {
             var $model = this;
             var path = this.get('path');
+            this.navigate('/read/' + path, { trigger: true });
             $.post('/api/edit/' + path, $(form).serialize())
                 .success(function(data) {
-                    $model.app.navigate('/read/' + path, { trigger: true });
+                    $model.navigate('/read/' + path, { trigger: true });
                 })
                 .error(function() {
                     alert('Error saving page...');
@@ -268,7 +270,7 @@
         doDiff: function(form) {
             var rev1 = $('input[name=rev1]:checked', form).val();
             var rev2 = $('input[name=rev2]:checked', form).val();
-            this.app.navigate('/diff/r/' + this.get('path') + '/' + rev1 + '/' + rev2, { trigger: true });
+            this.navigate('/diff/r/' + this.get('path') + '/' + rev1 + '/' + rev2, { trigger: true });
         },
         _onChangePath: function(path) {
             PageHistoryModel.__super__._onChangePath.apply(this, arguments);
@@ -299,6 +301,17 @@
             this._onChangeRev(this.get('rev'));
             return this;
         },
+        doRevert: function(form) {
+            var $model = this;
+            var path = this.get('path');
+            $.post('/api/revert/' + path, $(form).serialize())
+                .success(function(data) {
+                    $model.navigate('/read/' + path, { trigger: true });
+                })
+                .error(function() {
+                    alert('Error reverting page...');
+                });
+        },
         _onChangeRev: function(rev) {
             var setmap = { disp_rev: rev };
             if (rev.match(/[a-f0-9]{40}/)) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/wikked/view-manager.js	Tue Feb 12 20:55:13 2013 -0800
@@ -0,0 +1,22 @@
+/**
+ * An object responsible for opening, switching, and closing
+ * down view.
+ */
+define([
+        ],
+    function() {
+
+    var ViewManager = {
+        _currentView: false,
+        switchView: function(view, autoFetch) {
+            if (this._currentView) {
+                this._currentView.remove();
+                this._currentView = false;
+            }
+
+            if (view) {
+                this._currentView = view;
+            }
+        }
+    };
+});
--- a/static/js/wikked/views.js	Tue Feb 12 20:52:58 2013 -0800
+++ b/static/js/wikked/views.js	Tue Feb 12 20:55:13 2013 -0800
@@ -50,6 +50,16 @@
             }
             return this;
         },
+        dispose: function() {
+            this.remove();
+            this.unbind();
+            if (this.model) {
+                this.model.unbind();
+            }
+            if (this._onDispose) {
+                this._onDispose();
+            }
+        },
         render: function(view) {
             if (this.template !== undefined) {
                 this.renderTemplate(this.template);
@@ -214,8 +224,29 @@
                 else
                     jel.attr('href', '/#/read/' + jel.attr('data-wiki-url'));
             });
+            // If we've already rendered the content, and we need to display
+            // a warning, do so now.
+            if (this.model.get('content')) {
+                this._showPageStateWarning();
+            }
         },
-        events: {
+        _showPageStateWarning: function() {
+            if (this._pageState === undefined)
+                return;
+
+            var state = this._pageState.get('state');
+            if (state == 'new' || state == 'modified') {
+                var warning = $(this.warningTemplate(this._pageState.toJSON()));
+                warning.css('display', 'none');
+                warning.prependTo($('#app .page'));
+                warning.slideDown();
+                $('.dismiss', warning).click(function() {
+                    warning.slideUp();
+                    return false;
+                });
+            }
+        },
+        /*events: {
             "click .wiki-link": "_navigateLink"
         },
         _navigateLink: function(e) {
@@ -225,21 +256,17 @@
             this.model.fetch();
             e.preventDefault();
             return false;
-        },
+        },*/
         _checkPageState: function() {
-            var stateTpl = this.warningTemplate;
+            var $view = this;
             var stateModel = new Models.PageStateModel({ path: this.model.get('path') });
             stateModel.fetch({
                 success: function(model, response, options) {
-                    if (model.get('state') == 'new' || model.get('state') == 'modified') {
-                        var warning = $(stateTpl(model.toJSON()));
-                        warning.css('display', 'none');
-                        warning.prependTo($('#app .page'));
-                        warning.slideDown();
-                        $('.dismiss', warning).click(function() {
-                            warning.slideUp();
-                            return false;
-                        });
+                    $view._pageState = model;
+                    // If we've already rendered the content, display
+                    // the warning, if any, now.
+                    if ($view.model && $view.model.get('content')) {
+                        $view._showPageStateWarning();
                     }
                 }
             });
@@ -328,7 +355,7 @@
         },
         _submitDiffPage: function(e) {
             e.preventDefault();
-            this.model.doDiff(this);
+            this.model.doDiff(e.currentTarget);
             return false;
         },
         titleFormat: function(title) {
@@ -340,6 +367,14 @@
         defaultTemplateSource: tplRevisionPage,
         titleFormat: function(title) {
             return title + ' [' + this.model.get('rev') + ']';
+        },
+        events: {
+            "submit #page-revert": "_submitPageRevert"
+        },
+        _submitPageRevert: function(e) {
+            e.preventDefault();
+            this.model.doRevert(e.currentTarget);
+            return false;
         }
     });
 
--- a/static/tpl/revision-page.html	Tue Feb 12 20:52:58 2013 -0800
+++ b/static/tpl/revision-page.html	Tue Feb 12 20:55:13 2013 -0800
@@ -2,5 +2,10 @@
     <div class="page span12">
         <h1>{{meta.title}} <span class="decorator">Revision: <span class="rev_id">{{disp_rev}}</span></span></h1>
         <pre><code>{{text}}</code></pre>
+        <form id="page-revert" class="page-revert">
+            <input type="hidden" name="rev" value="{{rev}}"/>
+            <button type="submit" class="btn">Revert</button>
+            <small>Revert the page to this revision</small>
+        </form>
     </div>
 </article>