0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Merge branch 'ember'

Conflicts:
	Gruntfile.js
	core/client/models/post.js
	core/client/models/settings.js
	core/client/models/user.js
	core/client/router.js
	package.json
This commit is contained in:
Hannah Wolfe 2014-05-07 22:28:29 +01:00
commit d1f57a2569
165 changed files with 3331 additions and 455 deletions

8
.gitignore vendored
View file

@ -30,9 +30,15 @@ projectFilesBackup
.build
.dist
.tmp
/core/client/tpl/hbs-tpl.js
/core/clientold/tpl/hbs-tpl.js
/core/clientold/assets/css
/core/clientold/assets/fonts
/core/clientold/assets/vendor
/core/client/assets/css
!/core/client/assets/css/ember-hacks.css
/core/client/assets/fonts
/core/server/data/export/exported*
/docs

View file

@ -1,6 +1,7 @@
!**
.build
.dist
.tmp
docs/**
_site/**
content/images/**
@ -24,4 +25,4 @@ CONTRIBUTING.md
SECURITY.md
.travis.yml
*.html
bower_components/**
bower_components/**

View file

@ -28,7 +28,7 @@ var path = require('path'),
}()),
// ## Grunt configuration
//
configureGrunt = function (grunt) {
// *This is not useful but required for jshint*
@ -42,7 +42,8 @@ var path = require('path'),
var cfg = {
// #### Common paths used by tasks
paths: {
adminAssets: './core/client/assets',
// adminAssets: './core/client/', ?? who knows...
adminOldAssets: './core/clientold/assets',
build: buildDirectory,
releaseBuild: path.join(buildDirectory, 'release'),
dist: distDirectory,
@ -58,13 +59,21 @@ var path = require('path'),
// See the [grunt dev](#live%20reload) task for how this is used.
watch: {
handlebars: {
files: ['core/client/tpl/**/*.hbs'],
files: ['core/clientold/tpl/**/*.hbs'],
tasks: ['handlebars']
},
'handlebars-ember': {
files: ['core/client/**/*.hbs'],
tasks: ['emberTemplates:dev']
},
ember: {
files: ['core/client/**/*.js'],
tasks: ['clean:tmp', 'transpile', 'concat_sourcemap']
},
concat: {
files: [
'core/client/*.js',
'core/client/**/*.js'
'core/clientold/*.js',
'core/clientold/**/*.js'
],
tasks: ['concat']
},
@ -167,7 +176,9 @@ var path = require('path'),
Ember: true,
Em: true,
DS: true,
$: true
$: true,
validator: true,
ic: true
},
node: false,
browser: true,
@ -295,18 +306,64 @@ var path = require('path'),
},
// ### grunt-contrib-handlebars
// Compile handlebars templates into a JST file for the admin client
// Compile handlebars templates into a JST file for the admin client (old)
handlebars: {
core: {
options: {
namespace: 'JST',
processName: function (filename) {
filename = filename.replace('core/client/tpl/', '');
filename = filename.replace('core/clientold/tpl/', '');
return filename.replace('.hbs', '');
}
},
files: {
'core/client/tpl/hbs-tpl.js': 'core/client/tpl/**/*.hbs'
'core/clientold/tpl/hbs-tpl.js': 'core/clientold/tpl/**/*.hbs'
}
}
},
// ### grunt-ember-templates
// Compiles handlebar templates for ember
emberTemplates: {
dev: {
options: {
templateBasePath: /core\/client\//,
templateFileExtensions: /\.hbs/,
templateRegistration: function (name, template) {
return grunt.config.process("define('ghost/") + name + "', ['exports'], function(__exports__){ __exports__['default'] = " + template + "; });";
}
},
files: {
"core/built/scripts/templates-ember.js": "core/client/templates/**/*.hbs"
}
}
},
// ### grunt-es6-module-transpiler
// Compiles Ember es6 modules
transpile: {
client: {
type: 'amd',
moduleName: function (path) {
return 'ghost/' + path;
},
files: [{
expand: true,
cwd: 'core/client/',
src: ['**/*.js'],
dest: '.tmp/ember-transpiled/'
}]
}
},
// ### grunt-es6-module-transpiler
// Compiles Ember es6 modules
concat_sourcemap: {
client: {
src: ['.tmp/ember-transpiled/**/*.js'],
dest: 'core/built/scripts/ghost-dev-ember.js',
options: {
sourcesContent: true
}
}
},
@ -329,11 +386,17 @@ var path = require('path'),
// ### grunt-contrib-clean
// Clean up files as part of other tasks
clean: {
built: {
src: ['core/built/**']
},
release: {
src: ['<%= paths.releaseBuild %>/**']
},
test: {
src: ['content/data/ghost-test.db']
},
tmp: {
src: ['.tmp/**']
}
},
@ -351,6 +414,11 @@ var path = require('path'),
src: ['**'],
dest: 'core/client/assets/',
expand: true
}, {
cwd: 'bower_components/ghost-ui/dist/',
src: ['**'],
dest: 'core/clientold/assets/',
expand: true
}]
},
prod: {
@ -364,6 +432,11 @@ var path = require('path'),
src: ['**'],
dest: 'core/client/assets/',
expand: true
}, {
cwd: 'bower_components/ghost-ui/dist/',
src: ['**'],
dest: 'core/clientold/assets/',
expand: true
}]
},
release: {
@ -377,6 +450,11 @@ var path = require('path'),
src: ['**'],
dest: 'core/client/assets/',
expand: true
}, {
cwd: 'bower_components/ghost-ui/dist/',
src: ['**'],
dest: 'core/clientold/assets/',
expand: true
}, {
expand: true,
src: buildGlob,
@ -406,8 +484,8 @@ var path = require('path'),
'core/built/scripts/vendor.js': [
'bower_components/jquery/dist/jquery.js',
'bower_components/jquery-ui/ui/jquery-ui.js',
'core/client/assets/lib/jquery-utils.js',
'core/client/assets/lib/uploader.js',
'core/clientold/assets/lib/jquery-utils.js',
'core/clientold/assets/lib/uploader.js',
'bower_components/lodash/dist/lodash.underscore.js',
'bower_components/backbone/backbone.js',
@ -425,8 +503,8 @@ var path = require('path'),
'core/shared/lib/showdown/extensions/ghostgfm.js',
// TODO: Remove or replace
'core/client/assets/vendor/shortcuts.js',
'core/client/assets/vendor/to-title-case.js',
'core/clientold/assets/vendor/shortcuts.js',
'core/clientold/assets/vendor/to-title-case.js',
'bower_components/Countable/Countable.js',
'bower_components/fastclick/lib/fastclick.js',
@ -434,32 +512,54 @@ var path = require('path'),
],
'core/built/scripts/helpers.js': [
'core/client/init.js',
'core/clientold/init.js',
'core/client/mobile-interactions.js',
'core/client/toggle.js',
'core/client/markdown-actions.js',
'core/client/helpers/index.js',
'core/client/assets/lib/editor/index.js',
'core/client/assets/lib/editor/markerManager.js',
'core/client/assets/lib/editor/uploadManager.js',
'core/client/assets/lib/editor/markdownEditor.js',
'core/client/assets/lib/editor/htmlPreview.js',
'core/client/assets/lib/editor/scrollHandler.js',
'core/client/assets/lib/editor/mobileCodeMirror.js'
'core/clientold/mobile-interactions.js',
'core/clientold/toggle.js',
'core/clientold/markdown-actions.js',
'core/clientold/helpers/index.js',
'core/clientold/assets/lib/editor/index.js',
'core/clientold/assets/lib/editor/markerManager.js',
'core/clientold/assets/lib/editor/uploadManager.js',
'core/clientold/assets/lib/editor/markdownEditor.js',
'core/clientold/assets/lib/editor/htmlPreview.js',
'core/clientold/assets/lib/editor/scrollHandler.js',
'core/clientold/assets/lib/editor/mobileCodeMirror.js'
],
'core/built/scripts/templates.js': [
'core/client/tpl/hbs-tpl.js'
'core/clientold/tpl/hbs-tpl.js'
],
'core/built/scripts/models.js': [
'core/client/models/**/*.js'
'core/clientold/models/**/*.js'
],
'core/built/scripts/views.js': [
'core/client/views/**/*.js',
'core/client/router.js'
'core/clientold/views/**/*.js',
'core/clientold/router.js'
]
}
},
'dev-ember': {
files: {
'core/built/scripts/vendor-ember.js': [
'core/client/assets/vendor/loader.js',
'bower_components/jquery/dist/jquery.js',
'bower_components/handlebars/handlebars.js',
'bower_components/ember/ember.js',
'bower_components/ember-resolver/dist/ember-resolver.js',
'bower_components/ic-ajax/dist/globals/main.js',
'bower_components/validator-js/validator.js',
'bower_components/codemirror/lib/codemirror.js',
'bower_components/codemirror/addon/mode/overlay.js',
'bower_components/codemirror/mode/markdown/markdown.js',
'bower_components/codemirror/mode/gfm/gfm.js',
'bower_components/showdown/src/showdown.js',
'bower_components/moment/moment.js',
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
'core/shared/lib/showdown/extensions/ghostgfm.js',
]
}
},
@ -468,8 +568,8 @@ var path = require('path'),
'core/built/scripts/ghost.js': [
'bower_components/jquery/dist/jquery.js',
'bower_components/jquery-ui/ui/jquery-ui.js',
'core/client/assets/lib/jquery-utils.js',
'core/client/assets/lib/uploader.js',
'core/clientold/assets/lib/jquery-utils.js',
'core/clientold/assets/lib/uploader.js',
'bower_components/lodash/dist/lodash.underscore.js',
'bower_components/backbone/backbone.js',
@ -487,35 +587,35 @@ var path = require('path'),
'core/shared/lib/showdown/extensions/ghostgfm.js',
// TODO: Remove or replace
'core/client/assets/vendor/shortcuts.js',
'core/client/assets/vendor/to-title-case.js',
'core/clientold/assets/vendor/shortcuts.js',
'core/clientold/assets/vendor/to-title-case.js',
'bower_components/Countable/Countable.js',
'bower_components/fastclick/lib/fastclick.js',
'bower_components/nprogress/nprogress.js',
'core/client/init.js',
'core/clientold/init.js',
'core/client/mobile-interactions.js',
'core/client/toggle.js',
'core/client/markdown-actions.js',
'core/client/helpers/index.js',
'core/clientold/mobile-interactions.js',
'core/clientold/toggle.js',
'core/clientold/markdown-actions.js',
'core/clientold/helpers/index.js',
'core/client/assets/lib/editor/index.js',
'core/client/assets/lib/editor/markerManager.js',
'core/client/assets/lib/editor/uploadManager.js',
'core/client/assets/lib/editor/markdownEditor.js',
'core/client/assets/lib/editor/htmlPreview.js',
'core/client/assets/lib/editor/scrollHandler.js',
'core/client/assets/lib/editor/mobileCodeMirror.js',
'core/clientold/assets/lib/editor/index.js',
'core/clientold/assets/lib/editor/markerManager.js',
'core/clientold/assets/lib/editor/uploadManager.js',
'core/clientold/assets/lib/editor/markdownEditor.js',
'core/clientold/assets/lib/editor/htmlPreview.js',
'core/clientold/assets/lib/editor/scrollHandler.js',
'core/clientold/assets/lib/editor/mobileCodeMirror.js',
'core/client/tpl/hbs-tpl.js',
'core/clientold/tpl/hbs-tpl.js',
'core/client/models/**/*.js',
'core/clientold/models/**/*.js',
'core/client/views/**/*.js',
'core/clientold/views/**/*.js',
'core/client/router.js'
'core/clientold/router.js'
]
}
}
@ -772,6 +872,11 @@ var path = require('path'),
console.log('Use the', 'stable'.bold, 'branch for live blogs.', 'Never'.bold, 'master!');
});
// ### Ember Build *(Utility Task)*
// All tasks related to building the Ember client code including transpiling ES6 modules and building templates
grunt.registerTask('emberBuild', 'Build Ember JS & templates for development',
['clean:tmp', 'emberTemplates:dev', 'transpile', 'concat_sourcemap']);
// ### Init assets
// `grunt init` - will run an initial asset build for you
@ -800,7 +905,7 @@ var path = require('path'),
// Compiles handlebars templates, concatenates javascript files for the admin UI into a handful of files instead
// of many files, and makes sure the bower dependencies are in the right place.
grunt.registerTask('default', 'Build JS & templates for development',
['update_submodules', 'handlebars', 'concat', 'copy:dev']);
['update_submodules', 'handlebars', 'concat', 'copy:dev', 'emberBuild']);
// ### Live reload
// `grunt dev` - build assets on the fly whilst developing
@ -814,7 +919,7 @@ var path = require('path'),
//
// Note that the current implementation of watch only works with casper, not other themes.
grunt.registerTask('dev', 'Dev Mode; watch files and restart server on changes',
['handlebars', 'concat', 'copy:dev', 'express:dev', 'watch']);
['handlebars', 'concat', 'copy:dev', 'emberBuild', 'express:dev', 'watch']);
// ### Release
// Run `grunt release` to create a Ghost release zip file.

