Added models & collections for various pieces

Saving post as draft, or publishing
Added HBS parser for some client tmpls
Parsing paginated posts
Added grunt watch for hbs parsing on updates
This commit is contained in:
Tim Griesser 2013-06-01 19:45:02 -04:00
parent b7064185d4
commit e5ce70e175
23 changed files with 963 additions and 560 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ projectFilesBackup
# Ghost DB file
*.db
/core/admin/assets/js/hbs-tmpl.js
/core/admin/assets/css
/core/admin/assets/sass/modules/bourbon
.sass-cache/

View File

@ -17,7 +17,7 @@
// Lint files in the root, including Gruntfile.js
"*.js",
// Lint core files, but not libs
["core/**/*.js", "!**/assets/lib/**/*.js"]
["core/**/*.js", "!**/assets/lib/**/*.js", "!**/assets/**/hbs-tmpl.js"]
]
},
@ -49,16 +49,50 @@
bourbon: {
command: 'bourbon install --path core/admin/assets/sass/modules/'
}
},
handlebars: {
core: {
options: {
namespace: "JST",
processName: function (filename) {
filename = filename.replace('./core/admin/assets/tmpl/', '');
return filename.replace('.hbs', '');
}
},
files: {
"./core/admin/assets/js/hbs-tmpl.js": "./core/admin/assets/tmpl/**/*.hbs"
}
}
},
watch: {
handlebars: {
files: './core/admin/assets/tmpl/**/*.hbs',
tasks: ['handlebars']
}
}
};
grunt.initConfig(cfg);
grunt.loadNpmTasks("grunt-jslint");
grunt.loadNpmTasks("grunt-mocha-test");
grunt.loadNpmTasks("grunt-contrib-sass");
grunt.loadNpmTasks("grunt-shell");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-sass");
grunt.loadNpmTasks("grunt-contrib-handlebars");
// Prepare the project for development
// TODO: Git submodule init/update (https://github.com/jaubourg/grunt-update-submodules)?
grunt.registerTask("init", ["shell:bourbon", "sass:admin"]);
@ -68,6 +102,9 @@
// Run tests and lint code
grunt.registerTask("validate", ["jslint", "mochaTest:all"]);
// When you just say "grunt"
grunt.registerTask("default", ['sass:admin', 'handlebars', 'watch']);
};
module.exports = configureGrunt;

View File

@ -48,10 +48,5 @@
$(this).next("ul").fadeToggle(200);
});
$('.editor-options').on('click', 'li', function (e) {
$('.button-save').data("state", $(this).data("title")).attr('data-state', $(this).data("title")).text($(this).text());
$('.editor-options .active').removeClass('active');
$(this).addClass('active');
});
});
}(jQuery));

View File

