Add admin prototype

issue #2270

- from https://github.com/manuelmitasch/ghost-admin-ember-demo
- Not working properly: added ic-ajax mock in app.js but promise not resolving => loading route always active
This commit is contained in:
Manuel Mitasch 2014-03-02 15:30:35 +01:00 committed by Hannah Wolfe
parent 39c99209c5
commit 805aad31ca
38 changed files with 472 additions and 18 deletions

58
app.js
View File

@ -1,4 +1,4 @@
/*global Ember */
/*global Ember, ic */
import Resolver from 'ember/resolver';
@ -15,4 +15,60 @@ var App = Ember.Application.extend({
Resolver: Resolver['default']
});
// TODO move into ext/route.js
// needed to add body class depending on current route
Ember.Route.reopen({
activate: function () {
var cssClasses = this.get('classNames'),
rootElement = this.router.namespace.get('rootElement');
if (cssClasses) {
Ember.run.schedule('afterRender', null, function () {
Ember.$(rootElement).addClass(cssClasses);
});
}
},
deactivate: function () {
var cssClasses = this.get('classNames'),
rootElement = this.router.namespace.get('rootElement');
Ember.run.schedule('afterRender', null, function () {
Ember.$(rootElement).removeClass(cssClasses);
});
}
});
ic.ajax.defineFixture('/ghost/api/v0.1/posts', {
response: {
"posts": [
{
"id": 2,
"title": "Ghost Ember Demo Post",
"markdown": "Lorem **ipsum** dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.",
"html": "<p>Lorem <strong>ipsum<\/strong> dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.<\/p>",
"status": "draft",
"author": {
"id": 1,
"name": "manuel_mitasch",
},
},
{
"id": 1,
"title": "Welcome to Ghost",
"markdown": "You're live! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. You can manage your content by signing in to the admin area at `<your blog URL>\/ghost\/`. When you arrive, you can select this post from a list on the left and see a preview of it on the right. Click the little pencil icon at the top of the preview to edit this post and read the next section!\n\n## Getting Started\n\nGhost uses something called Markdown for writing. Essentially, it's a shorthand way to manage your post formatting as you write!\n\nWriting in Markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use *shortcuts* to **style** your content. For example, a list:\n\n* Item number one\n* Item number two\n * A nested item\n* A final item\n\nor with numbers!\n\n1. Remember to buy some milk\n2. Drink the milk\n3. Tweet that I remembered to buy the milk, and drank it\n\n### Links\n\nWant to link to a source? No problem. If you paste in url, like http:\/\/ghost.org - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to [the Ghost website](http:\/\/ghost.org). Neat.\n\n### What about Images?\n\nImages work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:\n\n![The Ghost Logo](https:\/\/ghost.org\/images\/ghost.png)\n\nNot sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:\n\n![A bowl of bananas]\n\n\n### Quoting\n\nSometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.\n\n> Wisdomous - it's definitely a word.\n\n### Working with Code\n\nGot a streak of geek? We've got you covered there, too. You can write inline `<code>` blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.\n\n .awesome-thing {\n display: block;\n width: 100%;\n }\n\n### Ready for a Break? \n\nThrow 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.\n\n---\n\n### Advanced Usage\n\nThere's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.\n\n<input type=\"text\" placeholder=\"I'm an input field!\" \/>\n\nThat should be enough to get you started. Have fun - and let us know what you think :)",
"html": "<p>You're live! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. You can manage your content by signing in to the admin area at <code>&lt;your blog URL&gt;\/ghost\/<\/code>. When you arrive, you can select this post from a list on the left and see a preview of it on the right. Click the little pencil icon at the top of the preview to edit this post and read the next section!<\/p>\n\n<h2 id=\"gettingstarted\">Getting Started<\/h2>\n\n<p>Ghost uses something called Markdown for writing. Essentially, it's a shorthand way to manage your post formatting as you write!<\/p>\n\n<p>Writing in Markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use <em>shortcuts<\/em> to <strong>style<\/strong> your content. For example, a list:<\/p>\n\n<ul>\n<li>Item number one<\/li>\n<li>Item number two\n<ul><li>A nested item<\/li><\/ul><\/li>\n<li>A final item<\/li>\n<\/ul>\n\n<p>or with numbers!<\/p>\n\n<ol>\n<li>Remember to buy some milk <\/li>\n<li>Drink the milk <\/li>\n<li>Tweet that I remembered to buy the milk, and drank it<\/li>\n<\/ol>\n\n<h3 id=\"links\">Links<\/h3>\n\n<p>Want to link to a source? No problem. If you paste in url, like <a href='http:\/\/ghost.org'>http:\/\/ghost.org<\/a> - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to <a href=\"http:\/\/ghost.org\">the Ghost website<\/a>. Neat.<\/p>\n\n<h3 id=\"whataboutimages\">What about Images?<\/h3>\n\n<p>Images work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:<\/p>\n\n<p><img src=\"https:\/\/ghost.org\/images\/ghost.png\" alt=\"The Ghost Logo\" \/><\/p>\n\n<p>Not sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:<\/p>\n\n<h3 id=\"quoting\">Quoting<\/h3>\n\n<p>Sometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.<\/p>\n\n<blockquote>\n <p>Wisdomous - it's definitely a word.<\/p>\n<\/blockquote>\n\n<h3 id=\"workingwithcode\">Working with Code<\/h3>\n\n<p>Got a streak of geek? We've got you covered there, too. You can write inline <code>&lt;code&gt;<\/code> blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.<\/p>\n\n<pre><code>.awesome-thing {\n display: block;\n width: 100%;\n}\n<\/code><\/pre>\n\n<h3 id=\"readyforabreak\">Ready for a Break?<\/h3>\n\n<p>Throw 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.<\/p>\n\n<hr \/>\n\n<h3 id=\"advancedusage\">Advanced Usage<\/h3>\n\n<p>There's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.<\/p>\n\n<p><input type=\"text\" placeholder=\"I'm an input field!\" \/><\/p>\n\n<p>That should be enough to get you started. Have fun - and let us know what you think :)<\/p>",
"status": "published",
"author": {
"id": 1,
"name": "manuel_mitasch",
},
}
],
},
jqXHR: {},
textStatus: 'success'
});
export default App;

