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}}

    + +
    \ 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 @@ +
    + + + + {{! TODO: JavaScript toggle featured/unfeatured}} + Published + by + John O'Nolan +
    + + + +
    +
    +
    +
    {{{content_html}}}
    +
    \ 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}} -
    +
    All Posts -
    -
      - {{#each posts}} - {{! #if featured class="featured"{{/if}} -
    1. - -
    2. - {{/each}} -
    +
      -
      -
      - - - {{! TODO: JavaScript toggle featured/unfeatured}} - - Published - by - John O'Nolan -
      - - - -
      -
      -
      -
      -
      +
      \ 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"}}} \ No newline at end of file diff --git a/core/admin/views/editor.hbs b/core/admin/views/editor.hbs index ba5efc8884..84e369b657 100644 --- a/core/admin/views/editor.hbs +++ b/core/admin/views/editor.hbs @@ -1,14 +1,3 @@ -{{#contentFor 'bodyScripts'}} - - - - - - - - -{{/contentFor}} - {{!< default}} {{! TODO: Add "scrolling" class only when one of the panels is scrolled down by 5px or more }}
      @@ -25,13 +14,13 @@
      - +
      {{!.entry-markdown}}
      - Preview 0 words + Preview 0 words
      @@ -53,13 +42,13 @@ diff --git a/core/shared/api.js b/core/shared/api.js index 7f62b9de09..2ddbf0c5c2 100644 --- a/core/shared/api.js +++ b/core/shared/api.js @@ -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) { diff --git a/core/shared/models/models.js b/core/shared/models/models.js deleted file mode 100644 index 262f61d708..0000000000 --- a/core/shared/models/models.js +++ /dev/null @@ -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 - }; -}()); \ No newline at end of file diff --git a/package.json b/package.json index bcda4c9063..4f85750438 100644 --- a/package.json +++ b/package.json @@ -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" } }