View file

@ -4,9 +4,12 @@
"backbone": "1.0.0",
"codemirror": "4.0.1",
"Countable": "2.0.2",
"ember": "1.5.0",
"ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#181251821cf513bb58d3e192faa13245a816f75e",
"fastclick": "1.0.0",
"ghost-ui": "0.1.3",
"handlebars": "1.3.0",
"ic-ajax": "1.0.1",
"jquery": "1.11.0",
"jquery-file-upload": "9.5.6",
"jquery-hammerjs": "1.0.1",
@ -16,5 +19,8 @@
"nprogress": "0.1.2",
"showdown": "https://github.com/ErisDS/showdown.git#v0.3.2-ghost",
"validator-js": "3.4.0"
},
"resolutions": {
"ember": "~1.4.0"
}
}
}

28
core/client/app.js Executable file
View file

@ -0,0 +1,28 @@
import Resolver from 'ember/resolver';
import initFixtures from 'ghost/fixtures/init';
import {currentUser, injectCurrentUser} from 'ghost/initializers/current-user';
import {registerNotifications, injectNotifications} from 'ghost/initializers/notifications';
import 'ghost/utils/link-view';
import 'ghost/utils/text-field';
var App = Ember.Application.extend({
/**
* These are debugging flags, they are useful during development
*/
LOG_ACTIVE_GENERATION: true,
LOG_MODULE_RESOLVER: true,
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true,
LOG_VIEW_LOOKUPS: true,
modulePrefix: 'ghost',
Resolver: Resolver['default']
});
initFixtures();
App.initializer(currentUser);
App.initializer(injectCurrentUser);
App.initializer(registerNotifications);
App.initializer(injectNotifications);
export default App;

View file

@ -0,0 +1,74 @@
/*
Cosmetic changes to ghost styles, that help during development.
The contents should be solved properly or moved into ghost-ui package.
*/
#entry-markdown,
.entry-preview,
.CodeMirror.cm-s-default {
height: 500px !important;
}
.editor input {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
/*
By default nav menu should be displayed as it's visibility is controllerd
by GhostPopover
*/
.navbar .subnav ul {
display: block;
}
/*
Styles for GhostPopoverComponent
*/
.ghost-popover {
display: none;
}
.ghost-popover.open {
display: block;
}
.fade-in {
animation: fadein 0.5s;
-moz-animation: fadein 0.5s; /* Firefox */
-webkit-animation: fadein 0.5s; /* Safari and Chrome */
-o-animation: fadein 0.5s; /* Opera */
}
@keyframes fadein {
from {
opacity:0;
}
to {
opacity:1;
}
}
@-moz-keyframes fadein { /* Firefox */
from {
opacity:0;
}
to {
opacity:1;
}
}
@-webkit-keyframes fadein { /* Safari and Chrome */
from {
opacity:0;
}
to {
opacity:1;
}
}
@-o-keyframes fadein { /* Opera */
from {
opacity:0;
}
to {
opacity: 1;
}
}

75
core/client/assets/vendor/loader.js vendored Normal file
View file

@ -0,0 +1,75 @@
var define, requireModule, require, requirejs;
(function() {
var registry = {}, seen = {}, state = {};
var FAILED = false;
define = function(name, deps, callback) {
registry[name] = {
deps: deps,
callback: callback
};
};
requirejs = require = requireModule = function(name) {
if (state[name] !== FAILED &&
seen.hasOwnProperty(name)) {
return seen[name];
}
if (!registry.hasOwnProperty(name)) {
throw new Error('Could not find module ' + name);
}
var mod = registry[name];
var deps = mod.deps;
var callback = mod.callback;
var reified = [];
var exports;
var value;
var loaded = false;
seen[name] = { }; // enable run-time cycles
try {
for (var i=0, l=deps.length; i<l; i++) {
if (deps[i] === 'exports') {
reified.push(exports = {});
} else {
reified.push(requireModule(resolve(deps[i], name)));
}
}
value = callback.apply(this, reified);
loaded = true;
} finally {
if (!loaded) {
state[name] = FAILED;
}
}
return seen[name] = exports || value;
};
function resolve(child, name) {
if (child.charAt(0) !== '.') { return child; }
var parts = child.split('/');
var parentBase = name.split('/').slice(0, -1);
for (var i = 0, l = parts.length; i < l; i++) {
var part = parts[i];
if (part === '..') { parentBase.pop(); }
else if (part === '.') { continue; }
else { parentBase.push(part); }
}
return parentBase.join('/');
}
requirejs._eak_seen = registry;
requirejs.clear = function(){
requirejs._eak_seen = registry = {};
seen = {};
};
})();

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);
};
var Codemirror = 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')
});
export default Codemirror;

View file

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

View file

@ -0,0 +1,5 @@
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: false
});

View file

@ -0,0 +1,13 @@
var BlurTextField = Ember.TextField.extend({
selectOnClick: false,
click: function (event) {
if (this.get('selectOnClick')) {
event.currentTarget.select();
}
},
focusOut: function () {
this.sendAction('action', this.get('value'));
}
});
export default BlurTextField;

View file

@ -0,0 +1,23 @@
var FileUpload = Ember.Component.extend({
_file: null,
uploadButtonText: 'Text',
uploadButtonDisabled: true,
change: function (event) {
this.set('uploadButtonDisabled', false);
this.sendAction('onAdd');
this._file = event.target.files[0];
},
actions: {
upload: function () {
var self = this;
if (!this.uploadButtonDisabled && self._file) {
self.sendAction('onUpload', self._file);
}
// Prevent double post by disabling the button.
this.set('uploadButtonDisabled', true);
}
}
});
export default FileUpload;

View file

@ -0,0 +1,13 @@
export default Ember.View.extend({
tagName: 'form',
attributeBindings: ['enctype'],
reset: function () {
this.$().get(0).reset();
},
didInsertElement: function () {
this.get('controller').on('reset', this, this.reset);
},
willClearRender: function () {
this.get('controller').off('reset', this, this.reset);
}
});

View file

@ -0,0 +1,21 @@
var NotificationComponent = Ember.Component.extend({
classNames: ['js-bb-notification'],
didInsertElement: function () {
var self = this;
self.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
/* jshint unused: false */
self.notifications.removeObject(self.get('message'));
});
},
actions: {
closeNotification: function () {
var self = this;
self.notifications.removeObject(self.get('message'));
}
}
});
export default NotificationComponent;

View file

@ -0,0 +1,7 @@
var NotificationsComponent = Ember.Component.extend({
tagName: 'aside',
classNames: 'notifications',
messages: Ember.computed.alias('notifications')
});
export default NotificationsComponent;

View file

@ -0,0 +1,8 @@
var GhostPopover = Ember.Component.extend({
classNames: 'ghost-popover',
classNameBindings: ['open'],
open: false
});
export default GhostPopover;

View file

@ -0,0 +1,59 @@
var ModalDialog = Ember.Component.extend({
didInsertElement: function () {
this.$('#modal-container').fadeIn(50);
this.$('.modal-background').show().fadeIn(10, function () {
$(this).addClass('in');
});
this.$('.js-modal').addClass('in');
},
willDestroyElement: function () {
this.$('.js-modal').removeClass('in');
this.$('.modal-background').removeClass('in');
return this._super();
},
actions: {
closeModal: function () {
this.sendAction();
},
confirm: function (type) {
var func = this.get('confirm.' + type + '.func');
if (typeof func === 'function') {
func();
}
this.sendAction();
}
},
klass: function () {
var classNames = [];
classNames.push(this.get('type') ? 'modal-' + this.get('type') : 'modal');
if (this.get('style')) {
this.get('style').split(',').forEach(function (style) {
classNames.push('modal-style-' + style);
});
}
classNames.push(this.get('animation'));
return classNames.join(' ');
}.property('type', 'style', 'animation'),
acceptButtonClass: function () {
return this.get('confirm.accept.buttonClass') ? this.get('confirm.accept.buttonClass') : 'button-add';
}.property('confirm.accept.buttonClass'),
rejectButtonClass: function () {
return this.get('confirm.reject.buttonClass') ? this.get('confirm.reject.buttonClass') : 'button-delete';
}.property('confirm.reject.buttonClass')
});
export default ModalDialog;