@ -1,242 +0,0 @@
// # Article Editor
/*global window, document, history, jQuery, Showdown, CodeMirror, shortcut, Countable */
(function ($, ShowDown, CodeMirror, shortcut, Countable) {
"use strict";
var wordCount = $('.entry-word-count'),
charCount = $('.entry-character-count'),
paragraphCount = $('.entry-paragraph-count'),
// ## Converter Initialisation
/**
* @property converter
* @type {ShowDown.converter}
*/
// Initialise the Showdown converter for Markdown.
// var delay;
converter = new ShowDown.converter({extensions: ['ghostdown']}),
editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
tabMode: 'indent',
lineWrapping: true
});
// ## Functions
/**
* @method Update word count
* @todo Really not the best way to do things as it includes Markdown formatting along with words
* @constructor
*/
// This updates the word count on the editor preview panel.
// function updateWordCount() {
// var wordCount = document.getElementsByClassName('entry-word-count')[0],
// editorValue = editor.getValue();
// if (editorValue.length) {
// wordCount.innerHTML = editorValue.match(/\S+/g).length + ' words';
// }
// }
/**
* @method updatePreview
* @constructor
*/
// This updates the editor preview panel.
// Currently gets called on every key press.
// Also trigger word count update
function updatePreview() {
var preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = converter.makeHtml(editor.getValue());
console.log(preview);
Countable.once(preview, function (counter) {
// updateWordCount(counter);
wordCount.text(counter.words + ' words');
charCount.text(counter.characters + ' characters');
paragraphCount.text(counter.paragraphs + ' paragraphs');
});
}
/**
* @method Save
* @constructor
*/
// This method saves a post
function save() {
var entry = {
title: document.getElementById('entry-title').value,
content: editor.getValue()
},
urlSegments = window.location.pathname.split('/'),
id;
if (urlSegments[2] === 'editor' && urlSegments[3] && /^[a-zA-Z0-9]+$/.test(urlSegments[2])) {
id = urlSegments[3];
$.ajax({
url: '/api/v0.1/posts/' + id,
method: 'PUT',
data: entry,
success: function (data) {
console.log('response', data);
},
error: function (error) {
console.log('error', error);
}
});
} else {
$.ajax({
url: '/api/v0.1/posts',
method: 'POST',
data: entry,
success: function (data) {
console.log('response', data);
history.pushState(data, '', '/ghost/editor/' + data.id);
},
error: function (jqXHR, status, error) {
var errors = JSON.parse(jqXHR.responseText);
console.log('FAILED', errors);
}
});
}
}
// ## Main Initialisation
$(document).ready(function () {
$('.entry-markdown header, .entry-preview header').click(function (e) {
$('.entry-markdown, .entry-preview').removeClass('active');
$(e.target).closest('section').addClass('active');
});
editor.on("change", function () {
//clearTimeout(delay);
//delay = setTimeout(updatePreview, 50);
updatePreview();
});
updatePreview();
$('.button-save').on('click', function () {
save();
});
// Sync scrolling
function syncScroll(e) {
// vars
var $codeViewport = $(e.target),
$previewViewport = $('.entry-preview-content'),
$codeContent = $('.CodeMirror-sizer'),
$previewContent = $('.rendered-markdown'),
// calc position
codeHeight = $codeContent.height() - $codeViewport.height(),
previewHeight = $previewContent.height() - $previewViewport.height(),
ratio = previewHeight / codeHeight,
previewPostition = $codeViewport.scrollTop() * ratio;
// apply new scroll
$previewViewport.scrollTop(previewPostition);
}
// TODO: Debounce
$('.CodeMirror-scroll').on('scroll', syncScroll);
// Shadow on Markdown if scrolled
$('.CodeMirror-scroll').on('scroll', function (e) {
if ($('.CodeMirror-scroll').scrollTop() > 10) {
$('.entry-markdown').addClass('scrolling');
} else {
$('.entry-markdown').removeClass('scrolling');
}
});
// Shadow on Preview if scrolled
$('.entry-preview-content').on('scroll', function (e) {
if ($('.entry-preview-content').scrollTop() > 10) {
$('.entry-preview').addClass('scrolling');
} else {
$('.entry-preview').removeClass('scrolling');
}
});
// ## Shortcuts
// Zen writing mode
shortcut.add("Alt+Shift+Z", function () {
$('body').toggleClass('zen');
});
var MarkdownShortcuts = [
{
'key': 'Ctrl+B',
'style': 'bold'
},
{
'key': 'Meta+B',
'style': 'bold'
},
{
'key': 'Ctrl+I',
'style': 'italic'
},
{
'key': 'Meta+I',
'style': 'italic'
},
{
'key': 'Ctrl+Alt+U',
'style': 'strike'
},
{
'key': 'Ctrl+Shift+K',
'style': 'code'
},
{
'key': 'Alt+1',
'style': 'h1'
},
{
'key': 'Alt+2',
'style': 'h2'
},
{
'key': 'Alt+3',
'style': 'h3'
},
{
'key': 'Alt+4',
'style': 'h4'
},
{
'key': 'Alt+5',
'style': 'h5'
},
{
'key': 'Alt+6',
'style': 'h6'
},
{
'key': 'Ctrl+Shift+L',
'style': 'link'
},
{
'key': 'Ctrl+Shift+I',
'style': 'image'
},
{
'key': 'Ctrl+Q',
'style': 'blockquote'
},
{
'key': 'Ctrl+Shift+1',
'style': 'currentdate'
}
];
$.each(MarkdownShortcuts, function (index, short) {
shortcut.add(short.key, function () {
return editor.addMarkdown({style: short.style});
});
});
});
}(jQuery, Showdown, CodeMirror, shortcut, Countable));

View File