44
components/code-mirror.js Normal file
View File

@ -0,0 +1,44 @@
/* global CodeMirror*/
var onChangeHandler = function (cm) {
cm.component.set('value', cm.getDoc().getValue());
};
var onScrollHandler = function (cm) {
var scrollInfo = cm.getScrollInfo(),
percentage = scrollInfo.top / scrollInfo.height,
component = cm.component;
// throttle scroll updates
component.throttle = Ember.run.throttle(component, function () {
this.set('scrollPosition', percentage);
}, 50);
};
export default Ember.TextArea.extend({
initCodemirror: function () {
// create codemirror
this.codemirror = CodeMirror.fromTextArea(this.get('element'), {
lineWrapping: true
});
this.codemirror.component = this; // save reference to this
// propagate changes to value property
this.codemirror.on("change", onChangeHandler);
// on scroll update scrollPosition property
this.codemirror.on("scroll", onScrollHandler);
}.on("didInsertElement"),
removeThrottle: function () {
Ember.run.cancel(this.throttle);
}.on("willDestroyElement"),
removeCodemirrorHandlers: function () {
// not sure if this is needed.
this.codemirror.off("change", onChangeHandler);
this.codemirror.off("scroll", onScrollHandler);
}.on("willDestroyElement")
});

8
components/mark-down.js Normal file
View File

@ -0,0 +1,8 @@
export default Ember.Component.extend({
adjustScrollPosition: function () {
var scrollWrapper = this.$(".entry-preview-content").get(0),
scrollPixel = scrollWrapper.scrollHeight * this.get('scrollPosition'); // calculate absolute scroll position from percentage
scrollWrapper.scrollTop = scrollPixel; // adjust scroll position
}.observes("scrollPosition")
});

View File

@ -1,5 +0,0 @@
export default Ember.Component.extend({
time: function () {
return new Date();
}.property()
});

View File

View File

@ -1,3 +0,0 @@
export default Ember.Controller.extend({
message: 'its a new beginning.'
});

6
controllers/post.js Normal file
View File

@ -0,0 +1,6 @@
var equal = Ember.computed.equal;
export default Ember.ObjectController.extend({
isPublished: equal('status', 'published'),
isDraft: equal('status', 'draft')
});

View File

5
helpers/count-words.js Normal file
View File

@ -0,0 +1,5 @@
import count from "ghost/utils/word-counter";
export default Ember.Handlebars.makeBoundHelper(function (markdown) {
return count(markdown);
});

View File

@ -0,0 +1,6 @@
/* global Showdown, Handlebars */
var showdown = new Showdown.converter();
export default Ember.Handlebars.makeBoundHelper(function (markdown) {
return new Handlebars.SafeString(showdown.makeHtml(markdown));
});

View File

@ -0,0 +1,7 @@
/* global moment */
export default Ember.Handlebars.makeBoundHelper(function (timeago) {
return moment(timeago).fromNow();
// stefanpenner says cool for small number of timeagos.
// For large numbers moment sucks => single Ember.Object based clock better
// https://github.com/manuelmitasch/ghost-admin-ember-demo/commit/fba3ab0a59238290c85d4fa0d7c6ed1be2a8a82e#commitcomment-5396524
});

View File