View file

@ -0,0 +1,32 @@
/*global console */
import ModalDialog from 'ghost/components/modal-dialog';
var UploadModal = ModalDialog.extend({
layoutName: 'components/modal-dialog',
didInsertElement: function () {
this._super();
// @TODO: get this real
console.log('UploadController:afterRender');
// var filestorage = $('#' + this.options.model.id).data('filestorage');
// this.$('.js-drop-zone').upload({fileStorage: filestorage});
},
actions: {
closeModal: function () {
this.sendAction();
},
confirm: function (type) {
var func = this.get('confirm.' + type + '.func');
if (typeof func === 'function') {
func();
}
this.sendAction();
}
},
});
export default UploadModal;

View file

@ -0,0 +1,10 @@
var ApplicationController = Ember.Controller.extend({
isLoggedOut: Ember.computed.match('currentPath', /(signin|signup|forgotten|reset)/),
actions: {
toggleMenu: function () {
this.toggleProperty('showMenu');
}
}
});
export default ApplicationController;

View file

@ -0,0 +1,21 @@
/*global console, alert */
var ForgottenController = Ember.Controller.extend({
email: '',
actions: {
submit: function () {
var self = this;
self.user.fetchForgottenPasswordFor(this.email)
.then(function () {
alert('@TODO Notification: Success');
self.transitionToRoute('signin');
})
.catch(function (response) {
alert('@TODO');
console.log(response);
});
}
}
});
export default ForgottenController;

View file

@ -0,0 +1,55 @@
/*global alert */
var DeleteAllController = Ember.Controller.extend({
confirm: {
accept: {
func: function () {
// @TODO make the below real :)
alert('Deleting everything!');
// $.ajax({
// url: Ghost.paths.apiRoot + '/db/',
// type: 'DELETE',
// headers: {
// 'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
// },
// success: function onSuccess(response) {
// if (!response) {
// throw new Error('No response received from server.');
// }
// if (!response.message) {
// throw new Error(response.detail || 'Unknown error');
// }
// Ghost.notifications.addItem({
// type: 'success',
// message: response.message,
// status: 'passive'
// });
// },
// error: function onError(response) {
// var responseText = JSON.parse(response.responseText),
// message = responseText && responseText.error ? responseText.error : 'unknown';
// Ghost.notifications.addItem({
// type: 'error',
// message: ['A problem was encountered while deleting content from your blog. Error: ', message].join(''),
// status: 'passive'
// });
// }
// });
},
text: "Delete",
buttonClass: "button-delete"
},
reject: {
func: function () {
return true;
},
text: "Cancel",
buttonClass: "button"
}
}
});
export default DeleteAllController;

View file

@ -0,0 +1,42 @@
/*global alert */
var DeletePostController = Ember.Controller.extend({
confirm: {
accept: {
func: function () {
// @TODO: make this real
alert('Deleting post');
// self.model.destroy({
// wait: true
// }).then(function () {
// // Redirect to content screen if deleting post from editor.
// if (window.location.pathname.indexOf('editor') > -1) {
// window.location = Ghost.paths.subdir + '/ghost/content/';
// }
// Ghost.notifications.addItem({
// type: 'success',
// message: 'Your post has been deleted.',
// status: 'passive'
// });
// }, function () {
// Ghost.notifications.addItem({
// type: 'error',
// message: 'Your post could not be deleted. Please try again.',
// status: 'passive'
// });
// });
},
text: "Delete",
buttonClass: "button-delete"
},
reject: {
func: function () {
return true;
},
text: "Cancel",
buttonClass: "button"
}
},
});
export default DeletePostController;

View file

@ -0,0 +1,14 @@
var UploadController = Ember.Controller.extend({
confirm: {
reject: {
func: function () { // The function called on rejection
return true;
},
buttonClass: true,
text: "Cancel" // The reject button text
}
}
});
export default UploadController;

View file

@ -0,0 +1,176 @@
import {parseDateString, formatDate} from 'ghost/utils/date-formatting';
var equal = Ember.computed.equal;
var PostController = Ember.ObjectController.extend({
isPublished: equal('status', 'published'),
isDraft: equal('status', 'draft'),
isEditingSettings: false,
isStaticPage: function (key, val) {
if (arguments.length > 1) {
this.set('model.page', val ? 1 : 0);
this.get('model').save('page').then(function () {
this.notifications.showSuccess('Succesfully converted ' + (val ? 'to static page' : 'to post'));
}, this.notifications.showErrors);
}
return !!this.get('model.page');
}.property('model.page'),
isOnServer: function () {
return this.get('model.id') !== undefined;
}.property('model.id'),
newSlugBinding: Ember.Binding.oneWay('model.slug'),
slugPlaceholder: null,
// Requests a new slug when the title was changed
updateSlugPlaceholder: function () {
var model,
self = this,
title = this.get('title');
// If there's a title present we want to
// validate it against existing slugs in the db
// and then update the placeholder value.
if (title) {
model = self.get('model');
model.generateSlug().then(function (slug) {
self.set('slugPlaceholder', slug);
}, function () {
self.notifications.showWarn('Unable to generate a slug for "' + title + '"');
});
} else {
// If there's no title set placeholder to blank
// and don't make an ajax request to server
// for a proper slug (as there won't be any).
self.set('slugPlaceholder', '');
}
}.observes('model.title'),
publishedAt: null,
publishedAtChanged: function () {
this.set('publishedAt', formatDate(this.get('model.published_at')));
}.observes('model.published_at'),
actions: {
editSettings: function () {
this.toggleProperty('isEditingSettings');
if (this.get('isEditingSettings')) {
//Stop editing if the user clicks outside the settings view
Ember.run.next(this, function () {
var self = this;
// @TODO has a race condition with click on the editSettings action
$(document).one('click', function () {
self.toggleProperty('isEditingSettings');
});
});
}
},
updateSlug: function () {
var newSlug = this.get('newSlug'),
slug = this.get('model.slug'),
placeholder = this.get('slugPlaceholder'),
self = this;
newSlug = (!newSlug && placeholder) ? placeholder : newSlug;
// Ignore unchanged slugs
if (slug === newSlug) {
return;
}
//reset to model's slug on empty string
if (!newSlug) {
this.set('newSlug', slug);
return;
}
//Validation complete
this.set('model.slug', newSlug);
// If the model doesn't currently
// exist on the server
// then just update the model's value
if (!this.get('isOnServer')) {
return;
}
this.get('model').save('slug').then(function () {
self.notifications.showSuccess('Permalink successfully changed to <strong>' + this.get('model.slug') + '</strong>.');
}, this.notifications.showErrors);
},
updatePublishedAt: function (userInput) {
var errMessage = '',
newPubDate = formatDate(parseDateString(userInput)),
pubDate = this.get('publishedAt'),
newPubDateMoment,
pubDateMoment;
// if there is no new pub date, mark that until the post is published,
// when we'll fill in with the current time.
if (!newPubDate) {
this.set('publishedAt', '');
return;
}
// Check for missing time stamp on new data
// If no time specified, add a 12:00
if (newPubDate && !newPubDate.slice(-5).match(/\d+:\d\d/)) {
newPubDate += " 12:00";
}
newPubDateMoment = parseDateString(newPubDate);
// If there was a published date already set
if (pubDate) {
// Check for missing time stamp on current model
// If no time specified, add a 12:00
if (!pubDate.slice(-5).match(/\d+:\d\d/)) {
pubDate += " 12:00";
}
pubDateMoment = parseDateString(pubDate);
// Quit if the new date is the same
if (pubDateMoment.isSame(newPubDateMoment)) {
return;
}
}
// Validate new Published date
if (!newPubDateMoment.isValid() || newPubDate.substr(0, 12) === "Invalid date") {
errMessage = 'Published Date must be a valid date with format: DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
}
if (newPubDateMoment.diff(new Date(), 'h') > 0) {
errMessage = 'Published Date cannot currently be in the future.';
}
if (errMessage) {
// Show error message
this.notifications.showError(errMessage);
//Hack to push a "change" when it's actually staying
// the same.
//This alerts the listener on post-settings-menu
this.notifyPropertyChange('publishedAt');
return;
}
//Validation complete
this.set('model.published_at', newPubDateMoment.toDate());
// If the model doesn't currently
// exist on the server
// then just update the model's value
if (!this.get('isOnServer')) {
return;
}
this.get('model').save('published_at').then(function () {
this.notifications.showSuccess('Publish date successfully changed to <strong>' + this.get('publishedAt') + '</strong>.');
}, this.notifications.showErrors);
}
}
});
export default PostController;

View file

@ -0,0 +1,30 @@
/*global alert, console */
var ResetController = Ember.Controller.extend({
passwords: {
newPassword: '',
ne2Password: ''
},
token: '',
submitButtonDisabled: false,
actions: {
submit: function () {
var self = this;
this.set('submitButtonDisabled', true);
this.user.resetPassword(this.passwords, this.token)
.then(function () {
alert('@TODO Notification : Success');
self.transitionToRoute('signin');
})
.catch(function (response) {
alert('@TODO Notification : Failure');
console.log(response);
})
.finally(function () {
self.set('submitButtonDisabled', false);
});
}
}
});
export default ResetController;

View file

@ -0,0 +1,37 @@
/*global alert, console */
var Debug = Ember.Controller.extend(Ember.Evented, {
uploadButtonText: 'Import',
actions: {
importData: function (file) {
var self = this;
this.set('uploadButtonText', 'Importing');
this.get('model').importFrom(file)
.then(function (response) {
console.log(response);
alert('@TODO: success');
})
.catch(function (response) {
console.log(response);
alert('@TODO: error');
})
.finally(function () {
self.set('uploadButtonText', 'Import');
self.trigger('reset');
});
},
sendTestEmail: function () {
this.get('model').sendTestEmail()
.then(function (response) {
console.log(response);
alert('@TODO: success');
})
.catch(function (response) {
console.log(response);
alert('@TODO: error');
});
}
}
});
export default Debug;

View file