@ -1,18 +1,63 @@
/*globals window, Backbone */
/*globals window, _, $, Backbone */
(function ($) {
"use strict";
var Ghost = {
Layout : {},
View : {},
Collection : {},
Model : {},
_.extend(Backbone.View.prototype, {
settings: {
baseUrl: '/api/v0.1'
// Adds a subview to the current view, which will
// ensure its removal when this view is removed,
// or when view.removeSubviews is called
addSubview: function (view) {
if (!(view instanceof Backbone.View)) {
throw new Error("Subview must be a Backbone.View");
}
this.subviews = this.subviews || [];
this.subviews.push(view);
return view;
},
currentView: null
// Removes any subviews associated with this view
// by `addSubview`, which will in-turn remove any
// children of those views, and so on.
removeSubviews: function () {
var i, l, children = this.subviews;
if (!children) {
return this;
}
for (i = 0, l = children.length; i < l; i += 1) {
children[i].remove();
}
this.subviews = [];
return this;
},
// Extends the view's remove, by calling `removeSubviews`
// if any subviews exist.
remove: function () {
if (this.subviews) {
this.removeSubviews();
}
return Backbone.View.prototype.remove.apply(this, arguments);
}
});
var Ghost = {
Layout : {},
Views : {},
Collections : {},
Models : {},
settings: {
apiRoot: '/api/v0.1'
},
// This is a helper object to denote legacy things in the
// middle of being transitioned.
temporary: {},
currentView: null,
router: null
};
window.Ghost = Ghost;

View File

@ -2,16 +2,40 @@
(function () {
"use strict";
Ghost.Model.Post = Backbone.Model.extend({
urlRoot: '/api/v0.1/posts/',
Ghost.Models.Post = Backbone.Model.extend({
defaults: {
status: 'draft'
},
parse: function (resp) {
if (resp.tags) {
// TODO: parse tags into it's own collection on the model (this.tags)
return resp;
}
return resp;
},
validate: function (attrs) {
if (_.isEmpty(attrs.title)) {
return 'You must specify a title for the post.';
}
}
});
Ghost.Collection.Posts = Backbone.Collection.extend({
url: Ghost.settings.baseURL + '/posts',
model: Ghost.Model.Post
Ghost.Collections.Posts = Backbone.Collection.extend({
url: Ghost.settings.apiRoot + '/posts',
model: Ghost.Models.Post,
parse: function (resp) {
if (_.isArray(resp.posts)) {
this.limit = resp.limit;
this.currentPage = resp.page;
this.totalPages = resp.pages;
this.totalPosts = resp.total;
return resp.posts;
}
return resp;
}
});
}());

View File

@ -0,0 +1,38 @@
/*global window, document, Ghost, Backbone, $, _ */
(function () {
"use strict";
Ghost.Router = Backbone.Router.extend({
routes: {
'content/': 'blog',
'editor': 'editor',
'editor/': 'editor',
'editor/:id': 'editor'
},
blog: function () {
var posts = new Ghost.Collections.Posts();
posts.fetch({data: {status: 'all'}}).then(function () {
Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts });
});
},
editor: function (id) {
var post = new Ghost.Models.Post();
post.urlRoot = Ghost.settings.apiRoot + '/posts';
if (id) {
post.id = id;
post.fetch().then(function () {
Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post });
});
} else {
Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post });
}
}
});
}());

View File

@ -0,0 +1,15 @@
/*global window, document, Ghost, Backbone, $, _ */
(function () {
"use strict";
Ghost.router = new Ghost.Router();
$(function () {
Backbone.history.start({pushState: true, hashChange: false, root: '/ghost'});
});
}());

View File

@ -1,8 +1,26 @@
// # Toggle Support
/*global document, jQuery */
/*global document, jQuery, Ghost */
(function ($) {
"use strict";
Ghost.temporary.initToggles = function ($el) {
$el.find('[data-toggle]').each(function () {
var toggle = $(this).data('toggle');
$(this).parent().children(toggle).hide();
});
$el.find('[data-toggle]').on('click', function (e) {
e.preventDefault();
$(this).toggleClass('active');
var toggle = $(this).data('toggle');
$(this).parent().children(toggle).fadeToggle(100).toggleClass('open');
});
};
$(document).ready(function () {
// ## Toggle Up In Your Grill
@ -14,17 +32,7 @@
// <li>Toggled yo</li>
// </ul>
// </nav>
$('[data-toggle]').each(function () {
var toggle = $(this).data('toggle');
$(this).parent().children(toggle).hide();
});
$('[data-toggle]').on('click', function (e) {
e.preventDefault();
$(this).toggleClass('active');
var toggle = $(this).data('toggle');
$(this).parent().children(toggle).fadeToggle(100).toggleClass('open');
});
Ghost.temporary.initToggles($(document));
});
}(jQuery));

View File

@ -1,83 +1,153 @@
/*global window, document, Ghost, Backbone, $, _ */
/*global window, document, Ghost, Backbone, confirm, JST, $, _ */
(function () {
"use strict";
var ContentList,
ContentItem,
PreviewContainer,
// Add shadow during scrolling
scrollShadow = function (target, e) {
if ($(e.currentTarget).scrollTop() > 10) {
$(target).addClass('scrolling');
} else {
$(target).removeClass('scrolling');
}
};
// Base view
// ----------
Ghost.Layout.Blog = Backbone.Layout.extend({
Ghost.Views.Blog = Backbone.View.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()
this.addSubview(new PreviewContainer({ el: '.js-content-preview', collection: this.collection })).render();
this.addSubview(new ContentList({ el: '.js-content-list', collection: this.collection })).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);
},
ContentList = Backbone.View.extend({
events: {
'click .content-list-content' : 'scrollHandler',
'click .content-list-content li' : 'showPreview'
'click .content-list-content' : 'scrollHandler'
},
initialize: function (options) {
this.$('.content-list-content').on('scroll', _.bind(scrollShadow, null, '.content-list'));
this.listenTo(this.collection, 'remove', this.showNext);
},
showNext: function () {
var id = this.collection.at(0).id;
if (id) {
Backbone.trigger('blog:activeItem', id);
}
},
render: function () {
this.collection.each(function (model) {
this.$('ol').append(this.addSubview(new ContentItem({model: model})).render().el);
}, this);
this.showNext();
}
});
// Content Item
// -----------------------
ContentItem = Backbone.View.extend({
tagName: 'li',
events: {
'click a': 'setActiveItem'
},
active: false,
initialize: function () {
this.listenTo(Backbone, 'blog:activeItem', this.checkActive);
this.listenTo(this.model, 'destroy', this.removeItem);
},
removeItem: function () {
var view = this;
$.when(this.$el.slideUp()).then(function () {
view.remove();
});
},
// If the current item isn't active, we trigger the event to
// notify a change in which item we're viewing.
setActiveItem: function (e) {
e.preventDefault();
if (this.active !== true) {
Backbone.trigger('blog:activeItem', this.model.id);
this.render();
}
},
// Checks whether this item is active and doesn't match the current id.
checkActive: function (id) {
if (this.model.id !== id) {
if (this.active) {
this.active = false;
this.$el.removeClass('active');
this.render();
}
} else {
this.active = true;
this.$el.addClass('active');
}
},
showPreview: function (e) {
var item = $(e.currentTarget);
this.$('.content-list-content li').removeClass('active');
item.addClass('active');
Backbone.trigger("blog:showPreview", item.data('id'));
Backbone.trigger('blog:activeItem', item.data('id'));
},
template: JST['content/list-item'],
render: function () {
this.$el.html(this.template(_.extend({active: this.active}, this.model.toJSON())));
return this;
}
});
// 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'));
},
PreviewContainer = Backbone.View.extend({
activeId: null,
events: {
'click .post-controls .delete' : 'deletePost',
'click .post-controls .post-edit' : 'editPost'
},
initialize: function (options) {
this.listenTo(Backbone, 'blog:activeItem', this.setActivePreview);
this.$('.content-preview-content').on('scroll', _.bind(scrollShadow, null, '.content-preview'));
},
setActivePreview: function (id) {
if (this.activeId !== id) {
this.activeId = id;
this.render();
}
},
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');
}
});
if (confirm('Are you sure you want to delete this post?')) {
this.model.destroy({
wait: true
});
}
},
editPost: function (e) {
@ -87,19 +157,20 @@
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();
},
template: JST['content/preview'],
render: function () {
this.$('.wrapper').html(this.model.get('content_html'));
if (this.activeId) {
this.model = this.collection.get(this.activeId);
this.$el.html(this.template(this.model.toJSON()));
}
this.$('.wrapper').on('click', 'a', function (e) {
$(e.currentTarget).attr('target', '_blank');
});
Ghost.temporary.initToggles(this.$el);
return this;
}
});
// Initialize views.
// TODO: move to a `Backbone.Router`
Ghost.currentView = new Ghost.Layout.Blog({ el: '#main' });
}());

