Implementing backbone for the blog / content view

closes #64 - adds a full example of using backbone on the frontend
remembered to squash this one!
This commit is contained in:
ErisDS 2013-05-31 06:58:20 +01:00
parent 33c28a88e8
commit 185eee2a6b
15 changed files with 3016 additions and 75 deletions

4
app.js
View File

@ -83,8 +83,8 @@
* Expose the standard locals that every external page should have available;
* path, navItems and ghostGlobals
*/
ghostLocals = function(req, res, next) {
ghost.doFilter('ghostNavItems', {path: req.path, navItems: []}, function(navData) {
ghostLocals = function (req, res, next) {
ghost.doFilter('ghostNavItems', {path: req.path, navItems: []}, function (navData) {
// Make sure we have a locals value.
res.locals = res.locals || {};

View File

@ -43,24 +43,6 @@
$('body').toggleClass('fullscreen');
});
$('.content-list-content li').on('click', function (e) {
var $target = $(e.target).closest('li'),
$preview = $('.content-preview');
$('.content-list-content li').removeClass('active');
$target.addClass('active');
// *****
// this means a *lot* of extra gumpf is in the DOM and should really be done with AJAX when we have proper
// data API endpoints
// ideally, we need a way to bind data to views properly... backbone marionette, angular, etc
// *****
//
/**
* @todo Remove gumpf
*/
$preview.find('.content-preview-content .wrapper').html($target.data('content'));
$preview.find('.post-controls .post-edit').attr('href', '/ghost/editor/' + $target.data('id'));
});
$('.options.up').on('click', function (e) {
e.stopPropagation();
$(this).next("ul").fadeToggle(200);

View File

@ -1,42 +0,0 @@
/*global window, history, jQuery, Showdown, CodeMirror */
(function ($) {
"use strict";
$(document).ready(function () {
// Shadow on Markdown if scrolled
$('.content-list-content').on('scroll', function (e) {
if ($('.content-list-content').scrollTop() > 10) {
$('.content-list').addClass('scrolling');
} else {
$('.content-list').removeClass('scrolling');
}
});
// Shadow on Preview if scrolled
$('.content-preview-content').on('scroll', function (e) {
if ($('.content-preview-content').scrollTop() > 10) {
$('.content-preview').addClass('scrolling');
} else {
$('.content-preview').removeClass('scrolling');
}
});
$('.post-controls .delete').on('click', function (e) {
e.preventDefault();
var postID = $('.content-list-content').find('li.active').data('id');
$.ajax({
method: 'DELETE',
url: '/api/v0.1/posts/' + postID,
success: function (res) {
window.location.reload();
},
error: function () {
window.alert('Delete failed.');
}
});
});
});
}(jQuery));

View File

@ -0,0 +1,20 @@
/*globals window, Backbone */
(function ($) {
"use strict";
var Ghost = {
Layout : {},
View : {},
Collection : {},
Model : {},
settings: {
baseUrl: '/api/v0.1'
},
currentView: null
};
window.Ghost = Ghost;
}());

View File

@ -0,0 +1,17 @@
/*global window, document, Ghost, $, Backbone, _ */
(function () {
"use strict";
Ghost.Model.Post = Backbone.Model.extend({
urlRoot: '/api/v0.1/posts/',
defaults: {
status: 'draft'
}
});
Ghost.Collection.Posts = Backbone.Collection.extend({
url: Ghost.settings.baseURL + '/posts',
model: Ghost.Model.Post
});
}());

View File

@ -0,0 +1,105 @@
/*global window, document, Ghost, Backbone, $, _ */
(function () {
"use strict";
// Base view
// ----------
Ghost.Layout.Blog = Backbone.Layout.extend({
initialize: function (options) {
this.addViews({
list : new Ghost.View.ContentList({ el: '.content-list' }),
preview : new Ghost.View.ContentPreview({ el: '.content-preview' })
});
// TODO: render templates on the client
// this.render()
}
});
// Add shadow during scrolling
var scrollShadow = function (target, e) {
if ($(e.currentTarget).scrollTop() > 10) {
$(target).addClass('scrolling');
} else {
$(target).removeClass('scrolling');
}
};
// Content list (sidebar)
// -----------------------
Ghost.View.ContentList = Backbone.View.extend({
initialize: function (options) {
this.$('.content-list-content').on('scroll', _.bind(scrollShadow, null, '.content-list'));
// Select first item
_.defer(function (el) {
el.find('.content-list-content li:first').trigger('click');
}, this.$el);
},
events: {
'click .content-list-content' : 'scrollHandler',
'click .content-list-content li' : 'showPreview'
},
showPreview: function (e) {
var item = $(e.currentTarget);
this.$('.content-list-content li').removeClass('active');
item.addClass('active');
Backbone.trigger("blog:showPreview", item.data('id'));
}
});
// Content preview
// ----------------
Ghost.View.ContentPreview = Backbone.View.extend({
initialize: function (options) {
this.listenTo(Backbone, "blog:showPreview", this.showPost);
this.$('.content-preview-content').on('scroll', _.bind(scrollShadow, null, '.content-preview'));
},
events: {
'click .post-controls .delete' : 'deletePost',
'click .post-controls .post-edit' : 'editPost'
},
deletePost: function (e) {
e.preventDefault();
this.model.destroy({
success: function (model) {
// here the ContentList would pick up the change in the Posts collection automatically
// after client-side rendering is implemented
var item = $('.content-list-content li[data-id=' + model.get('id') + ']');
item.next().add(item.prev()).eq(0).trigger('click');
item.remove();
},
error: function () {
// TODO: decent error handling
console.error('Error');
}
});
},
editPost: function (e) {
e.preventDefault();
// for now this will disable "open in new tab", but when we have a Router implemented
// it can go back to being a normal link to '#/ghost/editor/X'
window.location = '/ghost/editor/' + this.model.get('id');
},
showPost: function (id) {
this.model = new Ghost.Model.Post({ id: id });
this.model.once('change', this.render, this);
this.model.fetch();
},
render: function () {
this.$('.wrapper').html(this.model.get('content_html'));
}
});
// Initialize views.
// TODO: move to a `Backbone.Router`
Ghost.currentView = new Ghost.Layout.Blog({ el: '#main' });
}());

View File

@ -0,0 +1,56 @@
// Layout manager
// --------------
/*
* .addChild('sidebar', App.View.Sidebar)
* .childViews.sidebar.$('blah')
*/
Backbone.Layout = Backbone.View.extend({
// default to loading state, reverted on render()
loading: true,
addViews: function (views) {
if (!this.views) this.views = {}
_.each(views, function(view, name){
if (typeof view.model === 'undefined'){
view.model = this.model
}
this.views[name] = view
}, this)
return this
},
renderViews: function (data) {
_.invoke(this.views, 'render', data)
this.trigger('render')
return this
},
appendViews: function (target) {
_.each(this.views, function(view){
this.$el.append(view.el)
}, this)
this.trigger('append')
return this
},
destroyViews: function () {
_.each(this.views, function(view){
view.model = null
view.remove()
})
return this
},
render: function () {
this.loading = false
this.renderViews()
return this
},
remove: function () {
this.destroyViews()
Backbone.View.prototype.remove.call(this)
}
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@
}
};
ghost.doFilter('messWithAdmin', adminNavbar, function() {
ghost.doFilter('messWithAdmin', adminNavbar, function () {
console.log('the dofilter hook called in /core/admin/controllers/index.js');
});

View File

@ -1,7 +1,3 @@
{{#contentFor 'bodyScripts'}}
<script src="/core/admin/assets/js/blog.js"></script>
{{/contentFor}}
{{!< default}}
<section class="content-list">
<header class="floatingheader">
@ -20,7 +16,7 @@
<ol>
{{#each posts}}
{{! #if featured class="featured"{{/if}}
<li data-id="{{id}}" data-content="{{content_html}}">
<li data-id="{{id}}">
<a class="permalink" href="#">
<h3 class="entry-title">{{title}}</h3>
<section class="entry-meta">

View File

@ -23,8 +23,12 @@
<link rel="stylesheet" type="text/css" href="/core/admin/assets/lib/icheck/css/icheck.css"> <!-- TODO: Kill this - #29 -->
{{{block "pageStyles"}}}
<!-- TODO: move all scripts to the end of body -->
<script type="text/javascript" src="/core/admin/assets/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/icheck/jquery.icheck.min.js"></script> <!-- The right place for this? -->
<script type="text/javascript" src="/core/admin/assets/lib/icheck/jquery.icheck.min.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/underscore.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/backbone/backbone.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/backbone/backbone-layout.js"></script>
<script type="text/javascript" src="/core/admin/assets/js/toggle.js"></script>
<script type="text/javascript" src="/core/admin/assets/js/admin-ui-temp.js"></script>
{{{block "headScripts"}}}
@ -34,12 +38,17 @@
{{> navbar}}
{{/unless}}
<main role="main">
<main role="main" id="main">
{{> flashes}}
{{{body}}}
</main>
<script type="text/javascript" src="/core/admin/assets/js/init.js"></script>
<!-- // require '/core/admin/assets/js/models/*' -->
<script type="text/javascript" src="/core/admin/assets/js/models/post.js"></script>
<!-- // require '/core/admin/assets/js/views/*' -->
<script type="text/javascript" src="/core/admin/assets/js/views/blog.js"></script>
{{{block "bodyScripts"}}}
</body>
</html>

View File

@ -25,7 +25,7 @@
// Allow people to overwrite the navTemplatePath
templatePath = templatePath || this.navTemplatePath;
return nodefn.call(fs.readFile, templatePath).then(function(navTemplateContents) {
return nodefn.call(fs.readFile, templatePath).then(function (navTemplateContents) {
// TODO: Can handlebars compile async?
self.navTemplateFunc = handlebars.compile(navTemplateContents.toString());
});
@ -40,11 +40,11 @@
};
// A static helper method for registering with ghost
GhostNavHelper.registerWithGhost = function(ghost) {
GhostNavHelper.registerWithGhost = function (ghost) {
var templatePath = path.join(ghost.paths().frontendViews, 'nav.hbs'),
ghostNavHelper = new GhostNavHelper(templatePath);
return ghostNavHelper.compileTemplate().then(function() {
return ghostNavHelper.compileTemplate().then(function () {
ghost.registerThemeHelper("ghostNav", ghostNavHelper.renderNavItems);
});
};

View File

@ -68,7 +68,7 @@
dataProvider: function () { return bookshelfDataProvider; },
statuses: function () { return statuses; },
polyglot: function () { return polyglot; },
plugin: function() { return plugin; },
plugin: function () { return plugin; },
paths: function () {
return {
'activeTheme': __dirname + '/../content/' + config.themeDir + '/' + config.activeTheme + '/',

View File

@ -81,7 +81,7 @@
return function (req, res) {
var options = _.extend(req.body, req.params);
return apiMethod(options).then(function (result) {
res.json(result);
res.json(result || {});
}, function (error) {
res.json(400, {error: error});
});