@ -0,0 +1,59 @@
var elementLookup = {
title: '#blog-title',
description: '#blog-description',
email: '#email-address',
postsPerPage: '#postsPerPage'
};
var SettingsGeneralController = Ember.ObjectController.extend({
isDatedPermalinks: function (key, value) {
// setter
if (arguments.length > 1) {
this.set('permalinks', value ? '/:year/:month/:day/:slug/' : '/:slug/');
}
// getter
var slugForm = this.get('permalinks');
return slugForm !== '/:slug/';
}.property('permalinks'),
actions: {
'save': function () {
// Validate and save settings
var model = this.get('model'),
// @TODO: Don't know how to scope this to this controllers view because this.view is null
errs = model.validate();
if (errs.length > 0) {
// Set the actual element from this view based on the error
errs.forEach(function (err) {
// @TODO: Probably should still be scoped to this controllers root element.
err.el = $(elementLookup[err.el]);
});
// Let the applicationRoute handle validation errors
this.send('handleValidationErrors', errs);
} else {
model.save().then(function () {
// @TODO: Notification of success
window.alert('Saved data!');
}, function () {
// @TODO: Notification of error
window.alert('Error saving data');
});
}
},
'uploadLogo': function () {
// @TODO: Integrate with Modal component
},
'uploadCover': function () {
// @TODO: Integrate with Modal component
}
}
});
export default SettingsGeneralController;

View file

@ -0,0 +1,57 @@
/*global alert */
var SettingsUserController = Ember.Controller.extend({
cover: function () {
// @TODO: add {{asset}} subdir path
return this.user.getWithDefault('cover', '/shared/img/user-cover.png');
}.property('user.cover'),
coverTitle: function () {
return this.get('user.name') + '\'s Cover Image';
}.property('user.name'),
image: function () {
// @TODO: add {{asset}} subdir path
return 'background-image: url(' + this.user.getWithDefault('image', '/shared/img/user-image.png') + ')';
}.property('user.image'),
actions: {
save: function () {
alert('@TODO: Saving user...');
if (this.user.validate().get('isValid')) {
this.user.save().then(function (response) {
alert('Done saving' + JSON.stringify(response));
}, function () {
alert('Error saving.');
});
} else {
alert('Errors found! ' + JSON.stringify(this.user.get('errors')));
}
},
password: function () {
alert('@TODO: Changing password...');
var passwordProperties = this.getProperties('password', 'newPassword', 'ne2Password');
if (this.user.validatePassword(passwordProperties).get('passwordIsValid')) {
this.user.saveNewPassword(passwordProperties).then(function () {
alert('Success!');
// Clear properties from view
this.setProperties({
'password': '',
'newpassword': '',
'ne2password': ''
});
}.bind(this), function (errors) {
alert('Errors ' + JSON.stringify(errors));
});
} else {
alert('Errors found! ' + JSON.stringify(this.user.get('passwordErrors')));
}
}
}
});
export default SettingsUserController;

View file