View File

@ -0,0 +1,207 @@
// # Article Editor
/*global window, alert, document, history, Backbone, Ghost, $, _, Showdown, CodeMirror, shortcut, Countable */
(function () {
"use strict";
var PublishBar,
TagWidget,
ActionsWidget,
MarkdownShortcuts = [
{'key': 'Ctrl+B', 'style': 'bold'},
{'key': 'Meta+B', 'style': 'bold'},
{'key': 'Ctrl+I', 'style': 'italic'},
{'key': 'Meta+I', 'style': 'italic'},
{'key': 'Ctrl+Alt+U', 'style': 'strike'},
{'key': 'Ctrl+Shift+K', 'style': 'code'},
{'key': 'Alt+1', 'style': 'h1'},
{'key': 'Alt+2', 'style': 'h2'},
{'key': 'Alt+3', 'style': 'h3'},
{'key': 'Alt+4', 'style': 'h4'},
{'key': 'Alt+5', 'style': 'h5'},
{'key': 'Alt+6', 'style': 'h6'},
{'key': 'Ctrl+Shift+L', 'style': 'link'},
{'key': 'Ctrl+Shift+I', 'style': 'image'},
{'key': 'Ctrl+Q', 'style': 'blockquote'},
{'key': 'Ctrl+Shift+1', 'style': 'currentdate'}
];
// The publish bar associated with a post, which has the TagWidget and
// Save button and options and such.
// ----------------------------------------
PublishBar = Backbone.View.extend({
initialize: function () {
this.addSubview(new TagWidget({el: this.$('#entry-categories'), model: this.model})).render();
this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
}
});
// The Tag UI area associated with a post
// ----------------------------------------
TagWidget = Backbone.View.extend({
});
// The Publish, Queue, Publish Now buttons
// ----------------------------------------
ActionsWidget = Backbone.View.extend({
events: {
'click [data-set-status]': 'handleStatus',
'click .js-post-button': 'updatePost'
},
statusMap: {
'draft' : 'Save Draft',
'published': 'Update Post',
'scheduled' : 'Save Schedued Post'
},
initialize: function () {
this.listenTo(this.model, 'change:status', this.render);
this.model.on('change:id', function (m) {
Backbone.history.navigate('/editor/' + m.id);
});
},
handleStatus: function (e) {
e.preventDefault();
var status = $(e.currentTarget).attr('data-set-status'),
model = this.model;
if (status === 'publish-on') {
return alert('Scheduled publishing not supported yet.');
}
if (status === 'queue') {
return alert('Scheduled publishing not supported yet.');
}
this.savePost({
status: status
}).then(function () {
alert('Your post: ' + model.get('title') + ' has been ' + status);
});
},
updatePost: function (e) {
e.preventDefault();
this.savePost();
},
savePost: function (data) {
// TODO: The content getter here isn't great, shouldn't rely on currentView.
return this.model.save(_.extend({
title: $('#entry-title').val(),
content: Ghost.currentView.editor.getValue()
}, data));
},
render: function () {
this.$('.js-post-button').text(this.statusMap[this.model.get('status')]);
}
});
// The entire /editor page's route (TODO: move all views to client side templates)
// ----------------------------------------
Ghost.Views.Editor = Backbone.View.extend({
initialize: function () {
// Add the container view for the Publish Bar
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
this.$('#entry-markdown').html(this.model.get('content'));
this.initMarkdown();
this.renderPreview();
// TODO: Debounce
this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
// Shadow on Markdown if scrolled
this.$('.CodeMirror-scroll').on('scroll', function (e) {
if ($('.CodeMirror-scroll').scrollTop() > 10) {
$('.entry-markdown').addClass('scrolling');
} else {
$('.entry-markdown').removeClass('scrolling');
}
});
// Shadow on Preview if scrolled
this.$('.entry-preview-content').on('scroll', function (e) {
if ($('.entry-preview-content').scrollTop() > 10) {
$('.entry-preview').addClass('scrolling');
} else {
$('.entry-preview').removeClass('scrolling');
}
});
// Zen writing mode shortcut
shortcut.add("Alt+Shift+Z", function () {
$('body').toggleClass('zen');
});
$('.entry-markdown header, .entry-preview header').click(function (e) {
$('.entry-markdown, .entry-preview').removeClass('active');
$(e.target).closest('section').addClass('active');
});
},
syncScroll: function (e) {
var $codeViewport = $(e.target),
$previewViewport = $('.entry-preview-content'),
$codeContent = $('.CodeMirror-sizer'),
$previewContent = $('.rendered-markdown'),
// calc position
codeHeight = $codeContent.height() - $codeViewport.height(),
previewHeight = $previewContent.height() - $previewViewport.height(),
ratio = previewHeight / codeHeight,
previewPostition = $codeViewport.scrollTop() * ratio;
// apply new scroll
$previewViewport.scrollTop(previewPostition);
},
// This updates the editor preview panel.
// Currently gets called on every key press.
// Also trigger word count update
renderPreview: function () {
var view = this,
preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
Countable.once(preview, function (counter) {
view.$('.entry-word-count').text(counter.words + ' words');
view.$('.entry-character-count').text(counter.characters + ' characters');
view.$('.entry-paragraph-count').text(counter.paragraphs + ' paragraphs');
});
},
// Markdown converter & markdown shortcut initialization.
initMarkdown: function () {
this.converter = new Showdown.converter({extensions: ['ghostdown']});
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
tabMode: 'indent',
lineWrapping: true
});
_.each(MarkdownShortcuts, function (combo) {
shortcut.add(combo.key, function () {
return this.editor.addMarkdown({style: combo.style});
});
});
var view = this;
this.editor.on('change', function () {
view.renderPreview();
});
}
});
}());