@ -3,8 +3,17 @@
// ensure we don't share routes between all Router instances
var Router = Ember.Router.extend();
Router.reopen({
//location: 'history', // use HTML5 History API instead of hash-tag based URLs
rootURL: '/ghost/ember/' // admin interface lives under sub-directory /ghost
});
Router.map(function () {
'use strict';
this.resource('posts', { path: '/' }, function () {
this.resource('post', { path: '/:post_id' });
});
this.resource('editor', { path: '/editor/:post_id' });
this.route('new', { path: '/editor' });
});
export default Router;

View File

9
routes/editor.js Normal file
View File

@ -0,0 +1,9 @@
import ajax from "ghost/utils/ajax";
export default Ember.Route.extend({
classNames: "editor",
model: function (params) {
return ajax("/ghost/api/v0.1/posts/" + params.post_id);
}
});

11
routes/post.js Normal file
View File

@ -0,0 +1,11 @@
export default Ember.Route.extend({
model: function (params) {
var posts = this.modelFor('posts').posts,
id = parseInt(params.post_id);
return posts.findBy('id', id);
// model already loaded in parent route => no ajax call to server
// return ajax("/ghost/api/v0.1/posts/" + params.post_id);
}
});

15
routes/posts.js Normal file
View File

@ -0,0 +1,15 @@
import ajax from "ghost/utils/ajax";
export default Ember.Route.extend({
classNames: "manage",
model: function () {
return ajax("/ghost/api/v0.1/posts");
},
actions: {
openEditor: function (post) {
this.transitionTo('editor', post);
}
}
});

10
routes/posts/index.js Normal file
View File

@ -0,0 +1,10 @@
export default Ember.Route.extend({
// redirect to first post subroute
redirect: function () {
var firstPost = this.modelFor('posts').posts[0];
if (firstPost) {
this.transitionTo('post', firstPost);
}
}
});

33
styles/app.css Normal file
View File