@ -0,0 +1,60 @@
import postFixtures from 'ghost/fixtures/posts';
import userFixtures from 'ghost/fixtures/users';
import settingsFixtures from 'ghost/fixtures/settings';
var response = function (responseBody, status) {
status = status || 200;
var textStatus = (status === 200) ? 'success' : 'error';
return {
response: responseBody,
jqXHR: { status: status },
textStatus: textStatus
};
};
var user = function (status) {
return response(userFixtures.findBy('id', 1), status);
};
var post = function (id, status) {
return response(postFixtures.findBy('id', id), status);
};
var posts = function (status) {
return response({
'posts': postFixtures,
'page': 1,
'limit': 15,
'pages': 1,
'total': 2
}, status);
};
var settings = function (status) {
return response(settingsFixtures, status);
};
var defineFixtures = function (status) {
ic.ajax.defineFixture('/ghost/api/v0.1/posts', posts(status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/1', post(1, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/2', post(2, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/3', post(3, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/4', post(4, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/slug/test%20title/', response('generated-slug', status));
ic.ajax.defineFixture('/ghost/api/v0.1/signin', user(status));
ic.ajax.defineFixture('/ghost/api/v0.1/users/me/', user(status));
ic.ajax.defineFixture('/ghost/changepw/', response({
msg: 'Password changed successfully'
}));
ic.ajax.defineFixture('/ghost/api/v0.1/forgotten/', response({
redirect: '/ghost/signin/'
}));
ic.ajax.defineFixture('/ghost/api/v0.1/reset/', response({
msg: 'Password changed successfully'
}));
ic.ajax.defineFixture('/ghost/api/v0.1/settings/?type=blog,theme,app', settings(status));
};
export default defineFixtures;

View file

@ -0,0 +1,269 @@
var posts = [
{
"id": 4,
"uuid": "4dc16b9e-bf90-44c9-97c5-40a0a81e8297",
"title": "This post is featured",
"slug": "this-post-is-featured",
"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>",
"image": null,
"featured": 1,
"page": 0,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": "2014-02-15T23:27:08.000Z",
"published_by": 1,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "Bill Murray",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 3,
"uuid": "4dc16b9e-bf90-44c9-97c5-40a0a81e8297",
"title": "Example page entry",
"slug": "example-page-entry",
"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>",
"image": null,
"featured": 0,
"page": 1,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "Slimer",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 2,
"uuid": "4dc1cb9e-bf90-44c9-97c5-40a8381e8297",
"title": "Dummy draft post",
"slug": "dummy-draft-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>",
"image": null,
"featured": 0,
"page": 0,
"status": "draft",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 1,
"uuid": "4b96025d-050c-47ff-8bd4-047e4843b302",
"title": "Welcome to Ghost",
"slug": "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>",
"image": null,
"featured": 0,
"page": 0,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T20:02:01.000Z",
"created_by": 1,
"updated_at": "2014-02-15T20:02:01.000Z",
"updated_by": 1,
"published_at": "2014-02-15T20:02:01.000Z",
"published_by": 1,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
{
"id": 1,
"uuid": "406edaaf-5b1c-4199-b297-2af90b1de1a7",
"name": "Getting Started",
"slug": "getting-started",
"description": null,
"parent_id": null,
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:01.000Z",
"created_by": 1,
"updated_at": "2014-02-15T20:02:01.000Z",
"updated_by": 1
}
]
}
];
export default posts;

View file

@ -0,0 +1,24 @@
var settings = {
"title": "Ghost",
"description": "Just a blogging platform.",
"email": "ghost@tryghost.org",
"logo": "",
"cover": "",
"defaultLang": "en_US",
"postsPerPage": "6",
"forceI18n": "true",
"permalinks": "/:slug/",
"activeTheme": "casper",
"activeApps": "[]",
"installedApps": "[]",
"availableThemes": [
{
"name": "casper",
"package": false,
"active": true
}
],
"availableApps": []
};
export default settings;

View file

@ -0,0 +1,23 @@
var users = [
{
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "some-user",
"slug": "some-user",
"email": "some@email.com",
"image": undefined,
"cover": undefined,
"bio": "Example bio",
"website": "",
"location": "Imaginationland",
"accessibility": undefined,
"status": "active",
"language": "en_US",
"meta_title": undefined,
"meta_description": undefined,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-03-11T14:06:43.000Z"
}
];
export default users;

View file

@ -0,0 +1,7 @@
import count from 'ghost/utils/word-count';
var countWords = Ember.Handlebars.makeBoundHelper(function (markdown) {
return count(markdown || '');
});
export default countWords;

View file

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

View file

@ -0,0 +1,9 @@
/* global moment */
var formatTimeago = 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
});
export default formatTimeago;

View file

@ -0,0 +1,26 @@
import User from 'ghost/models/user';
import userFixtures from 'ghost/fixtures/users';
var currentUser = {
name: 'currentUser',
initialize: function (container) {
container.register('user:current', User);
}
};
var injectCurrentUser = {
name: 'injectCurrentUser',
initialize: function (container) {
if (container.lookup('user:current')) {
// @TODO: remove userFixture
container.lookup('user:current').setProperties(userFixtures.findBy('id', 1));
container.injection('route', 'user', 'user:current');
container.injection('controller', 'user', 'user:current');
}
}
};
export {currentUser, injectCurrentUser};

View file

@ -0,0 +1,21 @@
import Notifications from 'ghost/utils/notifications';
var registerNotifications = {
name: 'registerNotifications',
initialize: function (container, application) {
application.register('notifications:main', Notifications);
}
};
var injectNotifications = {
name: 'injectNotifications',
initialize: function (container, application) {
application.inject('controller', 'notifications', 'notifications:main');
application.inject('component', 'notifications', 'notifications:main');
application.inject('route', 'notifications', 'notifications:main');
}
};
export {registerNotifications, injectNotifications};

View file

@ -0,0 +1,27 @@
// mixin used for routes that need to set a css className on the body tag
var styleBody = Ember.Mixin.create({
activate: function () {
var cssClasses = this.get('classNames');
if (cssClasses) {
Ember.run.schedule('afterRender', null, function () {
cssClasses.forEach(function (curClass) {
Ember.$('body').addClass(curClass);
});
});
}
},
deactivate: function () {
var cssClasses = this.get('classNames');
Ember.run.schedule('afterRender', null, function () {
cssClasses.forEach(function (curClass) {
Ember.$('body').removeClass(curClass);
});
});
}
});
export default styleBody;

View file

@ -1,35 +1,35 @@
/*global Ghost, _, Backbone, NProgress */
(function () {
"use strict";
NProgress.configure({ showSpinner: false });
function ghostPaths() {
var path = window.location.pathname,
subdir = path.substr(0, path.search('/ghost/'));
// Adds in a call to start a loading bar
// This is sets up a success function which completes the loading bar
function wrapSync(method, model, options) {
if (options !== undefined && _.isObject(options)) {
NProgress.start();
return {
subdir: subdir,
adminRoot: subdir + '/ghost',
apiRoot: subdir + '/ghost/api/v0.1'
};
}
/*jshint validthis:true */
var self = this,
oldSuccess = options.success;
/*jshint validthis:false */
var BaseModel = Ember.Object.extend({
options.success = function () {
NProgress.done();
return oldSuccess.apply(self, arguments);
};
}
fetch: function () {
return ic.ajax.request(this.url, {
type: 'GET'
});
},
/*jshint validthis:true */
return Backbone.sync.call(this, method, model, options);
save: function () {
return ic.ajax.request(this.url, {
type: 'PUT',
dataType: 'json',
// @TODO: This is passing _oldWillDestory and _willDestroy and should not.
data: JSON.stringify(this.getProperties(Ember.keys(this)))
});
}
});
Ghost.ProgressModel = Backbone.Model.extend({
sync: wrapSync
});
BaseModel.apiRoot = ghostPaths().apiRoot;
BaseModel.subdir = ghostPaths().subdir;
BaseModel.adminRoot = ghostPaths().adminRoot;
Ghost.ProgressCollection = Backbone.Collection.extend({
sync: wrapSync
});
}());
export default BaseModel;

View file

@ -1,83 +1,56 @@
/*global Ghost, _, Backbone, JSON */
(function () {
'use strict';
import BaseModel from 'ghost/models/base';
Ghost.Models.Post = Ghost.ProgressModel.extend({
var PostModel = BaseModel.extend({
url: BaseModel.apiRoot + '/posts/',
defaults: {
status: 'draft'
},
generateSlug: function () {
// @TODO Make this request use this.get('title') once we're an actual user
var url = this.get('url') + 'slug/' + encodeURIComponent('test title') + '/';
return ic.ajax.request(url, {
type: 'GET'
});
},
blacklist: ['published', 'draft'],
save: function (properties) {
var url = this.url,
self = this,
type,
validationErrors = this.validate();
parse: function (resp) {
if (resp.posts) {
resp = resp.posts[0];
}
if (resp.status) {
resp.published = resp.status === 'published';
resp.draft = resp.status === 'draft';
}
if (resp.tags) {
return resp;
}
return resp;
},
validate: function (attrs) {
if (_.isEmpty(attrs.title)) {
return 'You must specify a title for the post.';
}
},
addTag: function (tagToAdd) {
var tags = this.get('tags') || [];
tags.push(tagToAdd);
this.set('tags', tags);
},
removeTag: function (tagToRemove) {
var tags = this.get('tags') || [];
tags = _.reject(tags, function (tag) {
return tag.id === tagToRemove.id || tag.name === tagToRemove.name;
if (validationErrors.length) {
return Ember.RSVP.Promise(function (resolve, reject) {
return reject(validationErrors);
});
this.set('tags', tags);
},
sync: function (method, model, options) {
//wrap post in {posts: [{...}]}
if (method === 'create' || method === 'update') {
options.data = JSON.stringify({posts: [this.attributes]});
options.contentType = 'application/json';
options.url = model.url() + '?include=tags';
}
return Backbone.Model.prototype.sync.apply(this, arguments);
}
});
Ghost.Collections.Posts = Backbone.Collection.extend({
currentPage: 1,
totalPages: 0,
totalPosts: 0,
nextPage: 0,
prevPage: 0,
url: Ghost.paths.apiRoot + '/posts/',
model: Ghost.Models.Post,
parse: function (resp) {
if (_.isArray(resp.posts)) {
this.limit = resp.meta.pagination.limit;
this.currentPage = resp.meta.pagination.page;
this.totalPages = resp.meta.pagination.pages;
this.totalPosts = resp.meta.pagination.total;
this.nextPage = resp.meta.pagination.next;
this.prevPage = resp.meta.pagination.prev;
return resp.posts;
}
return resp;
//If specific properties are being saved,
//this is an edit. Otherwise, it's an add.
if (properties && properties.length > 0) {
type = 'PUT';
url += this.get('id');
} else {
type = 'POST';
properties = Ember.keys(this);
}
});
}());
return ic.ajax.request(url, {
type: type,
data: this.getProperties(properties)
}).then(function (model) {
return self.setProperties(model);
});
},
validate: function () {
var validationErrors = [];
if (!(this.get('title') && this.get('title').length)) {
validationErrors.push({
message: "You must specify a title for the post."
});
}
return validationErrors;
}
});
export default PostModel;

View file

@ -1,33 +1,76 @@
/*global Backbone, Ghost, _ */
(function () {
'use strict';
//id:0 is used to issue PUT requests
Ghost.Models.Settings = Ghost.ProgressModel.extend({
url: Ghost.paths.apiRoot + '/settings/?type=blog,theme,app',
id: '0',
var validator = window.validator;
parse: function (response) {
var result = _.reduce(response.settings, function (settings, setting) {
settings[setting.key] = setting.value;
import BaseModel from 'ghost/models/base';
return settings;
}, {});
var SettingsModel = BaseModel.extend({
url: BaseModel.apiRoot + '/settings/?type=blog,theme,app',
return result;
},
title: null,
description: null,
email: null,
logo: null,
cover: null,
defaultLang: null,
postsPerPage: null,
forceI18n: null,
permalinks: null,
activeTheme: null,
activeApps: null,
installedApps: null,
availableThemes: null,
availableApps: null,
sync: function (method, model, options) {
var settings = _.map(this.attributes, function (value, key) {
return { key: key, value: value };
});
//wrap settings in {settings: [{...}]}
if (method === 'update') {
options.data = JSON.stringify({settings: settings});
options.contentType = 'application/json';
}
validate: function () {
var validationErrors = [],
postsPerPage;
return Backbone.Model.prototype.sync.apply(this, arguments);
if (!validator.isLength(this.get('title'), 0, 150)) {
validationErrors.push({message: "Title is too long", el: 'title'});
}
});
}());
if (!validator.isLength(this.get('description'), 0, 200)) {
validationErrors.push({message: "Description is too long", el: 'description'});
}
if (!validator.isEmail(this.get('email')) || !validator.isLength(this.get('email'), 0, 254)) {
validationErrors.push({message: "Please supply a valid email address", el: 'email'});
}
postsPerPage = this.get('postsPerPage');
if (!validator.isInt(postsPerPage) || postsPerPage > 1000) {
validationErrors.push({message: "Please use a number less than 1000", el: 'postsPerPage'});
}
if (!validator.isInt(postsPerPage) || postsPerPage < 0) {
validationErrors.push({message: "Please use a number greater than 0", el: 'postsPerPage'});
}
return validationErrors;
},
exportPath: BaseModel.adminRoot + '/export/',
importFrom: function (file) {
var formData = new FormData();
formData.append('importfile', file);
return ic.ajax.request(BaseModel.apiRoot + '/db/', {
headers: {
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
},
type: 'POST',
data: formData,
dataType: 'json',
cache: false,
contentType: false,
processData: false
});
},
sendTestEmail: function () {
return ic.ajax.request(BaseModel.apiRoot + '/mail/test/', {
type: 'POST',
headers: {
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
}
});
}
});
export default SettingsModel;

View file

@ -1,32 +1,124 @@
/*global Ghost,Backbone */
(function () {
'use strict';
import BaseModel from 'ghost/models/base';
Ghost.Models.User = Ghost.ProgressModel.extend({
url: Ghost.paths.apiRoot + '/users/me/',
var UserModel = BaseModel.extend({
url: BaseModel.apiRoot + '/users/me/',
forgottenUrl: BaseModel.apiRoot + '/forgotten/',
resetUrl: BaseModel.apiRoot + '/reset/',
parse: function (resp) {
// unwrap user from {users: [{...}]}
if (resp.users) {
resp = resp.users[0];
}
save: function () {
return ic.ajax.request(this.url, {
type: 'POST',
data: this.getProperties(Ember.keys(this))
});
},
return resp;
},
validate: function () {
var validationErrors = [];
sync: function (method, model, options) {
// wrap user in {users: [{...}]}
if (method === 'create' || method === 'update') {
options.data = JSON.stringify({users: [this.attributes]});
options.contentType = 'application/json';
}
return Backbone.Model.prototype.sync.apply(this, arguments);
if (!validator.isLength(this.get('name'), 0, 150)) {
validationErrors.push({message: "Name is too long"});
}
});
// Ghost.Collections.Users = Backbone.Collection.extend({
// url: Ghost.paths.apiRoot + '/users/'
// });
if (!validator.isLength(this.get('bio'), 0, 200)) {
validationErrors.push({message: "Bio is too long"});
}
}());
if (!validator.isEmail(this.get('email'))) {
validationErrors.push({message: "Please supply a valid email address"});
}
if (!validator.isLength(this.get('location'), 0, 150)) {
validationErrors.push({message: "Location is too long"});
}
if (this.get('website').length) {
if (!validator.isURL(this.get('website')) ||
!validator.isLength(this.get('website'), 0, 2000)) {
validationErrors.push({message: "Please use a valid url"});
}
}
if (validationErrors.length > 0) {
this.set('isValid', false);
} else {
this.set('isValid', true);
}
this.set('errors', validationErrors);
return this;
},
saveNewPassword: function (password) {
return ic.ajax.request(BaseModel.subdir + '/ghost/changepw/', {
type: 'POST',
data: password
});
},
validatePassword: function (password) {
var validationErrors = [];
if (!validator.equals(password.newPassword, password.ne2Password)) {
validationErrors.push("Your new passwords do not match");
}
if (!validator.isLength(password.newPassword, 8)) {
validationErrors.push("Your password is not long enough. It must be at least 8 characters long.");
}
if (validationErrors.length > 0) {
this.set('passwordIsValid', false);
} else {
this.set('passwordIsValid', true);
}
this.set('passwordErrors', validationErrors);
return this;
},
fetchForgottenPasswordFor: function (email) {
var self = this;
return new Ember.RSVP.Promise(function (resolve, reject) {
if (!validator.isEmail(email)) {
reject(new Error('Please enter a correct email address.'));
} else {
resolve(ic.ajax.request(self.forgottenUrl, {
type: 'POST',
headers: {
// @TODO Find a more proper way to do this.
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
},
data: {
email: email
}
}));
}
});
},
resetPassword: function (passwords, token) {
var self = this;
return new Ember.RSVP.Promise(function (resolve, reject) {
if (!self.validatePassword(passwords).get('passwordIsValid')) {
reject(new Error('Errors found! ' + JSON.stringify(self.get('passwordErrors'))));
} else {
resolve(ic.ajax.request(self.resetUrl, {
type: 'POST',
headers: {
// @TODO: find a more proper way to do this.
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
},
data: {
newpassword: passwords.newPassword,
ne2password: passwords.ne2Password,
token: token
}
}));
}
});
}
});
export default UserModel;

View file

@ -1,78 +1,30 @@
/*global Ghost, Backbone, NProgress */
(function () {
"use strict";
/*global Ember */
Ghost.Router = Backbone.Router.extend({
// ensure we don't share routes between all Router instances
var Router = Ember.Router.extend();
routes: {
'' : 'blog',
'content/' : 'blog',
'settings(/:pane)/' : 'settings',
'editor(/:id)/' : 'editor',
'debug/' : 'debug',
'register/' : 'register',
'signup/' : 'signup',
'signin/' : 'login',
'forgotten/' : 'forgotten',
'reset/:token/' : 'reset'
},
Router.reopen({
location: 'history', // use HTML5 History API instead of hash-tag based URLs
rootURL: '/ghost/ember/' // admin interface lives under sub-directory /ghost
});
signup: function () {
Ghost.currentView = new Ghost.Views.Signup({ el: '.js-signup-box' });
},
login: function () {
Ghost.currentView = new Ghost.Views.Login({ el: '.js-login-box' });
},
forgotten: function () {
Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' });
},
reset: function (token) {
Ghost.currentView = new Ghost.Views.ResetPassword({ el: '.js-reset-box', token: token });
},
blog: function () {
var posts = new Ghost.Collections.Posts();
NProgress.start();
posts.fetch({ data: { status: 'all', staticPages: 'all', include: 'author'} }).then(function () {
Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts });
NProgress.done();
});
},
settings: function (pane) {
if (!pane) {
// Redirect to settings/general if no pane supplied
this.navigate('/settings/general/', {
trigger: true,
replace: true
});
return;
}
// only update the currentView if we don't already have a Settings view
if (!Ghost.currentView || !(Ghost.currentView instanceof Ghost.Views.Settings)) {
Ghost.currentView = new Ghost.Views.Settings({ el: '#main', pane: pane });
}
},
editor: function (id) {
var post = new Ghost.Models.Post();
post.urlRoot = Ghost.paths.apiRoot + '/posts';
if (id) {
post.id = id;
post.fetch({ data: {status: 'all', include: 'tags'}}).then(function () {
Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post });
});
} else {
Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post });
}
},
debug: function () {
Ghost.currentView = new Ghost.Views.Debug({ el: "#main" });
}
Router.map(function () {
this.route('signin');
this.route('signup');
this.route('forgotten');
this.route('reset', { path: '/reset/:token' });
this.resource('posts', { path: '/' }, function () {
this.route('post', { path: ':post_id' });
});
}());
this.resource('editor', { path: '/editor/:post_id' });
this.route('new', { path: '/editor' });
this.resource('settings', function () {
this.route('general');
this.route('user');
this.route('debug');
this.route('apps');
});
this.route('debug');
});
export default Router;

View file

@ -0,0 +1,36 @@
var ApplicationRoute = Ember.Route.extend({
actions: {
openModal: function (modalName, model) {
modalName = 'modals/' + modalName;
// We don't always require a modal to have a controller
// so we're skipping asserting if one exists
if (this.controllerFor(modalName, true)) {
this.controllerFor(modalName).set('model', model);
}
return this.render(modalName, {
into: 'application',
outlet: 'modal'
});
},
closeModal: function () {
return this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
},
handleErrors: function (errors) {
this.notifications.clear();
errors.forEach(function (errorObj) {
this.notifications.showError(errorObj.message || errorObj);
if (errorObj.hasOwnProperty('el')) {
errorObj.el.addClass('input-error');
}
});
}
}
});
export default ApplicationRoute;

View file

@ -0,0 +1,11 @@
var AuthenticatedRoute = Ember.Route.extend({
actions: {
error: function (error) {
if (error.jqXHR.status === 401) {
this.transitionTo('signin');
}
}
}
});
export default AuthenticatedRoute;

View file

@ -0,0 +1,7 @@
var DebugRoute = Ember.Route.extend({
beforeModel: function () {
this.transitionTo('settings.debug');
}
});
export default DebugRoute;

View file

@ -0,0 +1,15 @@
import ajax from 'ghost/utils/ajax';
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import Post from 'ghost/models/post';
var EditorRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['editor'],
controllerName: 'posts.post',
model: function (params) {
return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) {
return Post.create(post);
});
}
});
export default EditorRoute;

