Mercurial > wikked
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; } });