diff wikked/static/js/wikked.js @ 11:aa6951805e1a

New features and bug fixes: - Extracted navigation and footer parts into their own model/view. - Added search. - Better typography styles. - Fixed some bugs in the Handlebars helpers.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 29 Dec 2012 18:21:44 -0800
parents 6ac0b74a57f7
children 30ae685b86df
line wrap: on
line diff
--- a/wikked/static/js/wikked.js	Sat Dec 22 22:33:11 2012 -0800
+++ b/wikked/static/js/wikked.js	Sat Dec 29 18:21:44 2012 -0800
@@ -18,6 +18,7 @@
     loadedTemplates: {},
     get: function(name, callback) {
         if (name in this.loadedTemplates) {
+            console.log('Returning cached template "{0}".'.format(name));
             callback(this.loadedTemplates[name]);
         } else {
             var $loader = this;
@@ -37,6 +38,9 @@
  * Handlebars helper: reverse iterator.
  */
 Handlebars.registerHelper('eachr', function(context, options) {
+    if (context === undefined) {
+        return '';
+    }
     data = undefined;
     if (options.data) {
         data = Handlebars.createFrame(options.data);
@@ -62,6 +66,12 @@
     }
 	return options.inverse(this);
 });
+Handlebars.registerHelper('ifneq', function(context, options) {
+    if (context != options.hash.to) {
+        return options.fn(this);
+    }
+    return options.inverse(this);
+});
 
 //-------------------------------------------------------------//
 
@@ -103,8 +113,46 @@
     /**
      * Wiki page models.
      */
+    var NavigationModel = Backbone.Model.extend({
+        idAttribute: 'path',
+        defaults: function() {
+            return {
+                path: "main-page",
+                action: "read"
+            };
+        },
+        initialize: function() {
+            this.on('change:path', function(model, path) {
+                model._onChangePath(path);
+            });
+            this._onChangePath(this.get('path'));
+            return this;
+        },
+        _onChangePath: function(path) {
+            this.set('url_home', '/#/read/main-page');
+            this.set('url_read', '/#/read/' + path);
+            this.set('url_edit', '/#/edit/' + path);
+            this.set('url_hist', '/#/changes/' + path);
+            this.set('url_search', '/search');
+        }
+    });
+
+    var FooterModel = Backbone.Model.extend({
+        defaults: function() {
+            return {
+                url_extras: [ { name: 'Home', url: '/' } ]
+            };
+        },
+        addExtraUrl: function(name, url, index) {
+            if (index === undefined) {
+                this.get('url_extras').push({ name: name, url: url });
+            } else {
+                this.get('url_extras').splice(index, 0, { name: name, url: url });
+            }
+        }
+    });
+
     var PageModel = Backbone.Model.extend({
-        urlRoot: '/api/read/',
         idAttribute: 'path',
         defaults: function() {
             return {
@@ -120,6 +168,7 @@
             });
             this._onChangePath(this.get('path'));
             this._onChangeText('');
+            return this;
         },
         url: function() {
             var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
@@ -137,14 +186,6 @@
             return meta[key];
         },
         _onChangePath: function(path) {
-            this.set('url_home', '/#/read/main-page');
-            this.set('url_read', '/#/read/' + path);
-            this.set('url_edit', '/#/edit/' + path);
-            this.set('url_hist', '/#/changes/' + path);
-            this.set('url_rev', '/#/revision/' + path);
-            this.set('url_diffc', '/#/diff/c/' + path);
-            this.set('url_diffr', '/#/diff/r/' + path);
-            this.set('url_inlinks', '/#/inlinks/' + path);
         },
         _onChangeText: function(text) {
             this.set('content', new Handlebars.SafeString(text));
@@ -154,22 +195,59 @@
     var PageStateModel = PageModel.extend({
         urlRoot: '/api/state/'
     });
-
-    var PageSourceModel = PageModel.extend({
-        urlRoot: '/api/raw/'
+    
+    var MasterPageModel = PageModel.extend({
+        initialize: function() {
+            this.nav = new NavigationModel({ id: this.id });
+            this.footer = new FooterModel();
+            MasterPageModel.__super__.initialize.apply(this, arguments);
+            if (this.action !== undefined) {
+                this.nav.set('action', this.action);
+                this.footer.set('action', this.action);
+            }
+            return this;
+        },
+        _onChangePath: function(path) {
+            MasterPageModel.__super__._onChangePath.apply(this, arguments);
+            this.nav.set('path', path);
+        }
     });
 
-    var PageEditModel = PageModel.extend({
-        urlRoot: '/api/edit/'
+    var PageReadModel = MasterPageModel.extend({
+        urlRoot: '/api/read/',
+        action: 'read',
+        _onChangePath: function(path) {
+            PageReadModel.__super__._onChangePath.apply(this, arguments);
+            this.footer.addExtraUrl('Pages Linking Here', '/#/inlinks/' + path, 1);
+            this.footer.addExtraUrl('JSON', '/api/read/' + path);
+        }
+    });
+
+    var PageSourceModel = MasterPageModel.extend({
+        urlRoot: '/api/raw/',
+        action: 'source'
     });
 
-    var PageHistoryModel = PageModel.extend({
-        urlRoot: '/api/history/'
+    var PageEditModel = MasterPageModel.extend({
+        urlRoot: '/api/edit/',
+        action: 'edit'
     });
 
-    var PageRevisionModel = PageModel.extend({
+    var PageHistoryModel = MasterPageModel.extend({
+        urlRoot: '/api/history/',
+        action: 'history',
+        _onChangePath: function(path) {
+            PageHistoryModel.__super__._onChangePath.apply(this, arguments);
+            this.set('url_rev', '/#/revision/' + path);
+            this.set('url_diffc', '/#/diff/c/' + path);
+            this.set('url_diffr', '/#/diff/r/' + path);
+        }
+    });
+
+    var PageRevisionModel = MasterPageModel.extend({
         urlRoot: '/api/revision/',
         idAttribute: 'path_and_rev',
+        action: 'revision',
         defaults: function() {
             return {
                 path: "main-page",
@@ -177,6 +255,7 @@
             };
         },
         initialize: function() {
+            PageRevisionModel.__super__.initialize.apply(this, arguments);
             this.on('change:path', function(model, path) {
                 model._onChangePathOrRev(path, model.get('rev'));
             });
@@ -184,7 +263,7 @@
                 model._onChangePathOrRev(model.get('path'), rev);
             });
             this._onChangePathOrRev(this.get('path'), this.get('rev'));
-            PageRevisionModel.__super__.initialize.call(this);
+            return this;
         },
         _onChangePathOrRev: function(path, rev) {
             this.set('path_and_rev', path + '/' + rev);
@@ -195,9 +274,10 @@
         }
     });
 
-    var PageDiffModel = PageModel.extend({
+    var PageDiffModel = MasterPageModel.extend({
         urlRoot: '/api/diff/',
         idAttribute: 'path_and_revs',
+        action: 'diff',
         defaults: function() {
             return {
                 path: "main-page",
@@ -206,6 +286,7 @@
             };
         },
         initialize: function() {
+            PageDiffModel.__super__.initialize.apply(this, arguments);
             this.on('change:path', function(model, path) {
                 model._onChangePathOrRevs(path, model.get('rev'));
             });
@@ -216,7 +297,7 @@
                 model._onChangePathOrRevs(model.get('path'), model.get('rev1'), rev2);
             });
             this._onChangePathOrRevs(this.get('path'), this.get('rev1'), this.get('rev2'));
-            PageRevisionModel.__super__.initialize.call(this);
+            return this;
         },
         _onChangePathOrRevs: function(path, rev1, rev2) {
             this.set('path_and_revs', path + '/' + rev1 + '/' + rev2);
@@ -234,52 +315,130 @@
         }
     });
 
-    var IncomingLinksModel = PageModel.extend({
-        urlRoot: '/api/inlinks/'
+    var IncomingLinksModel = MasterPageModel.extend({
+        urlRoot: '/api/inlinks/',
+        action: 'inlinks'
+    });
+
+    var WikiSearchModel = MasterPageModel.extend({
+        urlRoot: '/api/search/',
+        action: 'search',
+        title: function() {
+            return 'Search';
+        },
+        execute: function(query) {
+            var $model = this;
+            $.getJSON('/api/search', { q: query })
+                .success(function (data) {
+                    $model.set('hits', data.hits);
+                })
+                .error(function() {
+                    alert("Error searching...");
+                });
+        }
     });
 
     /**
      * Wiki page views.
      */
-    var PageReadView = Backbone.View.extend({
-        tagName: "div",
+    var PageView = Backbone.View.extend({
+        tagName: 'div',
+        className: 'wrapper',
         initialize: function() {
+            PageView.__super__.initialize.apply(this, arguments);
             var $view = this;
-            var model = new PageModel({ path: this.id });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('read-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        $('a.wiki-link[data-wiki-url]').each(function(i, el) {
-                            var jel = $(el);
-                            if (jel.hasClass('missing'))
-                                jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url'));
-                            else
-                                jel.attr('href', '/#/read/' + jel.attr('data-wiki-url'));
-                        });
-                        document.title = model.title();
-                    });
-                },
-                error: function(model, xhr, options) {
-                    TemplateLoader.get('404', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                    });
+            this.model.on("change", function() { $view.render(); });
+            return this;
+        },
+        render: function(view) {
+            if (this.templateName !== undefined) {
+                this.renderTemplate(this.templateName, this.renderCallback);
+            }
+            this.renderTitle(this.titleFormat);
+            return this;
+        },
+        renderTemplate: function(tpl_name, callback) {
+            var $view = this;
+            TemplateLoader.get(tpl_name, function(src) {
+                var template = Handlebars.compile(src);
+                $view.$el.html(template($view.model.toJSON()));
+                if (callback !== undefined) {
+                    callback.call($view, $view, $view.model);
                 }
             });
+        },
+        renderTitle: function(formatter) {
+            var title = this.model.title();
+            if (formatter !== undefined) {
+                title = formatter.call(this, title);
+            }
+            document.title = title;
+        }
+    });
+    _.extend(PageView, Backbone.Events);
+
+    var NavigationView = PageView.extend({
+        templateName: 'nav',
+        initialize: function() {
+            NavigationView.__super__.initialize.apply(this, arguments);
+            this.render();
+            return this;
+        },
+        render: function() {
+            this.renderTemplate('nav');
+        },
+        postRender: function() {
+            var $view = this;
+            this.$('#search').submit(function() {
+                app.navigate('/search/' + $(this.q).val(), { trigger: true });
+                return false;
+            });
+        }
+    });
+
+    var FooterView = PageView.extend({
+        templateName:  'footer',
+        initialize: function() {
+            FooterView.__super__.initialize.apply(this, arguments);
+            this.render();
+            return this;
+        },
+        render: function() {
+            this.renderTemplate('footer');
+        },
+        postRender: function() {
+        }
+    });
+
+    var MasterPageView = PageView.extend({
+        initialize: function() {
+            MasterPageView.__super__.initialize.apply(this, arguments);
+            this.nav = new NavigationView({ model: this.model.nav });
+            this.footer = new FooterView({ model: this.model.footer });
+            this.render();
+            return this;
+        },
+        renderCallback: function(view, model) {
+            this.nav.$el.prependTo(this.$el);
+            this.nav.postRender();
+            this.footer.$el.appendTo(this.$el);
+            this.footer.postRender();
+        }
+    });
+
+    var PageReadView = MasterPageView.extend({
+        templateName: 'read-page',
+        initialize: function() {
+            PageReadView.__super__.initialize.apply(this, arguments);
             // Also get the current state, and show a warning
             // if the page is new or modified.
-            var stateModel = new PageStateModel({ id: this.id });
+            var stateModel = new PageStateModel({ path: this.model.get('path') });
             stateModel.fetch({
                 success: function(model, response, options) {
                     if (model.get('state') == 'new' || model.get('state') == 'modified') {
                         TemplateLoader.get('state-warning', function(src) {
-                            var template_data = model.toJSON();
                             var template = Handlebars.compile(src);
-                            var warning = $(template(template_data));
+                            var warning = $(template(model.toJSON()));
                             warning.css('display', 'none');
                             warning.prependTo($('#app'));
                             warning.slideDown();
@@ -292,34 +451,35 @@
                 }
             });
             return this;
+        },
+        renderCallback: function(view, model) {
+            PageReadView.__super__.renderCallback.apply(this, arguments);
+            // Replace all wiki links with proper hyperlinks using the JS app's
+            // URL scheme.
+            this.$('a.wiki-link[data-wiki-url]').each(function(i) {
+                var jel = $(this);
+                if (jel.hasClass('missing'))
+                    jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url'));
+                else
+                    jel.attr('href', '/#/read/' + jel.attr('data-wiki-url'));
+            });
         }
     });
 
-    var PageEditView = Backbone.View.extend({
-        initialize: function() {
-            var $view = this;
-            var model = new PageEditModel({ path: this.id });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('edit-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        document.title = 'Editing: ' + model.title();
-
-                        $('#page-edit').submit(function() {
-                            $view._submitText(this, model.get('path'));
-                            return false;
-                        });
-                    });
-                },
-                error: function(model, xhr, options) {
-                }
+    var PageEditView = MasterPageView.extend({
+        templateName: 'edit-page',
+        renderCallback: function(view, model) {
+            PageEditView.__super__.renderCallback.apply(this, arguments);
+            this.$('#page-edit').submit(function() {
+                view._submitText(this, model.get('path'));
+                return false;
             });
-            return this;
+        },
+        titleFormat: function(title) {
+            return 'Editing: ' + title;
         },
         _submitText: function(form, path) {
-            $.post('/api/edit/' + path, $(form).serialize())
+            $.post('/api/edit/' + path, this.$(form).serialize())
                 .success(function(data) {
                     app.navigate('/read/' + path, { trigger: true });
                 })
@@ -329,28 +489,17 @@
         }
     });
 
-    var PageHistoryView = Backbone.View.extend({
-        initialize: function() {
-            var $view = this;
-            var model = new PageHistoryModel({ path: this.id });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('history-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        document.title = 'Changes: ' + model.title();
-
-                        $('#diff-page').submit(function() {
-                            $view._triggerDiff(this, model.get('path'));
-                            return false;
-                        });
-                    });
-                },
-                error: function() {
-                }
+    var PageHistoryView = MasterPageView.extend({
+        templateName: 'history-page',
+        renderCallback: function(view, model) {
+            PageHistoryView.__super__.renderCallback.apply(this, arguments);
+            this.$('#diff-page').submit(function() {
+                view._triggerDiff(this, model.get('path'));
+                return false;
             });
-            return this;
+        },
+        titleFormat: function(title) {
+            return 'History: ' + title;
         },
         _triggerDiff: function(form, path) {
             var rev1 = $('input[name=rev1]:checked', form).val();
@@ -359,60 +508,31 @@
         }
     });
 
-    var PageRevisionView = Backbone.View.extend({
-        initialize: function() {
-            var $view = this;
-            var model = new PageRevisionModel({ path: this.id, rev: this.options.rev });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('revision-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        document.title = model.title() + ' [' + model.get('rev') + ']';
-                    });
-                }
-            });
+    var PageRevisionView = MasterPageView.extend({
+        templateName: 'revision-page',
+        titleFormat: function(title) {
+            return title + ' [' + this.model.get('rev') + ']';
         }
     });
 
-    var PageDiffView = Backbone.View.extend({
-        initialize: function() {
-            var $view = this;
-            var model = new PageDiffModel({ path: this.id, rev1: this.options.rev1, rev2: this.options.rev2 });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('diff-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        document.title = model.title() + ' [' + model.get('rev1') + '-' + model.get('rev2') + ']';
-                    });
-                }
-            });
+    var PageDiffView = MasterPageView.extend({
+        templateName: 'diff-page',
+        titleFormat: function(title) {
+            return title + ' [' + this.model.get('rev1') + '-' + this.model.get('rev2') + ']';
         }
     });
 
-    var IncomingLinksView = Backbone.View.extend({
-        initialize: function() {
-            var $view = this;
-            var model = new IncomingLinksModel({ path: this.id });
-            model.fetch({
-                success: function(model, response, options) {
-                    TemplateLoader.get('inlinks-page', function(src) {
-                        var template_data = model.toJSON();
-                        var template = Handlebars.compile(src);
-                        $view.$el.html(template(template_data));
-                        document.title = 'Incoming Links: ' + model.title();
-                    });
-                },
-                error: function() {
-                }
-            });
-            return this;
+    var IncomingLinksView = MasterPageView.extend({
+        templateName: 'inlinks-page',
+        titleFormat: function(title) {
+            return 'Incoming Links: ' + title;
         }
     });
 
+    var WikiSearchView = MasterPageView.extend({
+        templateName: 'search-results'
+    });
+
     /**
      * Main URL router.
      */
@@ -425,38 +545,93 @@
             'inlinks/*path':        "showIncomingLinks",
             'revision/*path/:rev':  "readPageRevision",
             'diff/c/*path/:rev':    "showDiffWithPrevious",
-            'diff/r/*path/:rev1/:rev2':"showDiff"
+            'diff/r/*path/:rev1/:rev2':"showDiff",
+            'search/:query':         "showSearchResults"
         },
         readPage: function(path) {
-            var page_view = new PageReadView({ id: path, el: $('#app') });
+            var view = new PageReadView({ 
+                el: $('#app'), 
+                model: new PageReadModel({ path: path })
+            });
+            view.model.fetch();
             this.navigate('/read/' + path);
         },
         readMainPage: function() {
             this.readPage('main-page');
         },
         editPage: function(path) {
-            var edit_view = new PageEditView({ id: path, el: $('#app') });
+            var view = new PageEditView({ 
+                el: $('#app'),
+                model: new PageEditModel({ path: path })
+            });
+            view.model.fetch();
             this.navigate('/edit/' + path);
         },
         showPageHistory: function(path) {
-            var changes_view = new PageHistoryView({ id: path, el: $('#app') });
+            var view = new PageHistoryView({
+                el: $('#app'),
+                model: new PageHistoryModel({ path: path })
+            });
+            view.model.fetch();
             this.navigate('/changes/' + path);
         },
         showIncomingLinks: function(path) {
-            var in_view = new IncomingLinksView({ id: path, el: $('#app') });
+            var view = new IncomingLinksView({
+                el: $('#app'),
+                model: new IncomingLinksModel({ path: path })
+            });
+            view.model.fetch();
             this.navigate('/inlinks/' + path);
         },
         readPageRevision: function(path, rev) {
-            var rev_view = new PageRevisionView({ id: path, rev: rev, el: $('#app') });
+            var view = new PageRevisionView({
+                el: $('#app'),
+                rev: rev, 
+                model: new PageRevisionModel({ path: path, rev: rev })
+            });
+            view.model.fetch();
             this.navigate('/revision/' + path + '/' + rev);
         },
         showDiffWithPrevious: function(path, rev) {
-            var diff_view = new PageDiffView({ id: path, rev1: rev, el: $('#app') });
+            var view = new PageDiffView({
+                el: $('#app'),
+                rev1: rev,
+                model: new PageDiffModel({ path: path, rev1: rev })
+            });
+            view.model.fetch();
             this.navigate('/diff/c/' + path + '/' + rev);
         },
         showDiff: function(path, rev1, rev2) {
-            var diff_view = new PageDiffView({ id: path, rev1: rev1, rev2: rev2, el: $('#app') });
+            var view = new PageDiffView({
+                el: $('#app'),
+                rev1: rev1,
+                rev2: rev2,
+                model: new PageDiffModel({ path: path, rev1: rev1, rev2: rev2 })
+            });
+            view.model.fetch();
             this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2);
+        },
+        showSearchResults: function(query) {
+            if (query === '') {
+                query = this.getQueryVariable('q');
+            }
+            var view = new WikiSearchView({
+                el: $('#app'),
+                model: new WikiSearchModel()
+            });
+            view.model.execute(query);
+            this.navigate('/search/' + query);
+        },
+        getQueryVariable: function(variable) {
+            var query = window.location.search.substring(1);
+            var vars = query.split("&");
+            for (var i = 0; i < vars.length; i++) {
+                var pair = vars[i].split("=");
+                if (pair[0] == variable) {
+                    return unescape(pair[1]);
+                }
+            }
+            return false;
         }
     });