View File

@ -1,56 +0,0 @@
// 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)
}
})

View File

@ -0,0 +1,362 @@
/*
Copyright (C) 2011 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// lib/handlebars/browser-prefix.js
var Handlebars = {};
(function(Handlebars, undefined) {
;
// lib/handlebars/base.js
Handlebars.VERSION = "1.0.0";
Handlebars.COMPILER_REVISION = 4;
Handlebars.REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
};
Handlebars.helpers = {};
Handlebars.partials = {};
var toString = Object.prototype.toString,
functionType = '[object Function]',
objectType = '[object Object]';
Handlebars.registerHelper = function(name, fn, inverse) {
if (toString.call(name) === objectType) {
if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); }
Handlebars.Utils.extend(this.helpers, name);
} else {
if (inverse) { fn.not = inverse; }
this.helpers[name] = fn;
}
};
Handlebars.registerPartial = function(name, str) {
if (toString.call(name) === objectType) {
Handlebars.Utils.extend(this.partials, name);
} else {
this.partials[name] = str;
}
};
Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Missing helper: '" + arg + "'");
}
});
Handlebars.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if(type === "[object Array]") {
if(context.length > 0) {
return Handlebars.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
return fn(context);
}
});
Handlebars.K = function() {};
Handlebars.createFrame = Object.create || function(object) {
Handlebars.K.prototype = object;
var obj = new Handlebars.K();
Handlebars.K.prototype = null;
return obj;
};
Handlebars.logger = {
DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
// can be overridden in the host environment
log: function(level, obj) {
if (Handlebars.logger.level <= level) {
var method = Handlebars.logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, obj);
}
}
}
};
Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (options.data) {
data = Handlebars.createFrame(options.data);
}
if(context && typeof context === 'object') {
if(context instanceof Array){
for(var j = context.length; i<j; i++) {
if (data) { data.index = i; }
ret = ret + fn(context[i], { data: data });
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) { data.key = key; }
ret = ret + fn(context[key], {data: data});
i++;
}
}
}
}
if(i === 0){
ret = inverse(this);
}
return ret;
});
Handlebars.registerHelper('if', function(conditional, options) {
var type = toString.call(conditional);
if(type === functionType) { conditional = conditional.call(this); }
if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(conditional, options) {
return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
});
Handlebars.registerHelper('with', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
});
Handlebars.registerHelper('log', function(context, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
Handlebars.log(level, context);
});
;
// lib/handlebars/utils.js
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
Handlebars.Exception = function(message) {
var tmp = Error.prototype.constructor.apply(this, arguments);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
};
Handlebars.Exception.prototype = new Error();
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
this.string = string;
};
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
};
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
return escape[chr] || "&amp;";
};
Handlebars.Utils = {
extend: function(obj, value) {
for(var key in value) {
if(value.hasOwnProperty(key)) {
obj[key] = value[key];
}
}
},
escapeExpression: function(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
} else if (string == null || string === false) {
return "";
}
// Force a string conversion as this will be done by the append regardless and
// the regex test will do this transparently behind the scenes, causing issues if
// an object's to string has escaped characters in it.
string = string.toString();
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
},
isEmpty: function(value) {
if (!value && value !== 0) {
return true;
} else if(toString.call(value) === "[object Array]" && value.length === 0) {
return true;
} else {
return false;
}
}
};
;
// lib/handlebars/runtime.js
Handlebars.VM = {
template: function(templateSpec) {
// Just add water
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
programWrapper = Handlebars.VM.program(i, fn, data);
} else if (!programWrapper) {
programWrapper = this.programs[i] = Handlebars.VM.program(i, fn);
}
return programWrapper;
},
merge: function(param, common) {
var ret = param || common;
if (param && common) {
ret = {};
Handlebars.Utils.extend(ret, common);
Handlebars.Utils.extend(ret, param);
}
return ret;
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop,
compilerInfo: null
};
return function(context, options) {
options = options || {};
var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
var compilerInfo = container.compilerInfo || [],
compilerRevision = compilerInfo[0] || 1,
currentRevision = Handlebars.COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").";
}
}
return result;
};
},
programWithDepth: function(i, fn, data /*, $depth */) {
var args = Array.prototype.slice.call(arguments, 3);
var program = function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
};
program.program = i;
program.depth = args.length;
return program;
},
program: function(i, fn, data) {
var program = function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
program.program = i;
program.depth = 0;
return program;
},
noop: function() { return ""; },
invokePartial: function(partial, name, context, helpers, partials, data) {
var options = { helpers: helpers, partials: partials, data: data };
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
} else if (!Handlebars.compile) {
throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
} else {
partials[name] = Handlebars.compile(partial, {data: data !== undefined});
return partials[name](context, options);
}
}
};
Handlebars.template = Handlebars.VM.template;
;
// lib/handlebars/browser-suffix.js
})(Handlebars);
;