View file

@ -0,0 +1,7 @@
import styleBody from 'ghost/mixins/style-body';
var ForgottenRoute = Ember.Route.extend(styleBody, {
classNames: ['ghost-forgotten']
});
export default ForgottenRoute;

12
core/client/routes/new.js Normal file
View file

@ -0,0 +1,12 @@
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
var NewRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['editor'],
renderTemplate: function () {
this.render('editor');
}
});
export default NewRoute;

View file

@ -0,0 +1,24 @@
import ajax from 'ghost/utils/ajax';
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import Post from 'ghost/models/post';
var PostsRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['manage'],
model: function () {
return ajax('/ghost/api/v0.1/posts').then(function (response) {
return response.posts.map(function (post) {
return Post.create(post);
});
});
},
actions: {
openEditor: function (post) {
this.transitionTo('editor', post);
}
}
});
export default PostsRoute;

View file

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

View file

@ -0,0 +1,11 @@
/*global ajax */
import Post from 'ghost/models/post';
var PostsPostRoute = Ember.Route.extend({
model: function (params) {
return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) {
return Post.create(post);
});
}
});
export default PostsPostRoute;

View file

@ -0,0 +1,10 @@
import styleBody from 'ghost/mixins/style-body';
var ResetRoute = Ember.Route.extend(styleBody, {
classNames: ['ghost-reset'],
setupController: function (controller, params) {
controller.token = params.token;
}
});
export default ResetRoute;

View file

@ -0,0 +1,8 @@
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
var SettingsRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['settings']
});
export default SettingsRoute;

View file

@ -0,0 +1,11 @@
import SettingsModel from 'ghost/models/settings';
var settingsModel = SettingsModel.create();
var DebugRoute = Ember.Route.extend({
model: function () {
return settingsModel;
}
});
export default DebugRoute;

View file

@ -0,0 +1,13 @@
import ajax from 'ghost/utils/ajax';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import SettingsModel from 'ghost/models/settings';
var SettingsGeneralRoute = AuthenticatedRoute.extend({
model: function () {
return ajax('/ghost/api/v0.1/settings/?type=blog,theme,app').then(function (resp) {
return SettingsModel.create(resp);
});
}
});
export default SettingsGeneralRoute;

View file

@ -0,0 +1,10 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
var SettingsIndexRoute = AuthenticatedRoute.extend({
// redirect to general tab
redirect: function () {
this.transitionTo('settings.general');
}
});
export default SettingsIndexRoute;

View file

@ -0,0 +1,34 @@
import ajax from 'ghost/utils/ajax';
import styleBody from 'ghost/mixins/style-body';
var isEmpty = Ember.isEmpty;
var SigninRoute = Ember.Route.extend(styleBody, {
classNames: ['ghost-login'],
actions: {
login: function () {
var self = this,
controller = this.get('controller'),
data = controller.getProperties('email', 'password');
if (!isEmpty(data.email) && !isEmpty(data.password)) {
ajax('/ghost/api/v0.1/signin', data).then(
function (response) {
self.set('user', response);
self.transitionTo('posts');
}, function () {
window.alert('Error'); // Todo Show notification
}
);
} else {
this.notifications.clear();
this.notifications.showError('Must enter email + password');
}
}
}
});
export default SigninRoute;

View file

@ -0,0 +1,7 @@
import styleBody from 'ghost/mixins/style-body';
var SignupRoute = Ember.Route.extend(styleBody, {
classNames: ['ghost-signup']
});
export default SignupRoute;

View file