@ -0,0 +1,33 @@
/* Put your CSS here */
/*
@keyframes domChanged { from { background: yellow; } }
@-webkit-keyframes domChanged { from { background: yellow; } }
.ember-view { animation: domChanged 1s; -webkit-animation: domChanged 1s; }
*/
/* Cosmetic changes to ghost styles */
.post-settings-menu {
display: none !important;
}
#entry-markdown, .entry-preview, .CodeMirror.cm-s-default {
height: 500px !important;
}
.editor .entry-title {
box-shadow: none !important;
background: none !important;
padding: 0 !important;
height: auto !important;
}
.editor input {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
@font-face {
font-family: "Icons";
src: url("https://testblog111.ghost.io/ghost/fonts/icons.woff") format('woff');
}

2
styles/screen.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,47 @@
<header class="floatingheader">
<button class="button-back" href="#">Back</button>
<a class="unfeatured" href="#" title="Feature this post">
<span class="hidden">Star</span>
</a>
<small>
<span class="status">Written</span>
<span class="normal">by</span>
<span class="author">{{author.name}}</span>
</small>
<section class="post-controls">
{{#link-to "editor" this class="post-edit" title="Edit Post"}}<span class="hidden">Edit Post</span>{{/link-to}}
<a class="post-settings" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-drop-right overlay" style="display: none;">
<form>
<table class="plain">
<tbody><tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" value="">
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" value="" placeholder="17 Feb 14 @ 02:35"><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</tbody></table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
</section>
</header>

8
templates/-navbar.hbs Normal file
View File

@ -0,0 +1,8 @@
<header id="global-header" class="navbar">
<nav id="global-nav" role="navigation">
<ul id="main-menu" >
<li class="content">{{#link-to "posts"}}Content{{/link-to}}</li>
<li class="editor">{{#link-to "new"}}New post{{/link-to}}</li>
</ul>
</nav>
</header>

View File

@ -0,0 +1,58 @@
<footer id="publish-bar">
<nav>
<section id="entry-tags" href="#" class="left">
<label class="tag-label" for="tags" title="Tags"><span class="hidden">Tags</span></label>
<div class="tags">Nothing implemented</div>
<input type="hidden" class="tags-holder" id="tags-holder">
<input class="tag-input" id="tags" type="text" data-input-behaviour="tag" />
<ul class="suggestions overlay"></ul>
</section>
<div class="right">
<section id="entry-controls">
<a class="post-settings" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-right overlay">
<form>
<table class="plain">
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" placeholder="" value="" />
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" placeholder="Now" value=""><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
</section>
<section id="entry-actions" class="js-publish-splitbutton splitbutton-save">
<button type="button" class="js-publish-button button-save"></button>
<a class="options up" data-toggle="ul" href="#" title="Post Settings"><span class="hidden">Post Settings</span></a>
<ul class="editor-options overlay" style="display:none">
<li data-set-status="published"><a href="#"></a></li>
<li data-set-status="draft"><a href="#"></a></li>
</ul>
</section>
</div>
</nav>
</footer>

8
templates/application.hbs Executable file → Normal file
View File

@ -1,5 +1,5 @@
<h2 id='title'>Welcome to Ghost on Ember.js</h2>
{{partial "navbar"}}
{{message}}
{{outlet}}
<main role="main" id="main">
{{outlet}}
</main>

View File

@ -0,0 +1,5 @@
<section class="entry-preview-content">
<div class="rendered-markdown">
{{format-markdown markdown}}
</div>
</section>

View File

@ -1 +0,0 @@
Time is now {{time}}

48
templates/editor.hbs Normal file
View File

@ -0,0 +1,48 @@
<style>
/*
Cosmetic changes to ghost styles, that help during development.
The contents should be solved properly or moved into the other assets.
*/
.post-settings-menu {
display: none !important;
}
#entry-markdown, .entry-preview,
.CodeMirror.cm-s-default {
height: 500px !important;
}
.editor input {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
</style>
<section class="entry-container">
<header>
<section class="box entry-title">
{{input type="text" id="entry-title" placeholder="Your Post Title" value=title tabindex="1"}}
</section>
</header>
<section class="entry-markdown active">
<header class="floatingheader">
<small>Markdown</small>
<a class="markdown-help" href="#"><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
{{code-mirror value=markdown scrollPosition=view.scrollPosition}}
</section>
</section>
<section class="entry-preview">
<header class="floatingheader">
<small>Preview <span class="entry-word-count js-entry-word-count">{{count-words markdown}} words</span></small>
</header>
{{mark-down markdown=markdown scrollPosition=view.scrollPosition}}
</section>
</section>
{{partial 'publish-bar'}}

5
templates/error.hbs Normal file
View File

@ -0,0 +1,5 @@
<h1>Sorry, Something went wrong</h1>
{{message}}
<pre>
{{stack}}
</pre>

View File

@ -1,3 +0,0 @@
<em>This is the index route</em>
{{time-now}}

1
templates/loading.hbs Normal file
View File

@ -0,0 +1 @@
<h1>Loading...</h1>

1
templates/new.hbs Normal file
View File

@ -0,0 +1 @@
TODO

8
templates/post.hbs Normal file
View File

@ -0,0 +1,8 @@
{{partial "floating-header"}}
<section class="content-preview-content">
<div class="wrapper">
<h1>{{title}}</h1>
{{format-markdown markdown}}
</div>
</section>

37
templates/posts.hbs Normal file
View File

@ -0,0 +1,37 @@
<section class="content-view-container">
<section class="content-list js-content-list">
<header class="floatingheader">
<section class="content-filter">
<small>All Posts</small>
</section>
{{#link-to "new" class="button button-add" title="New Post"}}New Post{{/link-to}}
</header>
<section class="content-list-content">
<ol class="posts-list">
{{#each posts itemController="post"}}
{{#view "post-item-view" tagName="li" post=this}} <!-- needed because of "active" class on li item -->
{{#link-to 'post' this class="permalink" title="Edit this post"}}
<h3 class="entry-title">{{title}}</h3>
<section class="entry-meta">
<span class="status">
{{#if isDraft}}
<span class="draft">Draft</span>
{{/if}}
{{#if isPublished}}
<time datetime="{{unbound published_at}}" class="date published">
Published {{format-timeago published_at}}
</time>
{{/if}}
</span>
</section>
{{/link-to}}
{{/view}}
{{/each}}
</ol>
</section>
</section>
<section class="content-preview js-content-preview">
{{outlet}}
</section>
</section>

4
utils/ajax.js Normal file
View File

@ -0,0 +1,4 @@
/* global ic */
export default function ajax() {
return ic.ajax.raw.apply(null, arguments);
}

6
utils/word-counter.js Normal file
View File

@ -0,0 +1,6 @@
export default function (s) {
s = s.replace(/(^\s*)|(\s*$)/gi, ""); // exclude start and end white-space
s = s.replace(/[ ]{2,}/gi, " "); // 2 or more space to 1
s = s.replace(/\n /, "\n"); // exclude newline with a start spacing
return s.split(' ').length;
}

View File

3
views/editor.js Normal file
View File

@ -0,0 +1,3 @@
export default Ember.View.extend({
scrollPosition: 0 // percentage of scroll position
});

7
views/item-view.js Normal file
View File

@ -0,0 +1,7 @@
export default Ember.View.extend({
classNameBindings: ['active'],
active: function () {
return this.get('childViews.firstObject.active');
}.property('childViews.firstObject.active')
});

7
views/post-item-view.js Normal file
View File

@ -0,0 +1,7 @@
import itemView from 'ghost/views/item-view';
export default itemView.extend({
openEditor: function () {
this.get('controller').send('openEditor', this.get('post')); // send action to handle transition to editor route
}.on("doubleClick")
});