View File

@ -0,0 +1,7 @@
<a class="permalink{{#if featured}} featured{{/if}}" href="#">
<h3 class="entry-title">{{title}}</h3>
<section class="entry-meta">
<time datetime="2013-01-04" class="date">5 minutes ago</time>
{{!<span class="views">1,934</span>}}
</section>
</a>

View File

@ -0,0 +1,21 @@
<header class="floatingheader">
<a class="{{#if featured}}featured{{else}}unfeatured{{/if}}" href="#">
<span class="hidden">Star</span>
</a>
{{! TODO: JavaScript toggle featured/unfeatured}}
<span class="status">Published</span>
<span class="normal">by</span>
<span class="author">John O'Nolan</span>
<section class="post-controls">
<a class="post-edit" href="#"><span class="hidden">Edit Post</span></a>
<a class="post-settings" href="#" data-toggle=".menu-drop-right"><span class="hidden">Post Settings</span></a>
<ul class="menu-drop-right">
<li><a href="#" class="url">URL</a></li>
<li><a href="#" class="something">Something</a></li>
<li><a href="#" class="delete">Delete</a></li>
</ul>
</section>
</header>
<section class="content-preview-content">
<div class="wrapper">{{{content_html}}}</div>
</section>

View File

@ -1,9 +1,9 @@
{{!< default}}
<section class="content-list">
<section class="content-list js-content-list">
<header class="floatingheader">
<section class="content-filter">
<a class="dropdown" href="#" data-toggle=".menu-drop">All Posts</a>
<ul class="menu-drop">
<ul class="menu-drop" style="display:none;">
<li class="active"><a href="#">All Posts</a></li>
<li><a href="#">Recently Edited</a></li>
<li><a href="#">By Author...</a></li>
@ -13,43 +13,9 @@
<a href="/ghost/editor" class="button button-add"><span class="hidden">New Post</span></a>
</header>
<section class="content-list-content">
<ol>
{{#each posts}}
{{! #if featured class="featured"{{/if}}
<li data-id="{{id}}">
<a class="permalink" href="#">
<h3 class="entry-title">{{title}}</h3>
<section class="entry-meta">
<time datetime="2013-01-04" class="date">5 minutes ago</time>
{{!<span class="views">1,934</span>}}
</section>
</a>
</li>
{{/each}}
</ol>
<ol></ol>
</section>
</section>
<section class="content-preview">
<header class="floatingheader">
<a class="unfeatured" href="#"><span class="hidden">Star</span></a>
{{! TODO: JavaScript toggle featured/unfeatured}}
<span class="status">Published</span>
<span class="normal">by</span>
<span class="author">John O'Nolan</span>
<section class="post-controls">
<a class="post-edit" href="#"><span class="hidden">Edit Post</span></a>
<a class="post-settings" href="#" data-toggle=".menu-drop-right"><span class="hidden">Post Settings</span></a>
<ul class="menu-drop-right">
<li><a href="#" class="url">URL</a></li>
<li><a href="#" class="something">Something</a></li>
<li><a href="#" class="delete">Delete</a></li>
</ul>
</section>
</header>
<section class="content-preview-content">
<div class="wrapper"></div>
</section>
<section class="content-preview js-content-preview">
</section>

View File

@ -2,8 +2,6 @@
<script src="/core/admin/assets/lib/chart.min.js"></script>
<script>
$(document).ready(function(){
//$('body').click(function(){
$('.widget-time').fadeIn(1000);
$('.widget-image').delay(300).fadeIn(1000);
$('.widget-posts').delay(600).fadeIn(900, function(){
@ -48,7 +46,6 @@
$('.widget-ideas').delay(3200).fadeIn(1000);
$('.widget-tweets').delay(3400).fadeIn(1000);
$('.widget-backups').delay(3600).fadeIn(1000);
//});
});
</script>
{{/contentFor}}

View File

@ -22,17 +22,6 @@
<link rel="stylesheet" type="text/css" href="/core/admin/assets/lib/codemirror/codemirror.css"> <!-- TODO: Kill this - #29 -->
<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>
<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/lib/countable.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"}}}
</head>
<body class="{{bodyClass}}">
{{#unless hideNavbar}}
@ -45,11 +34,32 @@
{{{body}}}
</main>
<script type="text/javascript" src="/core/admin/assets/js/init.js"></script>
<script src="/core/admin/assets/lib/jquery/jquery.min.js"></script>
<script src="/core/admin/assets/lib/icheck/jquery.icheck.min.js"></script>
<script src="/core/admin/assets/lib/underscore.js"></script>
<script src="/core/admin/assets/lib/backbone/backbone.js"></script>
<script src="/core/admin/assets/lib/handlebars/handlebars-runtime.js"></script>
<script src="/core/admin/assets/lib/codemirror/codemirror.js"></script>
<script src="/core/admin/assets/lib/codemirror/mode/markdown/markdown.js"></script>
<script src="/core/admin/assets/lib/showdown/showdown.js"></script>
<script src="/core/admin/assets/lib/showdown/extensions/ghostdown.js"></script>
<script src="/core/admin/assets/lib/shortcuts.js"></script>
<script src="/core/admin/assets/lib/countable.js"></script>
<script src="/core/admin/assets/js/init.js"></script>
<script src="/core/admin/assets/js/hbs-tmpl.js"></script>
<script src="/core/admin/assets/js/toggle.js"></script>
<script src="/core/admin/assets/js/admin-ui-temp.js"></script>
<script src="/core/admin/assets/js/markdown-actions.js"></script>
<script src="/core/admin/assets/js/tagui.js"></script>
<!-- // require '/core/admin/assets/js/models/*' -->
<script type="text/javascript" src="/core/admin/assets/js/models/post.js"></script>
<script 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>
<script src="/core/admin/assets/js/views/blog.js"></script>
<script src="/core/admin/assets/js/views/editor.js"></script>
<script src="/core/admin/assets/js/router.js"></script>
<script src="/core/admin/assets/js/starter.js"></script>
{{{block "bodyScripts"}}}
</body>
</html>

View File

@ -1,14 +1,3 @@
{{#contentFor 'bodyScripts'}}
<script src="/core/admin/assets/lib/codemirror/codemirror.js"></script>
<script src="/core/admin/assets/lib/codemirror/mode/markdown/markdown.js"></script>
<script src="/core/admin/assets/lib/showdown/showdown.js"></script>
<script src="/core/admin/assets/lib/showdown/extensions/ghostdown.js"></script>
<script src="/core/admin/assets/lib/shortcuts.js"></script>
<script src="/core/admin/assets/js/markdown-actions.js"></script>
<script src="/core/admin/assets/js/editor.js"></script>
<script src="/core/admin/assets/js/tagui.js"></script>
{{/contentFor}}
{{!< default}}
{{! TODO: Add "scrolling" class only when one of the panels is scrolled down by 5px or more }}
<header>
@ -25,13 +14,13 @@
<a class="markdown-help" href="#"><span class="hidden">What is Markdown?</span></a>
</header>
<section class="entry-markdown-content">
<textarea id="entry-markdown">{{content}}</textarea>
<textarea id="entry-markdown"></textarea>
</section>
</section>{{!.entry-markdown}}
<section class="entry-preview">
<header class="floatingheader">
Preview <span class="entry-word-count">0 words</span>
Preview <span class="entry-word-count js-entry-word-count">0 words</span>
</header>
<section class="entry-preview-content">
<div class="rendered-markdown">
@ -53,13 +42,13 @@
<div class="right">
<button id="entry-settings" href="#" class="button-link"><span class="hidden">Settings</span></button>
<section id="entry-actions" class="splitbutton-save">
<button type="button" class="button-save" data-state="save-draft">Save Draft</button>
<button type="button" class="button-save js-post-button"></button>
<a class="options up" href="#"><span class="hidden">Options</span></a>
<ul class="editor-options overlay" style="display:none">
<li data-title="publish-now" data-url=""><a href="#">Publish Now</a></li>
<li data-title="queue" data-url=""><a href="#">Add to Queue</a></li>
<li data-title="publish-on" data-url=""><a href="#">Publish on...</a></li>
<li data-title="save-draft" data-url="" class="active"><a href="#">Save Draft</a></li>
<li data-set-status="published"><a href="#">Publish Now</a></li>
<li data-set-status="queue"><a href="#">Add to Queue</a></li>
<li data-set-status="publish-on"><a href="#">Publish on...</a></li>
<li data-set-status="draft"><a href="#">Save Draft</a></li>
</ul>
</section>
</div>

View File

@ -80,7 +80,7 @@
// takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response
requestHandler = function (apiMethod) {
return function (req, res) {
var options = _.extend(req.body, req.params);
var options = _.extend(req.body, req.query, req.params);
return apiMethod(options).then(function (result) {
res.json(result || {});
}, function (error) {

View File

@ -1,95 +0,0 @@
/*global require, module */
(function () {
"use strict";
// We should just be able to require bookshelf and have it reference
// the `Knex` instance bootstraped at the app initialization.
var Bookshelf = require('bookshelf'),
Showdown = require('showdown'),
converter = new Showdown.converter(),
Post,
Posts,
User,
Users,
Setting,
Settings;
Post = Bookshelf.Model.extend({
tableName: 'posts',
hasTimestamps: true,
initialize: function () {
this.on('creating', this.creating, this);
this.on('saving', this.saving, this);
},
saving: function () {
if (!this.get('title')) {
throw new Error('Post title cannot be blank');
}
this.set('content_html', converter.makeHtml(this.get('content')));
// refactoring of ghost required in order to make these details available here
// this.set('language', this.get('language') || ghost.config().defaultLang);
// this.set('status', this.get('status') || ghost.statuses().draft);
},
creating: function () {
if (!this.get('slug')) {
this.generateSlug();
}
},
generateSlug: function () {
return this.set('slug', this.get('title').replace(/\:/g, '').replace(/\s/g, '-').toLowerCase());
},
user: function () {
return this.belongsTo(User, 'created_by');
}
});
Posts = Bookshelf.Collection.extend({
model: Post
});
User = Bookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true,
posts: function () {
return this.hasMany(Posts, 'created_by');
}
});
Users = Bookshelf.Collection.extend({
model: User
});
Setting = Bookshelf.Model.extend({
tableName: 'settings',
hasTimestamps: true
});
Settings = Bookshelf.Collection.extend({
model: Setting
});
module.exports = {
Post: Post,
Posts: Posts,
User: User,
Users: Users,
Setting: Setting,
Settings: Settings
};
}());

View File

@ -77,7 +77,8 @@
opts = {page: opts};
}
opts = _.extend({page: 1}, {
opts = _.extend({
page: 1,
limit: 15,
where: {},
status: 'published'

View File

@ -29,6 +29,8 @@
"grunt-shell": "~0.2.2",
"grunt-contrib-sass": "~0.3.0",
"sinon": "~1.7.2",
"mocha": "~1.10.0"
"mocha": "~1.10.0",
"grunt-contrib-handlebars": "~0.5.9",
"grunt-contrib-watch": "~0.4.4"
}
}