@ -0,0 +1,21 @@
<header class="floatingheader">
<button class="button-back" href="#">Back</button>
{{!-- @TODO: add back title updates depending on featured state --}}
<a {{bind-attr class="featured:featured:unfeatured"}} href="#" title="Feature this post">
<span class="hidden">Star</span>
</a>
<small>
{{!-- @TODO: the if published doesn't seem to work, needs to be fixed --}}
<span class="status">{{#if published}}Published{{else}}Written{{/if}}</span>
<span class="normal">by</span>
<span class="author">{{#if author.name}}{{author.name}}{{else}}{{author.email}}{{/if}}</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" title="Post Settings" {{action 'editSettings'}}><span class="hidden">Post Settings</span></a>
<!-- @TODO use Ghost Popover (#2565) --->
{{view "post-settings-menu-view"}}
</section>
</header>

View file

@ -0,0 +1,31 @@
<header id="global-header" class="navbar">
<a class="ghost-logo" href="/" data-off-canvas="left" title="/">
<span class="hidden">Ghost </span>
</a>
<nav id="global-nav" role="navigation">
<ul id="main-menu" >
{{activating-list-item route="posts" title="Content" classNames="content"}}
{{activating-list-item route="new" title="New post" classNames="content"}}
{{activating-list-item route="settings" title="Settings" classNames="content"}}
<li id="usermenu" class="usermenu subnav">
<a href="" {{action 'toggleMenu'}} class="dropdown">
{{!-- @TODO: show avatar of logged in user --}}
<img class="avatar" src="/shared/img/user-image.png" alt="Avatar" />
{{!-- @TODO: show logged in user name or email --}}
<span class="name">Fake Ghost</span>
</a>
{{!-- @TODO: add functionality to allow for dropdown to work --}}
{{#ghost-popover open=showMenu}}
<ul class="overlay">
<li class="usermenu-profile"><a href="#">Your Profile</a></li>
<li class="divider"></li>
<li class="usermenu-help"><a href="http://ghost.org/forum/">Help / Support</a></li>
<li class="divider"></li>
<li class="usermenu-signout"><a href="#">Sign Out</a></li>
</ul>
{{/ghost-popover}}
</li>
</ul>
</nav>
</header>

View file

@ -0,0 +1,29 @@
<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"></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" title="Post Settings" {{action 'editSettings'}}><span class="hidden">Post Settings</span></a>
<!-- @TODO Use Ghost Popover (#2565) and style arrow down -->
{{view "post-settings-menu-view"}}
</section>
<section id="entry-actions" class="js-publish-splitbutton splitbutton-save">
<button type="button" class="js-publish-button button-save">Save Draft</button>
<a class="options up" data-toggle="ul" href="#" title="Post Settings"><span class="hidden">Post Settings</span></a>
{{!-- @TODO: implement popover --}}
<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>

View file

@ -0,0 +1,10 @@
{{#unless isLoggedOut}}
{{partial "navbar"}}
{{/unless}}
<main role="main" id="main">
{{ghost-notifications}}
{{outlet}}
</main>
{{outlet modal}}

View file

@ -0,0 +1,3 @@
<div class="rendered-markdown">
{{format-markdown markdown}}
</div>

View file

@ -0,0 +1 @@
{{#link-to route alternateActive=active}}{{title}}{{yield}}{{/link-to}}

View file

@ -0,0 +1,2 @@
<input type="file" class="button-add" />
<button type="submit" class="button-save" {{bind-attr disabled=uploadButtonDisabled}} {{action "upload"}}>{{uploadButtonText}}</button>

View file

@ -0,0 +1,4 @@
<section {{bind-attr class=":js-notification message.typeClass"}}>
{{message.message}}
<a class="close" {{action "closeNotification"}}><span class="hidden">Close</span></a>
</section>

View file

@ -0,0 +1,3 @@
{{#each messages}}
{{ghost-notification message=this}}
{{/each}}

View file

@ -0,0 +1,22 @@
<div id="modal-container" {{action bubbles=false preventDefault=false}}>
<article {{bind-attr class="klass :js-modal"}}>
<section class="modal-content">
{{#if title}}<header class="modal-header"><h1>{{title}}</h1></header>{{/if}}
{{#if showClose}}<a class="close" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>{{/if}}
<section class="modal-body">
{{yield}}
</section>
{{#if confirm}}
<footer class="modal-footer">
<button {{bind-attr class="acceptButtonClass :js-button-accept"}} {{action "confirm" "accept"}}>
{{confirm.accept.text}}
</button>
<button {{bind-attr class="rejectButtonClass :js-button-reject"}} {{action "confirm" "reject"}}>
{{confirm.reject.text}}
</button>
</footer>
{{/if}}
</section>
</article>
</div>
<div class="modal-background fade" {{action "closeModal"}}></div>

View file

@ -0,0 +1,28 @@
<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="" {{action "openModal" "markdown"}}><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
{{-codemirror 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>
<section class="entry-preview-content">
{{-markdown markdown=markdown scrollPosition=view.scrollPosition}}
</section>
</section>
</section>
{{partial 'publish-bar'}}

View file

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

View file

@ -0,0 +1,8 @@
<section class="forgotten-box js-forgotten-box fade-in">
<form id="forgotten" class="forgotten-form" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
<div class="email-wrap">
{{input value=email class="email" type="email" placeholder="Email Address" name="email" autofocus="autofocus" autocapitalize="off" autocorrect="off"}}
</div>
<button class="button-save" type="submit">Send new password</button>
</form>
</section>

View file

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

View file

@ -0,0 +1,6 @@
{{#modal-dialog action="closeModal" type="action" style="wide,centered" animation="fade"
title="Would you really like to delete all content from your blog?" confirm=confirm}}
<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>
{{/modal-dialog}}

View file

@ -0,0 +1,6 @@
{{#modal-dialog action="closeModal" showClose=true type="action" style="wide,centered" animation="fade"
title="Are you sure you want to delete this post?" confirm=confirm}}
<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>
{{/modal-dialog}}

View file

@ -1,13 +1,15 @@
<section class="markdown-help-container">
<table class="modal-markdown-help-table">
<thead>
{{#modal-dialog action="closeModal" showClose=true style="wide" animation="fade"
title="Markdown Help"}}
<section class="markdown-help-container">
<table class="modal-markdown-help-table">
<thead>
<tr>
<th>Result</th>
<th>Markdown</th>
<th>Shortcut</th>
</tr>
</thead>
<tbody>
</thead>
<tbody>
<tr>
<td><strong>Bold</strong></td>
<td>**text**</td>
@ -63,7 +65,8 @@
<td>`code`</td>
<td>Cmd + K / Ctrl + Shift + K</td>
</tr>
</tbody>
</table>
For further Markdown syntax reference: <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Documentation</a>
</section>
</tbody>
</table>
For further Markdown syntax reference: <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Documentation</a>
</section>
{{/modal-dialog}}

View file

@ -0,0 +1,9 @@
{{#upload-modal action="closeModal" close=true type="action" style="wide"
animation="fade"}}
<section class="js-drop-zone">
<img class="js-upload-target" {{bind-attr src=src}} alt="logo">
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" {{#if options.acceptEncoding}}accept="{{options.acceptEncoding}}"{{/if}}>
</section>
{{/upload-modal}}

View file

@ -0,0 +1 @@
TODO

View file

@ -0,0 +1,32 @@
<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">
{{blur-text-field class="post-setting-slug" id="url" value=newSlug action="updateSlug" placeholder=slugPlaceholder selectOnClick="true"}}
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
{{blur-text-field class="post-setting-date" value=view.publishedAt action="updatePublishedAt" placeholder=view.datePlaceholder}}
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label class="label" for="static-page">Static Page</label>
</td>
<td class="post-setting-item">
{{input type="checkbox" name="static-page" id="static-page" class="post-setting-static-page" checked=isStaticPage}}
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</tbody>
</table>
</form>
<a class="delete" {{action 'openModal' 'delete-post' post}}>Delete This Post</a>

View file

@ -0,0 +1,38 @@
<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"}}<span class="hidden">New Post</span>{{/link-to}}
</header>
<section class="content-list-content">
<ol class="posts-list">
{{#each itemController="posts/post" itemView="post-item-view" itemTagName="li"}}
{{!-- @TODO: Restore functionality where 'featured' and 'page' classes are added for proper posts --}}
{{#link-to 'posts.post' this class="permalink" title="Edit this post"}}
<h3 class="entry-title">{{title}}</h3>
<section class="entry-meta">
<span class="status">
{{#if isPublished}}
{{#if page}}
<span class="page">Page</span>
{{else}}
<time datetime="{{unbound published_at}}" class="date published">
Published {{format-timeago published_at}}
</time>
{{/if}}
{{else}}
<span class="draft">Draft</span>
{{/if}}
</span>
</section>
{{/link-to}}
{{/each}}
</ol>
</section>
</section>
<section class="content-preview js-content-preview">
{{outlet}}
</section>
</section>

View file

@ -0,0 +1,21 @@
{{#if title}}
{{partial "floating-header"}}
<section class="content-preview-content">
<div class="wrapper">
<h1>{{title}}</h1>
{{format-markdown markdown}}
</div>
</section>
{{else}}
<div class="no-posts-box">
<div class="no-posts">
<h3>You Haven't Written Any Posts Yet!</h3>
{{#link-to 'new'}}<button class="button-add large" title="New Post">Write a new Post</button>{{/link-to}}
</div>
</div>
{{/if}}

View file

@ -0,0 +1,11 @@
<section class="reset-box js-reset-box fade-in">
<form id="reset" class="reset-form" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
<div class="password-wrap">
{{input value=passwords.newPassword class="password" type="password" placeholder="Password" name="newpassword" autofocus="autofocus" }}
</div>
<div class="password-wrap">
{{input value=passwords.ne2Password class="password" type="password" placeholder="Confirm Password" name="ne2password" }}
</div>
<button class="button-save" type="submit" {{bind-attr disabled='submitButtonDisabled'}}>Reset Password</button>
</form>
</section>

View file

@ -0,0 +1,18 @@
<div class="wrapper">
<aside class="settings-sidebar" role="complementary">
<header>
<h1 class="title">Settings</h1>
</header>
<nav class="settings-menu">
<ul>
<li class="general">{{#link-to 'settings.general'}}General{{/link-to}}</li>
<li class="users">{{#link-to 'settings.user'}}User{{/link-to}}</li>
<li class="apps">{{#link-to 'settings.apps'}}Apps{{/link-to}}</li>
</ul>
</nav>
</aside>
<section class="settings-content active">
{{outlet}}
</section>
</div>

View file

@ -0,0 +1,41 @@
<header>
<h2 class="title">General</h2>
</header>
<section class="content">
<form id="settings-export">
<fieldset>
<div class="form-group">
<label>Export</label>
<a class="button-save" {{bind-attr href=model.exportPath}}>Export</a>
<p>Export the blog settings and data.</p>
</div>
</fieldset>
</form>
{{#gh-form id="settings-import" enctype="multipart/form-data"}}
<fieldset>
<div class="form-group">
<label>Import</label>
{{file-upload onUpload="importData" uploadButtonText=uploadButtonText}}
<p>Import from another Ghost installation. If you import a user, this will replace the current user & log you out.</p>
</div>
</fieldset>
{{/gh-form}}
<form id="settings-resetdb">
<fieldset>
<div class="form-group">
<label>Delete all Content</label>
<a href="javascript:void(0);" class="button-delete js-delete" {{action "openModal" "deleteAll"}}>Delete</a>
<p>Delete all posts and tags from the database.</p>
</div>
</fieldset>
</form>
<form id="settings-testmail">
<fieldset>
<div class="form-group">
<label>Send a test email</label>
<button type="submit" id="sendtestmail" class="button-save" {{action "sendTestEmail"}}>Send</button>
<p>Sends a test email to your address.</p>
</div>
</fieldset>
</form>
</section>

View file

@ -0,0 +1,80 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">General</h2>
<section class="page-actions">
<button class="button-save" {{action 'save'}}>Save</button>
</section>
</header>
<section class="content">
<form id="settings-general" novalidate="novalidate">
<fieldset>
<div class="form-group">
<label for="blog-title">Blog Title</label>
{{input id="blog-title" name="general[title]" type="text" value=title}}
<p>The name of your blog</p>
</div>
<div class="form-group description-container">
<label for="blog-description">Blog Description</label>
{{textarea id="blog-description" value=description}}
<p>
Describe what your blog is about
<span class="word-count">{{count-words description}}</span>
</p>
</div>
</fieldset>
<div class="form-group">
<label for="blog-logo">Blog Logo</label>
{{#if logo}}
<a class="js-modal-logo" href="#" {{action 'openModal' 'upload'}}><img id="blog-logo" {{bind-attr src=logo}} alt="logo"></a>
{{else}}
<a class="button-add js-modal-logo" {{action 'openModal' 'upload'}}>Upload Image</a>
{{/if}}
<p>Display a sexy logo for your publication</p>
</div>
<div class="form-group">
<label for="blog-cover">Blog Cover</label>
{{#if cover}}
<a class="js-modal-cover" href="#" {{action 'openModal' 'upload'}}><img id="blog-cover" {{bind-attr src=logo}} alt="cover photo"></a>
{{else}}
<a class="button-add js-modal-cover" {{action 'openModal' 'upload'}}>Upload Image</a>
{{/if}}
<p>Display a cover image on your site</p>
</div>
<fieldset>
<div class="form-group">
<label for="email-address">Email Address</label>
{{input id="email-address" name="general[email-address]" type="email" value=email autocapitalize="off" autocorrect="off"}}
<p>Address to use for admin notifications</p>
</div>
<div class="form-group">
<label for="postsPerPage">Posts per page</label>
{{input id="postsPerPage" name="general[postsPerPage]" type="number" value=postsPerPage}}
<p>How many posts should be displayed on each page</p>
</div>
<div class="form-group">
<label for="permalinks">Dated Permalinks</label>
{{input id="permalinks" name="general[permalinks]" type="checkbox" checked=isDatedPermalinks}}
<label class="checkbox" for="permalinks"></label>
<p>Include the date in your post URLs</p>
</div>
<div class="form-group">
<label for="activeTheme">Theme</label>
<select id="activeTheme" name="general[activeTheme]">
{{#each availableThemes}}
<option value="{{name}}" {{#if active}}selected{{/if}}>{{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}}{{/if}}</option>
{{/each}}
</select>
<p>Select a theme for your blog</p>
</div>
</fieldset>
</form>
</section>

View file

@ -0,0 +1,90 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">Your Profile</h2>
<section class="page-actions">
<button class="button-save" {{action 'save'}}>Save</button>
</section>
</header>
<section class="content no-padding">
<header class="user-profile-header">
<img id="user-cover" class="cover-image" {{bind-attr src=cover title=coverTitle}} />
<a class="edit-cover-image js-modal-cover button" {{action 'openModal' 'upload'}}>Change Cover</a>
</header>
<form class="user-profile" novalidate="novalidate">
<fieldset class="user-details-top">
<figure class="user-image">
<div id="user-image" class="img" {{bind-attr style=image}} href="#"><span class="hidden">{{name}}'s Picture</span></div>
<a href="" {{action 'openModal' 'upload'}} class="edit-user-image js-modal-image">Edit Picture</a>
</figure>
<div class="form-group">
<label for="user-name" class="hidden">Full Name</label>
{{input value=user.name id="user-name" placeholder="Full Name" autocorrect="off"}}
<p>Use your real name so people can recognise you</p>
</div>
</fieldset>
<fieldset class="user-details-bottom">
<div class="form-group">
<label for"user-email">Email</label>
{{input type="email" value=user.email id="user-email" placeholder="Email Address" autocapitalize="off" autocorrect="off"}}
<p>Used for notifications</p>
</div>
<div class="form-group">
<label for="user-location">Location</label>
{{input type="text" value=user.location id="user-location"}}
<p>Where in the world do you live?</p>
</div>
<div class="form-group">
<label for="user-website">Website</label>
{{input type="url" value=user.website id="user-website" autocapitalize="off" autocorrect="off"}}
<p>Have a website or blog other than this one? Link it!</p>
</div>
<div class="form-group bio-container">
<label for="user-bio">Bio</label>
{{textarea id="user-bio" value=user.bio}}
<p>
Write about you, in 200 characters or less.
<span class="word-count">{{count-words user.bio}}</span>
</p>
</div>
<hr />
</fieldset>
<fieldset>
<div class="form-group">
<label for="user-password-old">Old Password</label>
{{input value=password type="password" id="user-password-old"}}
</div>
<div class="form-group">
<label for="user-password-new">New Password</label>
{{input value=newPassword type="password" id="user-password-new"}}
</div>
<div class="form-group">
<label for="user-new-password-verification">Verify Password</label>
{{input value=ne2Password type="password" id="user-new-password-verification"}}
</div>
<div class="form-group">
<button type="button" class="button-delete button-change-password" {{action 'password'}}>Change Password</button>
</div>
</fieldset>
</form>
</section>

View file

@ -0,0 +1,14 @@
<section class="login-box js-login-box fade-in">
<form id="login" class="login-form" method="post" novalidate="novalidate">
<div class="email-wrap">
{{input class="email" type="email" placeholder="Email Address" name="email" autofocus="autofocus" autocapitalize="off" autocorrect="off" value=email}}
</div>
<div class="password-wrap">
{{input class="password" type="password" placeholder="Password" name="password" value=password}}
</div>
<button class="button-save" type="submit" {{action "login"}}>Log in</button>
<section class="meta">
{{#link-to 'forgotten' class="forgotten-password"}}Forgotten password?{{/link-to}}
</section>
</form>
</section>

View file

@ -0,0 +1,14 @@
<section class="signup-box js-signup-box fade-in">
<form id="signup" class="signup-form" method="post" novalidate="novalidate">
<div class="name-wrap">
<input class="name" type="text" placeholder="Full Name" name="name" autofocus autocorrect="off" />
</div>
<div class="email-wrap">
<input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off" />
</div>
<div class="password-wrap">
<input class="password" type="password" placeholder="Password" name="password" />
</div>
<button class="button-save" type="submit">Sign Up</button>
</form>
</section>

View file

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

View file

@ -0,0 +1,21 @@
/* global moment */
var parseDateFormats = ["DD MMM YY HH:mm",
"DD MMM YYYY HH:mm",
"DD/MM/YY HH:mm",
"DD/MM/YYYY HH:mm",
"DD-MM-YY HH:mm",
"DD-MM-YYYY HH:mm",
"YYYY-MM-DD HH:mm"],
displayDateFormat = 'DD MMM YY @ HH:mm';
//Parses a string to a Moment
var parseDateString = function (value) {
return value ? moment(value, parseDateFormats) : '';
};
//Formats a Date or Moment
var formatDate = function (value) {
return value ? moment(value).format(displayDateFormat) : '';
};
export {parseDateString, formatDate};

View file

@ -0,0 +1,9 @@
Ember.LinkView.reopen({
active: Ember.computed('resolvedParams', 'routeArgs', function () {
var isActive = this._super();
Ember.set(this, 'alternateActive', isActive);
return isActive;
})
});

View file

@ -0,0 +1,43 @@
var Notifications = Ember.ArrayProxy.extend({
content: Ember.A(),
timeout: 3000,
pushObject: function (object) {
object.typeClass = 'notification-' + object.type;
// This should be somewhere else.
if (object.type === 'success') {
object.typeClass = object.typeClass + " notification-passive";
}
this._super(object);
},
showError: function (message) {
this.pushObject({
type: 'error',
message: message
});
},
showErrors: function (errors) {
for (var i = 0; i < errors.length; i += 1) {
this.showError(errors[i].message || errors[i]);
}
},
showInfo: function (message) {
this.pushObject({
type: 'info',
message: message
});
},
showSuccess: function (message) {
this.pushObject({
type: 'success',
message: message
});
},
showWarn: function (message) {
this.pushObject({
type: 'warn',
message: message
});
}
});
export default Notifications;

View file

@ -0,0 +1,3 @@
Ember.TextField.reopen({
attributeBindings: ['autofocus']
});

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

@ -1,158 +1,3 @@
// # Article Editor
/*global document, setTimeout, navigator, $, Backbone, Ghost, shortcut */
(function () {
'use strict';
var PublishBar;
// 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 Ghost.View.EditorTagWidget(
{el: this.$('#entry-tags'), model: this.model}
)).render();
this.addSubview(new Ghost.View.PostSettings(
{el: $('#entry-controls'), model: this.model}
)).render();
// Pass the Actions widget references to the title and editor so that it can get
// the values that need to be saved
this.addSubview(new Ghost.View.EditorActionsWidget(
{
el: this.$('#entry-actions'),
model: this.model,
$title: this.options.$title,
editor: this.options.editor
}
)).render();
},
render: function () { return this; }
});
// The entire /editor page's route
// ----------------------------------------
Ghost.Views.Editor = Ghost.View.extend({
events: {
'click .markdown-help': 'showHelp',
'blur #entry-title': 'trimTitle',
'orientationchange': 'orientationChange'
},
initialize: function () {
this.$title = this.$('#entry-title');
this.$editor = this.$('#entry-markdown');
this.$title.val(this.model.get('title')).focus();
this.$editor.text(this.model.get('markdown'));
// Create a new editor
this.editor = new Ghost.Editor.Main();
// Add the container view for the Publish Bar
// Passing reference to the title and editor
this.addSubview(new PublishBar(
{el: '#publish-bar', model: this.model, $title: this.$title, editor: this.editor}
)).render();
this.listenTo(this.model, 'change:title', this.renderTitle);
this.listenTo(this.model, 'change:id', this.handleIdChange);
this.bindShortcuts();
$('.entry-markdown header, .entry-preview header').on('click', function (e) {
$('.entry-markdown, .entry-preview').removeClass('active');
$(e.currentTarget).closest('section').addClass('active');
});
},
bindShortcuts: function () {
var self = this;
// Zen writing mode shortcut - full editor view
shortcut.add('Alt+Shift+Z', function () {
$('body').toggleClass('zen');
});
// HTML copy & paste
shortcut.add('Ctrl+Alt+C', function () {
self.showHTML();
});
},
trimTitle: function () {
var rawTitle = this.$title.val(),
trimmedTitle = $.trim(rawTitle);
if (rawTitle !== trimmedTitle) {
this.$title.val(trimmedTitle);
}
// Trigger title change for post-settings.js
this.model.set('title', trimmedTitle);
},
renderTitle: function () {
this.$title.val(this.model.get('title'));
},
handleIdChange: function (m) {
// This is a special case for browsers which fire an unload event when using navigate. The id change
// happens before the save success and can cause the unload alert to appear incorrectly on first save
// The id only changes in the event that the save has been successful, so this workaround is safes
this.editor.setDirty(false);
Backbone.history.navigate('/editor/' + m.id + '/');
},
// This is a hack to remove iOS6 white space on orientation change bug
// See: http://cl.ly/RGx9
orientationChange: function () {
if (/iPhone/.test(navigator.userAgent) && !/Opera Mini/.test(navigator.userAgent)) {
var focusedElement = document.activeElement,
s = document.documentElement.style;
focusedElement.blur();
s.display = 'none';
setTimeout(function () { s.display = 'block'; focusedElement.focus(); }, 0);
}
},
showEditorModal: function (content) {
this.addSubview(new Ghost.Views.Modal({
model: {
options: {
close: true,
style: ['wide'],
animation: 'fade'
},
content: content
}
}));
},
showHelp: function () {
var content = {
template: 'markdown',
title: 'Markdown Help'
};
this.showEditorModal(content);
},
showHTML: function () {
var content = {
template: 'copyToHTML',
title: 'Copied HTML'
};
this.showEditorModal(content);
},
render: function () { return this; }
});
}());
export default Ember.View.extend({
scrollPosition: 0 // percentage of scroll position
});

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')
});

View file

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

View file

@ -0,0 +1,18 @@
/* global moment */
import {formatDate} from 'ghost/utils/date-formatting';
var PostSettingsMenuView = Ember.View.extend({
templateName: 'post-settings-menu',
classNames: ['post-settings-menu', 'menu-drop-right', 'overlay'],
classNameBindings: ['controller.isEditingSettings::hidden'],
publishedAtBinding: Ember.Binding.oneWay('controller.publishedAt'),
click: function (event) {
//Stop click propagation to prevent window closing
event.stopPropagation();
},
datePlaceholder: function () {
return formatDate(moment());
}.property('controller.publishedAt')
});
export default PostSettingsMenuView;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Some files were not shown because too many files have changed in this diff Show more