diff --git a/.gitignore b/.gitignore
index 5e9f7bd654..ab8e681a28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ projectFilesBackup
# Ghost DB file
*.db
+/core/admin/assets/tmpl/hbs-tmpl.js
/core/admin/assets/css
/core/admin/assets/sass/modules/bourbon
.sass-cache/
diff --git a/Gruntfile.js b/Gruntfile.js
index 24641dccdb..436ce3607a 100644
--- a/Gruntfile.js
+++ b/Gruntfile.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/tmpl/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;
diff --git a/core/admin/assets/js/admin-ui-temp.js b/core/admin/assets/js/admin-ui-temp.js
index 39fada118d..af4cc85841 100644
--- a/core/admin/assets/js/admin-ui-temp.js
+++ b/core/admin/assets/js/admin-ui-temp.js
@@ -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));
\ No newline at end of file
diff --git a/core/admin/assets/js/editor.js b/core/admin/assets/js/editor.js
deleted file mode 100644
index 2951939e5d..0000000000
--- a/core/admin/assets/js/editor.js
+++ /dev/null
@@ -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));
\ No newline at end of file
diff --git a/core/admin/assets/js/init.js b/core/admin/assets/js/init.js
index 68af6e9087..8f302daa82 100644
--- a/core/admin/assets/js/init.js
+++ b/core/admin/assets/js/init.js
@@ -1,20 +1,66 @@
-/*globals window, Backbone */
+/*globals window, _, $, Backbone */
(function ($) {
+
"use strict";
var Ghost = {
- Layout : {},
- View : {},
- Collection : {},
- Model : {},
+ Layout : {},
+ Views : {},
+ Collections : {},
+ Models : {},
settings: {
- baseUrl: '/api/v0.1'
+ apiRoot: '/api/v0.1'
},
- currentView: null
+ // This is a helper object to denote legacy things in the
+ // middle of being transitioned.
+ temporary: {},
+
+ currentView: null,
+ router: null
};
+ Ghost.View = Backbone.View.extend({
+
+ // 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;
+ },
+
+ // 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);
+ }
+
+ });
+
window.Ghost = Ghost;
}());
\ No newline at end of file
diff --git a/core/admin/assets/js/models/post.js b/core/admin/assets/js/models/post.js
index 47c7fde7df..e7a97216ab 100644
--- a/core/admin/assets/js/models/post.js
+++ b/core/admin/assets/js/models/post.js
@@ -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;
+ }
});
}());
\ No newline at end of file
diff --git a/core/admin/assets/js/router.js b/core/admin/assets/js/router.js
new file mode 100644
index 0000000000..c2c332d406
--- /dev/null
+++ b/core/admin/assets/js/router.js
@@ -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 });
+ }
+ }
+
+ });
+
+}());
\ No newline at end of file
diff --git a/core/admin/assets/js/starter.js b/core/admin/assets/js/starter.js
new file mode 100644
index 0000000000..adb6c2112d
--- /dev/null
+++ b/core/admin/assets/js/starter.js
@@ -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'});
+
+ });
+
+
+}());
\ No newline at end of file
diff --git a/core/admin/assets/js/toggle.js b/core/admin/assets/js/toggle.js
index 3814426a87..b46340d90c 100644
--- a/core/admin/assets/js/toggle.js
+++ b/core/admin/assets/js/toggle.js
@@ -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 @@
//
Toggled yo
//
//
- $('[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));
\ No newline at end of file
diff --git a/core/admin/assets/js/views/blog.js b/core/admin/assets/js/views/blog.js
index 192ddd119f..bd159ee929 100644
--- a/core/admin/assets/js/views/blog.js
+++ b/core/admin/assets/js/views/blog.js
@@ -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 = Ghost.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 = Ghost.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 = Ghost.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 = Ghost.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' });
-
}());
\ No newline at end of file
diff --git a/core/admin/assets/js/views/editor.js b/core/admin/assets/js/views/editor.js
new file mode 100644
index 0000000000..1e98727821
--- /dev/null
+++ b/core/admin/assets/js/views/editor.js
@@ -0,0 +1,219 @@
+// # 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 = Ghost.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 = Ghost.View.extend({
+
+ });
+
+ // The Publish, Queue, Publish Now buttons
+ // ----------------------------------------
+ ActionsWidget = Ghost.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();
+ var model = this.model;
+ this.savePost().then(function () {
+ alert('Your post was saved as ' + model.get('status'));
+ }, function () {
+ alert(model.validationError);
+ });
+ },
+
+ savePost: function (data) {
+ // TODO: The content getter here isn't great, shouldn't rely on currentView.
+ var saved = this.model.save(_.extend({
+ title: $('#entry-title').val(),
+ content: Ghost.currentView.editor.getValue()
+ }, data));
+
+ // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
+ // ourselves for more consistent promises.
+ if (saved) {
+ return saved;
+ }
+ return $.Deferred().reject();
+ },
+
+ 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 = Ghost.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();
+ });
+ }
+
+ });
+
+}());
\ No newline at end of file
diff --git a/core/admin/assets/lib/backbone/backbone-layout.js b/core/admin/assets/lib/backbone/backbone-layout.js
deleted file mode 100644
index 17493687eb..0000000000
--- a/core/admin/assets/lib/backbone/backbone-layout.js
+++ /dev/null
@@ -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)
- }
-})
diff --git a/core/admin/assets/lib/handlebars/handlebars-runtime.js b/core/admin/assets/lib/handlebars/handlebars-runtime.js
new file mode 100644
index 0000000000..863658ef2c
--- /dev/null
+++ b/core/admin/assets/lib/handlebars/handlebars-runtime.js
@@ -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": ">",
+ '"': """,
+ "'": "'",
+ "`": "`"
+};
+
+var badChars = /[&<>"'`]/g;
+var possible = /[&<>"'`]/;
+
+var escapeChar = function(chr) {
+ return escape[chr] || "&";
+};
+
+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);
+;
\ No newline at end of file
diff --git a/core/admin/assets/tmpl/content/list-item.hbs b/core/admin/assets/tmpl/content/list-item.hbs
new file mode 100644
index 0000000000..cdbde4174d
--- /dev/null
+++ b/core/admin/assets/tmpl/content/list-item.hbs
@@ -0,0 +1,7 @@
+
+ {{title}}
+
+ 5 minutes ago
+ {{!1,934 }}
+
+
\ No newline at end of file
diff --git a/core/admin/assets/tmpl/content/preview.hbs b/core/admin/assets/tmpl/content/preview.hbs
new file mode 100644
index 0000000000..6fbbdbf09d
--- /dev/null
+++ b/core/admin/assets/tmpl/content/preview.hbs
@@ -0,0 +1,21 @@
+
+
\ No newline at end of file
diff --git a/core/admin/views/content.hbs b/core/admin/views/content.hbs
index 08d209564b..3c68409282 100644
--- a/core/admin/views/content.hbs
+++ b/core/admin/views/content.hbs
@@ -1,9 +1,9 @@
{{!< default}}
-
+
-
- {{#each posts}}
- {{! #if featured class="featured"{{/if}}
-
-
- {{title}}
-
- 5 minutes ago
- {{!1,934 }}
-
-
-
- {{/each}}
-
+
-
-
-
+
\ No newline at end of file
diff --git a/core/admin/views/dashboard.hbs b/core/admin/views/dashboard.hbs
index 9cbd6ea457..340b00c921 100644
--- a/core/admin/views/dashboard.hbs
+++ b/core/admin/views/dashboard.hbs
@@ -2,8 +2,6 @@
{{/contentFor}}
diff --git a/core/admin/views/default.hbs b/core/admin/views/default.hbs
index 77b11b1135..dce72828b2 100644
--- a/core/admin/views/default.hbs
+++ b/core/admin/views/default.hbs
@@ -22,17 +22,6 @@
{{{block "pageStyles"}}}
-
-
-
-
-
-
-
-
-
-
- {{{block "headScripts"}}}
{{#unless hideNavbar}}
@@ -45,11 +34,32 @@
{{{body}}}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
{{{block "bodyScripts"}}}