Replace the old admin with the ember admin

closes #3056

- Remove clientold
- Remove clientold tests
- Cleanup old admin helpers
- Remove old routes from admin and controllers from admin controller
- Comment out / remove old and broken tests
- Cleanup Gruntfile.js, bower.js, package.json etc

Still TODO:

- cleanup / add removed tests
- do we still need countable?
This commit is contained in:
Hannah Wolfe 2014-07-01 00:26:08 +01:00
parent c35884ea6e
commit f70f99b5cf
104 changed files with 273 additions and 8789 deletions

4
.gitignore vendored
View File

@ -42,10 +42,6 @@ Session.vim
.tmp
/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

View File

@ -42,8 +42,6 @@ var path = require('path'),
var cfg = {
// #### Common paths used by tasks
paths: {
// adminAssets: './core/client/', ?? who knows...
adminOldAssets: './core/clientold/assets',
build: buildDirectory,
releaseBuild: path.join(buildDirectory, 'release'),
dist: distDirectory,
@ -58,15 +56,11 @@ var path = require('path'),
// Watch files and livereload in the browser during development.
// See the [grunt dev](#live%20reload) task for how this is used.
watch: {
handlebars: {
files: ['core/clientold/tpl/**/*.hbs'],
tasks: ['handlebars']
},
shared: {
files: ['core/shared/**/*.js'],
tasks: ['concat:dev', 'concat:dev-ember']
tasks: ['concat:dev']
},
'handlebars-ember': {
'emberTemplates': {
files: ['core/client/**/*.hbs'],
tasks: ['emberTemplates:dev']
},
@ -74,13 +68,6 @@ var path = require('path'),
files: ['core/client/**/*.js'],
tasks: ['clean:tmp', 'transpile', 'concat_sourcemap']
},
concat: {
files: [
'core/clientold/*.js',
'core/clientold/**/*.js'
],
tasks: ['concat']
},
'ghost-ui': {
files: [
'bower_components/ghost-ui/dist/css/*.css'
@ -225,7 +212,6 @@ var path = require('path'),
}
},
// ### grunt-shell
// Command line tools where it's easier to run a command directly than configure a grunt plugin
shell: {
@ -253,23 +239,6 @@ var path = require('path'),
}
},
// ### grunt-contrib-handlebars
// Compile handlebars templates into a JST file for the admin client (old)
handlebars: {
core: {
options: {
namespace: 'JST',
processName: function (filename) {
filename = filename.replace('core/clientold/tpl/', '');
return filename.replace('.hbs', '');
}
},
files: {
'core/clientold/tpl/hbs-tpl.js': 'core/clientold/tpl/**/*.hbs'
}
}
},
// ### grunt-ember-templates
// Compiles handlebar templates for ember
emberTemplates: {
@ -278,11 +247,13 @@ var path = require('path'),
templateBasePath: /core\/client\//,
templateFileExtensions: /\.hbs/,
templateRegistration: function (name, template) {
return grunt.config.process("define('ghost/") + name + "', ['exports'], function(__exports__){ __exports__['default'] = " + 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"
'core/built/scripts/templates-ember.js': 'core/client/templates/**/*.hbs'
}
}
},
@ -362,11 +333,6 @@ 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: {
@ -380,11 +346,6 @@ 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: {
@ -398,11 +359,6 @@ 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,
@ -427,69 +383,7 @@ var path = require('path'),
// ### grunt-contrib-concat
// concatenate multiple JS files into a single file ready for use
concat: {
dev: {
files: {
'core/built/scripts/vendor.js': [
'bower_components/jquery/dist/jquery.js',
'bower_components/jquery-ui/ui/jquery-ui.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',
'bower_components/handlebars/handlebars.runtime.js',
'bower_components/moment/moment.js',
'bower_components/jquery-file-upload/js/jquery.fileupload.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/validator-js/validator.js',
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
'core/shared/lib/showdown/extensions/ghostgfm.js',
// TODO: Remove or replace
'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/built/scripts/helpers.js': [
'core/clientold/init.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/clientold/tpl/hbs-tpl.js'
],
'core/built/scripts/models.js': [
'core/clientold/models/**/*.js'
],
'core/built/scripts/views.js': [
'core/clientold/views/**/*.js',
'core/clientold/router.js'
]
}
},
'dev-ember': {
'dev': {
files: {
'core/built/scripts/vendor-ember.js': [
'bower_components/loader.js/loader.js',
@ -520,62 +414,6 @@ var path = require('path'),
'core/shared/lib/showdown/extensions/ghostgfm.js',
]
}
},
prod: {
files: {
'core/built/scripts/ghost.js': [
'bower_components/jquery/dist/jquery.js',
'bower_components/jquery-ui/ui/jquery-ui.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',
'bower_components/handlebars/handlebars.runtime.js',
'bower_components/moment/moment.js',
'bower_components/jquery-file-upload/js/jquery.fileupload.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/validator-js/validator.js',
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
'core/shared/lib/showdown/extensions/ghostgfm.js',
// TODO: Remove or replace
'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/clientold/init.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/clientold/tpl/hbs-tpl.js',
'core/clientold/models/**/*.js',
'core/clientold/views/**/*.js',
'core/clientold/router.js'
]
}
}
},
@ -584,7 +422,6 @@ var path = require('path'),
uglify: {
prod: {
files: {
'core/built/scripts/ghost.min.js': 'core/built/scripts/ghost.js',
'core/built/public/jquery.min.js': 'core/built/public/jquery.js'
}
}
@ -592,10 +429,10 @@ var path = require('path'),
// ### grunt-update-submodules
// Grunt task to update git submodules
"update_submodules": {
'update_submodules': {
default: {
options: {
params: "--init"
params: '--init'
}
}
}
@ -611,7 +448,7 @@ var path = require('path'),
// This really ought to be refactored into a separate grunt task module
grunt.registerTask('spawnCasperJS', function (target) {
target = _.contains(['client', 'clientold', 'frontend'], target) ? target + '/' : undefined;
target = _.contains(['client', 'frontend'], target) ? target + '/' : undefined;
var done = this.async(),
options = ['host', 'noPort', 'port', 'email', 'password'],
@ -808,7 +645,7 @@ var path = require('path'),
// The purpose of the functional tests is to ensure that Ghost is working as is expected from a user perspective
// including buttons and other important interactions in the admin UI.
grunt.registerTask('test-functional', 'Run functional interface tests (CasperJS)',
['clean:test', 'emberBuild', 'setTestEnv', 'loadConfig', 'copy:dev', 'express:test', 'spawnCasperJS', 'express:test:stop']
['clean:test', 'setTestEnv', 'loadConfig', 'express:test', 'spawnCasperJS', 'express:test:stop']
);
// ### Coverage
@ -881,15 +718,15 @@ var path = require('path'),
//
// It is otherwise the same as running `grunt`, but is only used when running Ghost in the `production` env.
grunt.registerTask('prod', 'Build JS & templates for production',
['handlebars', 'concat', 'uglify', 'copy:prod', 'master-warn']);
['concat', 'uglify', 'copy:prod', 'master-warn']);
// ### Default asset build
// `grunt` - default grunt task
//
// Compiles handlebars templates, concatenates javascript files for the admin UI into a handful of files instead
// Compiles 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',
['handlebars', 'concat', 'copy:dev', 'emberBuild']);
['concat', 'copy:dev', 'emberBuild']);
// ### Live reload
// `grunt dev` - build assets on the fly whilst developing
@ -903,7 +740,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', 'emberBuild', 'express:dev', 'watch']);
['concat', 'copy:dev', 'emberBuild', 'express:dev', 'watch']);
// ### Release
// Run `grunt release` to create a Ghost release zip file.
@ -912,11 +749,11 @@ var path = require('path'),
// either environment, and packages all the files up into a zip.
grunt.registerTask('release',
'Release task - creates a final built zip\n' +
' - Do our standard build steps (handlebars, etc)\n' +
' - Do our standard build steps \n' +
' - Copy files to release-folder/#/#{version} directory\n' +
' - Clean out unnecessary files (travis, .git*, etc)\n' +
' - Zip files in release-folder to dist-folder/#{version} directory',
['shell:bower', 'update_submodules', 'handlebars', 'concat', 'uglify', 'clean:release', 'copy:release', 'compress:release']);
['shell:bower', 'update_submodules', 'concat', 'uglify', 'clean:release', 'copy:release', 'compress:release']);
};
// Export the configuration

View File

@ -1,13 +1,13 @@
{
"name": "ghost",
"dependencies": {
"backbone": "1.0.0",
"codemirror": "4.0.1",
"Countable": "2.0.2",
"ember": "1.5.0",
"ember-data": "~1.0.0-beta.8",
"ember-load-initializers": "git://github.com/stefanpenner/ember-load-initializers.git#0.0.1",
"ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#181251821cf513bb58d3e192faa13245a816f75e",
"ember-simple-auth": "https://github.com/simplabs/ember-simple-auth-component.git#0.5.3",
"fastclick": "1.0.0",
"ghost-ui": "0.8.1",
"handlebars": "1.3.0",
@ -17,13 +17,12 @@
"jquery-hammerjs": "1.0.1",
"jquery-ui": "1.10.4",
"keymaster": "madrobby/keymaster#0f09fc1b7e66c2b7e07afe89a419366dcf2d1cd8",
"loader.js": "stefanpenner/loader.js#1.0.0",
"lodash": "2.4.1",
"moment": "2.4.0",
"nprogress": "0.1.2",
"showdown": "https://github.com/ErisDS/showdown.git#v0.3.2-ghost",
"validator-js": "3.4.0",
"loader.js": "stefanpenner/loader.js#1.0.0",
"ember-simple-auth": "https://github.com/simplabs/ember-simple-auth-component.git#0.5.3"
"validator-js": "3.4.0"
},
"resolutions": {
"ember": "~1.4.0"

View File

@ -1,13 +0,0 @@
## What's this?
This is the shiny new Ghost admin UI built in Ember.js. It gets served if you visit the URL `/ghost/ember/`.
We're currently in the process of building this awesome new UI to replace the old one which was written in backbone,
lives in the `/clientold/` folder and is still served when you visit the URL `/ghost/`.
In short, we currently have 2 admins:
* Old, Backbone Admin UI lives in `/clientold/` and is served from `/ghost/`
* New, Ember Admin UI lives in `/client/` and is served from `/ghost/ember/`
For more information, please read the [Ember admin wiki page](https://github.com/TryGhost/Ghost/wiki/Ember-Admin-UI)

View File

@ -6,7 +6,7 @@ var Router = Ember.Router.extend();
Router.reopen({
location: 'trailing-history', // use HTML5 History API instead of hash-tag based URLs
rootURL: ghostPaths().subdir + '/ghost/ember/', // admin interface lives under sub-directory /ghost
rootURL: ghostPaths().subdir + '/ghost/', // admin interface lives under sub-directory /ghost
clearNotifications: function () {
// @TODO This should call closePassive() to only close passive notifications

View File

@ -1,13 +0,0 @@
## What's this?
This is the old Ghost admin UI built in backbone.js. It gets served if you visit the URL `/ghost/`.
We're currently in the process of replacing this UI with a new one written in Ember which lives in the `/client/`
folder, and is served when you visit the URL `/ghost/ember/`.
In short, we currently have 2 admins:
* Old, Backbone Admin UI lives in `/clientold/` and is served from `/ghost/`
* New, Ember Admin UI lives in `/client/` and is served from `/ghost/ember/`
For more information, please read the [Ember admin wiki page](https://github.com/TryGhost/Ghost/wiki/Ember-Admin-UI)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 B

View File

@ -1,44 +0,0 @@
// # Ghost Editor HTML Preview
//
// HTML Preview is the right pane in the split view editor.
// It is effectively just a scrolling container for the HTML output from showdown
// It knows how to update itself, and that's pretty much it.
/*global Ghost, Showdown, Countable, _, $ */
(function () {
'use strict';
var HTMLPreview = function (markdown, uploadMgr) {
var converter = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm']}),
preview = document.getElementsByClassName('rendered-markdown')[0],
update;
// Update the preview
// Includes replacing all the HTML, intialising upload dropzones, and updating the counter
update = function () {
preview.innerHTML = converter.makeHtml(markdown.value());
uploadMgr.enable();
Countable.once(preview, function (counter) {
$('.entry-word-count').text($.pluralize(counter.words, 'word'));
$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
});
};
// Public API
_.extend(this, {
scrollViewPort: function () {
return $('.entry-preview-content');
},
scrollContent: function () {
return $('.rendered-markdown');
},
update: update
});
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.HTMLPreview = HTMLPreview;
} ());

View File

@ -1,79 +0,0 @@
// # Ghost Editor
//
// Ghost Editor contains a set of modules which make up the editor component
// It manages the left and right panes, and all of the communication between them
// Including scrolling,
/*global document, $, _, Ghost */
(function () {
'use strict';
var Editor = function () {
var self = this,
$document = $(document),
// Create all the needed editor components, passing them what they need to function
markdown = new Ghost.Editor.MarkdownEditor(),
uploadMgr = new Ghost.Editor.UploadManager(markdown),
preview = new Ghost.Editor.HTMLPreview(markdown, uploadMgr),
scrollHandler = new Ghost.Editor.ScrollHandler(markdown, preview),
unloadDirtyMessage,
handleChange,
handleDrag;
unloadDirtyMessage = function () {
return '==============================\n\n' +
'Hey there! It looks like you\'re in the middle of writing' +
' something and you haven\'t saved all of your content.' +
'\n\nSave before you go!\n\n' +
'==============================';
};
handleChange = function () {
self.setDirty(true);
preview.update();
};
handleDrag = function (e) {
e.preventDefault();
};
// Public API
_.extend(this, {
enable: function () {
// Listen for changes
$document.on('markdownEditorChange', handleChange);
// enable editing and scrolling
markdown.enable();
scrollHandler.enable();
},
disable: function () {
// Don't listen for changes
$document.off('markdownEditorChange', handleChange);
// disable editing and scrolling
markdown.disable();
scrollHandler.disable();
},
// Get the markdown value from the editor for saving
// Upload manager makes sure the upload markers are removed beforehand
value: function () {
return uploadMgr.value();
},
setDirty: function (dirty) {
window.onbeforeunload = dirty ? unloadDirtyMessage : null;
}
});
// Initialise
$document.on('drop dragover', handleDrag);
preview.update();
this.enable();
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.Main = Editor;
}());

View File

@ -1,100 +0,0 @@
// # Ghost Editor Markdown Editor
//
// Markdown Editor is a light wrapper around CodeMirror
/*global Ghost, CodeMirror, shortcut, _, $ */
(function () {
'use strict';
var MarkdownShortcuts,
MarkdownEditor;
MarkdownShortcuts = [
{'key': 'Ctrl+Alt+U', 'style': 'strike'},
{'key': 'Ctrl+Shift+K', 'style': 'code'},
{'key': 'Meta+K', 'style': 'code'},
{'key': 'Ctrl+Alt+1', 'style': 'h1'},
{'key': 'Ctrl+Alt+2', 'style': 'h2'},
{'key': 'Ctrl+Alt+3', 'style': 'h3'},
{'key': 'Ctrl+Alt+4', 'style': 'h4'},
{'key': 'Ctrl+Alt+5', 'style': 'h5'},
{'key': 'Ctrl+Alt+6', 'style': 'h6'},
{'key': 'Ctrl+Shift+L', 'style': 'link'},
{'key': 'Ctrl+Shift+I', 'style': 'image'},
{'key': 'Ctrl+Q', 'style': 'blockquote'},
{'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
{'key': 'Ctrl+U', 'style': 'uppercase'},
{'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
{'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
{'key': 'Ctrl+Alt+W', 'style': 'selectword'},
{'key': 'Ctrl+L', 'style': 'list'}
];
if (navigator.userAgent.indexOf('Mac') !== -1) {
MarkdownShortcuts.push({'key': 'Meta+B', 'style': 'bold'});
MarkdownShortcuts.push({'key': 'Meta+I', 'style': 'italic'});
MarkdownShortcuts.push({'key': 'Meta+Alt+C', 'style': 'copyHTML'});
MarkdownShortcuts.push({'key': 'Meta+Enter', 'style': 'newLine'});
} else {
MarkdownShortcuts.push({'key': 'Ctrl+B', 'style': 'bold'});
MarkdownShortcuts.push({'key': 'Ctrl+I', 'style': 'italic'});
MarkdownShortcuts.push({'key': 'Ctrl+Alt+C', 'style': 'copyHTML'});
MarkdownShortcuts.push({'key': 'Ctrl+Enter', 'style': 'newLine'});
}
MarkdownEditor = function () {
var codemirror = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'gfm',
tabMode: 'indent',
tabindex: '2',
cursorScrollMargin: 10,
lineWrapping: true,
dragDrop: false,
extraKeys: {
Home: 'goLineLeft',
End: 'goLineRight'
}
});
// Markdown shortcuts for the editor
_.each(MarkdownShortcuts, function (combo) {
shortcut.add(combo.key, function () {
return codemirror.addMarkdown({style: combo.style});
});
});
// Public API
_.extend(this, {
codemirror: codemirror,
scrollViewPort: function () {
return $('.CodeMirror-scroll');
},
scrollContent: function () {
return $('.CodeMirror-sizer');
},
enable: function () {
codemirror.setOption('readOnly', false);
codemirror.on('change', function () {
$(document).trigger('markdownEditorChange');
});
},
disable: function () {
codemirror.setOption('readOnly', 'nocursor');
codemirror.off('change', function () {
$(document).trigger('markdownEditorChange');
});
},
isCursorAtEnd: function () {
return codemirror.getCursor('end').line > codemirror.lineCount() - 5;
},
value: function () {
return codemirror.getValue();
}
});
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.MarkdownEditor = MarkdownEditor;
} ());

View File

@ -1,154 +0,0 @@
// # Ghost Editor Marker Manager
//
// MarkerManager looks after the array of markers which are attached to image markdown in the editor.
//
// Marker Manager is told by the Upload Manager to add a marker to a line.
// A marker takes the form of a 'magic id' which looks like:
// {<1>}
// It is appended to the start of the given line, and then defined as a CodeMirror 'TextMarker' widget which is
// subsequently added to an array of markers to keep track of all markers in the editor.
// The TextMarker is also set to 'collapsed' mode which means it does not show up in the display.
// Currently, the markers can be seen if you copy and paste your content out of Ghost into a text editor.
// The markers are stripped on save so should not appear in the DB
/*global _, Ghost */
(function () {
'use strict';
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
markerRegex = /\{<([\w\W]*?)>\}/,
MarkerManager;
MarkerManager = function (editor) {
var markers = {},
uploadPrefix = 'image_upload',
uploadId = 1,
addMarker,
removeMarker,
markerRegexForId,
stripMarkerFromLine,
findAndStripMarker,
checkMarkers,
initMarkers;
// the regex
markerRegexForId = function (id) {
id = id.replace('image_upload_', '');
return new RegExp('\\{<' + id + '>\\}', 'gmi');
};
// Add a marker to the given line
// Params:
// line - CodeMirror LineHandle
// ln - line number
addMarker = function (line, ln) {
var marker,
magicId = '{<' + uploadId + '>}',
newText = magicId + line.text;
editor.replaceRange(
newText,
{line: ln, ch: 0},
{line: ln, ch: newText.length}
);
marker = editor.markText(
{line: ln, ch: 0},
{line: ln, ch: (magicId.length)},
{collapsed: true}
);
markers[uploadPrefix + '_' + uploadId] = marker;
uploadId += 1;
};
// Remove a marker
// Will be passed a LineHandle if we already know which line the marker is on
removeMarker = function (id, marker, line) {
delete markers[id];
marker.clear();
if (line) {
stripMarkerFromLine(line);
} else {
findAndStripMarker(id);
}
};
// Removes the marker on the given line if there is one
stripMarkerFromLine = function (line) {
var markerText = line.text.match(markerRegex),
ln = editor.getLineNumber(line);
if (markerText) {
editor.replaceRange(
'',
{line: ln, ch: markerText.index},
{line: ln, ch: markerText.index + markerText[0].length}
);
}
};
// Find a marker in the editor by id & remove it
// Goes line by line to find the marker by it's text if we've lost track of the TextMarker
findAndStripMarker = function (id) {
editor.eachLine(function (line) {
var markerText = markerRegexForId(id).exec(line.text),
ln;
if (markerText) {
ln = editor.getLineNumber(line);
editor.replaceRange(
'',
{line: ln, ch: markerText.index},
{line: ln, ch: markerText.index + markerText[0].length}
);
}
});
};
// Check each marker to see if it is still present in the editor and if it still corresponds to image markdown
// If it is no longer a valid image, remove it
checkMarkers = function () {
_.each(markers, function (marker, id) {
var line;
marker = markers[id];
if (marker.find()) {
line = editor.getLineHandle(marker.find().from.line);
if (!line.text.match(imageMarkdownRegex)) {
removeMarker(id, marker, line);
}
} else {
removeMarker(id, marker);
}
});
};
// Add markers to the line if it needs one
initMarkers = function (line) {
var isImage = line.text.match(imageMarkdownRegex),
hasMarker = line.text.match(markerRegex);
if (isImage && !hasMarker) {
addMarker(line, editor.getLineNumber(line));
}
};
// Initialise
editor.eachLine(initMarkers);
// Public API
_.extend(this, {
markers: markers,
checkMarkers: checkMarkers,
addMarker: addMarker,
stripMarkerFromLine: stripMarkerFromLine,
getMarkerRegexForId: markerRegexForId
});
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.MarkerManager = MarkerManager;
}());

View File

@ -1,112 +0,0 @@
// Taken from js-bin with thanks to Remy Sharp
// yeah, nasty, but it allows me to switch from a RTF to plain text if we're running a iOS
/*global Ghost, $, _, DocumentTouch, CodeMirror*/
(function () {
Ghost.touchEditor = false;
var noop = function () {},
hasTouchScreen,
smallScreen,
TouchEditor,
_oldCM,
key;
// Taken from "Responsive design & the Guardian" with thanks to Matt Andrews
// Added !window._phantom so that the functional tests run as though this is not a touch screen.
// In future we can do something more advanced here for testing both touch and non touch
hasTouchScreen = function () {
return !window._phantom &&
(
('ontouchstart' in window) ||
(window.DocumentTouch && document instanceof DocumentTouch)
);
};
smallScreen = function () {
if (window.matchMedia('(max-width: 1000px)').matches) {
return true;
}
return false;
};
if (hasTouchScreen()) {
$('body').addClass('touch-editor');
Ghost.touchEditor = true;
TouchEditor = function (el, options) {
/*jshint unused:false*/
this.textarea = el;
this.win = { document : this.textarea };
this.ready = true;
this.wrapping = document.createElement('div');
var textareaParent = this.textarea.parentNode;
this.wrapping.appendChild(this.textarea);
textareaParent.appendChild(this.wrapping);
this.textarea.style.opacity = 1;
$(this.textarea).blur(_.throttle(function () {
$(document).trigger('markdownEditorChange', { panelId: el.id });
}, 200));
if (!smallScreen()) {
$(this.textarea).on('change', _.throttle(function () {
$(document).trigger('markdownEditorChange', { panelId: el.id });
}, 200));
}
};
TouchEditor.prototype = {
setOption: function (type, handler) {
if (type === 'onChange') {
$(this.textarea).change(handler);
}
},
eachLine: function () {
return [];
},
getValue: function () {
return this.textarea.value;
},
setValue: function (code) {
this.textarea.value = code;
},
focus: noop,
getCursor: function () {
return { line: 0, ch: 0 };
},
setCursor: noop,
currentLine: function () {
return 0;
},
cursorPosition: function () {
return { character: 0 };
},
addMarkdown: noop,
nthLine: noop,
refresh: noop,
selectLines: noop,
on: noop
};
_oldCM = CodeMirror;
// CodeMirror = noop;
for (key in _oldCM) {
if (_oldCM.hasOwnProperty(key)) {
CodeMirror[key] = noop;
}
}
CodeMirror.fromTextArea = function (el, options) {
return new TouchEditor(el, options);
};
CodeMirror.keyMap = { basic: {} };
}
}());

View File

@ -1,47 +0,0 @@
// # Ghost Editor Scroll Handler
//
// Scroll Handler does the (currently very simple / naive) job of syncing the right pane with the left pane
// as the right pane scrolls
/*global Ghost, _ */
(function () {
'use strict';
var ScrollHandler = function (markdown, preview) {
var $markdownViewPort = markdown.scrollViewPort(),
$previewViewPort = preview.scrollViewPort(),
$markdownContent = markdown.scrollContent(),
$previewContent = preview.scrollContent(),
syncScroll;
syncScroll = _.throttle(function () {
// calc position
var markdownHeight = $markdownContent.height() - $markdownViewPort.height(),
previewHeight = $previewContent.height() - $previewViewPort.height(),
ratio = previewHeight / markdownHeight,
previewPosition = $markdownViewPort.scrollTop() * ratio;
if (markdown.isCursorAtEnd()) {
previewPosition = previewHeight + 30;
}
// apply new scroll
$previewViewPort.scrollTop(previewPosition);
}, 10);
_.extend(this, {
enable: function () { // Handle Scroll Events
$markdownViewPort.on('scroll', syncScroll);
$markdownViewPort.scrollClass({target: '.entry-markdown', offset: 10});
$previewViewPort.scrollClass({target: '.entry-preview', offset: 10});
},
disable: function () {
$markdownViewPort.off('scroll', syncScroll);
}
});
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.ScrollHandler = ScrollHandler;
} ());

View File

@ -1,153 +0,0 @@
// # Ghost Editor Upload Manager
//
// UploadManager ensures that markdown gets updated when images get uploaded via the Preview.
//
// The Ghost Editor has a particularly tricky problem to solve, in that it is possible to upload an image by
// interacting with the preview. The process of uploading an image is handled by uploader.js, but there is still
// a lot of work needed to ensure that uploaded files end up in the right place - that is that the image
// path gets added to the correct piece of markdown in the editor.
//
// To solve this, Ghost adds a unique 'marker' to each piece of markdown which represents an image:
// More detail about how the markers work can be find in markerManager.js
//
// UploadManager handles changes in the editor, looking for text which matches image markdown, and telling the marker
// manager to add a marker. It also checks changed lines to see if they have a marker but are no longer an image.
//
// UploadManager's most important job is handling uploads such that when a successful upload completes, the correct
// piece of image markdown is updated with the path.
// This is done in part by ghostImagePreview.js, which takes the marker from the markdown and uses it to create an ID
// on the dropzone. When an upload completes successfully from uploader.js, the event thrown contains reference to the
// dropzone, from which uploadManager can pull the ID & then get the right marker from the Marker Manager.
//
// Without a doubt, the separation of concerns between the uploadManager, and the markerManager could be vastly
// improved
/*global $, _, Ghost */
(function () {
'use strict';
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
markerRegex = /\{<([\w\W]*?)>\}/,
UploadManager;
UploadManager = function (markdown) {
var editor = markdown.codemirror,
markerMgr = new Ghost.Editor.MarkerManager(editor),
findLine,
checkLine,
value,
handleUpload,
handleChange;
// Find the line with the marker which matches
findLine = function (result_id) {
// try to find the right line to replace
if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
}
return false;
};
// Check the given line to see if it has an image, and if it correctly has a marker
// In the special case of lines which were just pasted in, any markers are removed to prevent duplication
checkLine = function (ln, mode) {
var line = editor.getLineHandle(ln),
isImage = line.text.match(imageMarkdownRegex),
hasMarker;
// We care if it is an image
if (isImage) {
hasMarker = line.text.match(markerRegex);
if (hasMarker && (mode === 'paste' || mode === 'undo')) {
// this could be a duplicate, and won't be a real marker
markerMgr.stripMarkerFromLine(line);
}
if (!hasMarker) {
markerMgr.addMarker(line, ln);
}
}
// TODO: hasMarker but no image?
};
// Get the markdown with all the markers stripped
value = function () {
var value = editor.getValue();
_.each(markerMgr.markers, function (marker, id) {
/*jshint unused:false*/
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
});
return value;
};
// Match the uploaded file to a line in the editor, and update that line with a path reference
// ensuring that everything ends up in the correct place and format.
handleUpload = function (e, result_src) {
var line = findLine($(e.currentTarget).attr('id')),
lineNumber = editor.getLineNumber(line),
match = line.text.match(/\([^\n]*\)?/),
replacement = '(http://)';
if (match) {
// simple case, we have the parenthesis
editor.setSelection(
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + match[0].length - 1}
);
} else {
match = line.text.match(/\]/);
if (match) {
editor.replaceRange(
replacement,
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + 1}
);
editor.setSelection(
{line: lineNumber, ch: match.index + 2},
{line: lineNumber, ch: match.index + replacement.length }
);
}
}
editor.replaceSelection(result_src);
};
// Change events from CodeMirror tell us which lines have changed.
// Each changed line is then checked to see if a marker needs to be added or removed
handleChange = function (cm, changeObj) {
/*jshint unused:false*/
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
_.each(linesChanged, function (ln) {
checkLine(ln, changeObj.origin);
});
// Is this a line which may have had a marker on it?
markerMgr.checkMarkers();
};
// Public API
_.extend(this, {
value: value,
enable: function () {
var filestorage = $('#entry-markdown-content').data('filestorage');
$('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
$('.js-drop-zone').on('uploadstart', markdown.off);
$('.js-drop-zone').on('uploadfailure', markdown.on);
$('.js-drop-zone').on('uploadsuccess', markdown.on);
$('.js-drop-zone').on('uploadsuccess', handleUpload);
},
disable: function () {
$('.js-drop-zone').off('uploadsuccess', handleUpload);
}
});
editor.on('change', handleChange);
};
Ghost.Editor = Ghost.Editor || {};
Ghost.Editor.UploadManager = UploadManager;
}());

View File

@ -1,175 +0,0 @@
// # Ghost jQuery Utils
/*global window, document, $ */
(function () {
"use strict";
// ## UTILS
/**
* Allows to check contents of each element exactly
* @param {Object} obj
* @param {*} index
* @param {*} meta
* @param {*} stack
* @returns {boolean}
*/
$.expr[":"].containsExact = function (obj, index, meta, stack) {
/*jshint unused:false*/
return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3];
};
/**
* Center an element to the window vertically and centrally
* @returns {*}
*/
$.fn.center = function (options) {
var $window = $(window),
config = $.extend({
animate : true,
successTrigger : 'centered'
}, options);
return this.each(function () {
var $this = $(this);
$this.css({
'position': 'absolute'
});
if (config.animate) {
$this.animate({
'left': ($window.width() / 2) - $this.outerWidth() / 2 + 'px',
'top': ($window.height() / 2) - $this.outerHeight() / 2 + 'px'
});
} else {
$this.css({
'left': ($window.width() / 2) - $this.outerWidth() / 2 + 'px',
'top': ($window.height() / 2) - $this.outerHeight() / 2 + 'px'
});
}
$(window).trigger(config.successTrigger);
});
};
// ## getTransformProperty
// This returns the transition duration for an element, good for calling things after a transition has finished.
// **Original**: [https://gist.github.com/mandelbro/4067903](https://gist.github.com/mandelbro/4067903)
// **returns:** the elements transition duration
$.fn.transitionDuration = function () {
var $this = $(this);
// check the main transition duration property
if ($this.css('transition-duration')) {
return Math.round(parseFloat(this.css('transition-duration')) * 1000);
}
// check the vendor transition duration properties
if (this.css('-webkit-transition-duration')) {
return Math.round(parseFloat(this.css('-webkit-transition-duration')) * 1000);
}
if (this.css('-ms-transition-duration')) {
return Math.round(parseFloat(this.css('-ms-transition-duration')) * 1000);
}
if (this.css('-moz-transition-duration')) {
return Math.round(parseFloat(this.css('-moz-transition-duration')) * 1000);
}
if (this.css('-o-transition-duration')) {
return Math.round(parseFloat(this.css('-o-transition-duration')) * 1000);
}
// if we're here, then no transition duration was found, return 0
return 0;
};
// ## scrollShadow
// This adds a 'scroll' class to the targeted element when the element is scrolled
// **target:** The element in which the class is applied. Defaults to scrolled element.
// **class-name:** The class which is applied.
// **offset:** How far the user has to scroll before the class is applied.
$.fn.scrollClass = function (options) {
var config = $.extend({
'target' : '',
'class-name' : 'scrolling',
'offset' : 1
}, options);
return this.each(function () {
var $this = $(this),
$target = $this;
if (config.target) {
$target = $(config.target);
}
$this.scroll(function () {
if ($this.scrollTop() > config.offset) {
$target.addClass(config['class-name']);
} else {
$target.removeClass(config['class-name']);
}
});
});
};
$.fn.selectText = function () {
var elem = this[0],
range,
selection;
if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(elem);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(elem);
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Set interactions for all menus and overlays
* This finds all visible 'hideClass' elements and hides them upon clicking away from the element itself.
* A callback can be defined to customise the results. By default it will hide the element.
* @param {Function} callback
*/
$.fn.hideAway = function (callback) {
var $self = $(this);
$("body").on('click', function (event) {
var $target = $(event.target),
hideClass = $self.selector;
if (!$target.parents().is(hideClass + ":visible") && !$target.is(hideClass + ":visible")) {
if (callback) {
callback($("body").find(hideClass + ":visible"));
} else {
$("body").find(hideClass + ":visible").fadeOut(150);
// Toggle active classes on menu headers
$("[data-toggle].active").removeClass("active");
}
}
});
return this;
};
// ## GLOBALS
$('.overlay').hideAway();
/**
* Adds appropriate inflection for pluralizing the singular form of a word when appropriate.
* This is an overly simplistic implementation that does not handle irregular plurals.
* @param {Number} count
* @param {String} singularWord
* @returns {String}
*/
$.pluralize = function inflect(count, singularWord) {
var base = [count, ' ', singularWord];
return (count === 1) ? base.join('') : base.concat('s').join('');
};
}());

View File

@ -1,260 +0,0 @@
/*global jQuery, Ghost */
(function ($) {
"use strict";
var UploadUi;
UploadUi = function ($dropzone, settings) {
var $url = '<div class="js-url"><input class="url js-upload-url" type="url" placeholder="http://"/></div>',
$cancel = '<a class="image-cancel js-cancel" title="Delete"><span class="hidden">Delete</span></a>',
$progress = $('<div />', {
"class" : "js-upload-progress progress progress-success active",
"role": "progressbar",
"aria-valuemin": "0",
"aria-valuemax": "100"
}).append($("<div />", {
"class": "js-upload-progress-bar bar",
"style": "width:0%"
}));
$.extend(this, {
complete: function (result) {
var self = this;
function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').remove();
$dropzone.css({"height": "auto"});
$dropzone.delay(250).animate({opacity: 100}, 1000, function () {
$('.js-button-accept').prop('disabled', false);
self.init();
});
}
function animateDropzone($img) {
$dropzone.animate({opacity: 0}, 250, function () {
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
$dropzone.css({minHeight: 0});
self.removeExtras();
$dropzone.animate({height: $img.height()}, 250, function () {
showImage($img.width(), $img.height());
});
});
}
function preLoadImage() {
var $img = $dropzone.find('img.js-upload-target')
.attr({'src': '', "width": 'auto', "height": 'auto'});
$progress.animate({"opacity": 0}, 250, function () {
$dropzone.find('span.media').after('<img class="fileupload-loading" src="' + Ghost.paths.subdir + '/ghost/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
});
$dropzone.trigger("uploadsuccess", [result]);
$img.one('load', function () {
animateDropzone($img);
}).attr('src', result);
}
preLoadImage();
},
bindFileUpload: function () {
var self = this;
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
url: Ghost.paths.subdir + '/ghost/upload/',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
add: function (e, data) {
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', true);
$dropzone.find('.js-fileupload').removeClass('right');
$dropzone.find('.js-url').remove();
$progress.find('.js-upload-progress-bar').removeClass('fail');
$dropzone.trigger('uploadstart', [$dropzone.attr('id')]);
$dropzone.find('span.media, div.description, a.image-url, a.image-webcam')
.animate({opacity: 0}, 250, function () {
$dropzone.find('div.description').hide().css({"opacity": 100});
if (settings.progressbar) {
$dropzone.find('div.js-fail').after($progress);
$progress.animate({opacity: 100}, 250);
}
data.submit();
});
},
dropZone: settings.fileStorage ? $dropzone : null,
progressall: function (e, data) {
/*jshint unused:false*/
var progress = parseInt(data.loaded / data.total * 100, 10);
if (!settings.editor) {$progress.find('div.js-progress').css({"position": "absolute", "top": "40px"}); }
if (settings.progressbar) {
$dropzone.trigger("uploadprogress", [progress, data]);
$progress.find('.js-upload-progress-bar').css('width', progress + '%');
}
},
fail: function (e, data) {
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', false);
$dropzone.trigger("uploadfailure", [data.result]);
$dropzone.find('.js-upload-progress-bar').addClass('fail');
if (data.jqXHR.status === 413) {
$dropzone.find('div.js-fail').text("The image you uploaded was larger than the maximum file size your server allows.");
} else if (data.jqXHR.status === 415) {
$dropzone.find('div.js-fail').text("The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.");
} else {
$dropzone.find('div.js-fail').text("Something went wrong :(");
}
$dropzone.find('div.js-fail, button.js-fail').fadeIn(1500);
$dropzone.find('button.js-fail').on('click', function () {
$dropzone.css({minHeight: 0});
$dropzone.find('div.description').show();
self.removeExtras();
self.init();
});
},
done: function (e, data) {
/*jshint unused:false*/
self.complete(data.result);
}
});
},
buildExtras: function () {
if (!$dropzone.find('span.media')[0]) {
$dropzone.prepend('<span class="media"><span class="hidden">Image Upload</span></span>');
}
if (!$dropzone.find('div.description')[0]) {
$dropzone.append('<div class="description">Add image</div>');
}
if (!$dropzone.find('div.js-fail')[0]) {
$dropzone.append('<div class="js-fail failed" style="display: none">Something went wrong :(</div>');
}
if (!$dropzone.find('button.js-fail')[0]) {
$dropzone.append('<button class="js-fail button-add" style="display: none">Try Again</button>');
}
if (!$dropzone.find('a.image-url')[0]) {
$dropzone.append('<a class="image-url" title="Add image from URL"><span class="hidden">URL</span></a>');
}
// if (!$dropzone.find('a.image-webcam')[0]) {
// $dropzone.append('<a class="image-webcam" title="Add image from webcam"><span class="hidden">Webcam</span></a>');
// }
},
removeExtras: function () {
$dropzone.find('span.media, div.js-upload-progress, a.image-url, a.image-upload, a.image-webcam, div.js-fail, button.js-fail, a.js-cancel').remove();
},
initWithDropzone: function () {
var self = this;
//This is the start point if no image exists
$dropzone.find('img.js-upload-target').css({"display": "none"});
$dropzone.removeClass('pre-image-uploader image-uploader-url').addClass('image-uploader');
this.removeExtras();
this.buildExtras();
this.bindFileUpload();
if (!settings.fileStorage) {
self.initUrl();
return;
}
$dropzone.find('a.image-url').on('click', function () {
self.initUrl();
});
},
initUrl: function () {
var self = this, val;
this.removeExtras();
$dropzone.addClass('image-uploader-url').removeClass('pre-image-uploader');
$dropzone.find('.js-fileupload').addClass('right');
if (settings.fileStorage) {
$dropzone.append($cancel);
}
$dropzone.find('.js-cancel').on('click', function () {
$dropzone.find('.js-url').remove();
$dropzone.find('.js-fileupload').removeClass('right');
self.removeExtras();
self.initWithDropzone();
});
$dropzone.find('div.description').before($url);
if (settings.editor) {
$dropzone.find('div.js-url').append('<button class="js-button-accept button-save">Save</button>');
}
$dropzone.find('.js-button-accept').on('click', function () {
val = $dropzone.find('.js-upload-url').val();
$dropzone.find('div.description').hide();
$dropzone.find('.js-fileupload').removeClass('right');
$dropzone.find('.js-url').remove();
if (val === "") {
$dropzone.trigger("uploadsuccess", 'http://');
self.initWithDropzone();
} else {
self.complete(val);
}
});
// Only show the toggle icon if there is a dropzone mode to go back to
if (settings.fileStorage !== false) {
$dropzone.append('<a class="image-upload" title="Add image"><span class="hidden">Upload</span></a>');
}
$dropzone.find('a.image-upload').on('click', function () {
$dropzone.find('.js-url').remove();
$dropzone.find('.js-fileupload').removeClass('right');
self.initWithDropzone();
});
},
initWithImage: function () {
var self = this;
// This is the start point if an image already exists
$dropzone.removeClass('image-uploader image-uploader-url').addClass('pre-image-uploader');
$dropzone.find('div.description').hide();
$dropzone.append($cancel);
$dropzone.find('.js-cancel').on('click', function () {
$dropzone.find('img.js-upload-target').attr({'src': ''});
$dropzone.find('div.description').show();
$dropzone.delay(2500).animate({opacity: 100}, 1000, function () {
self.init();
});
$dropzone.trigger("uploadsuccess", 'http://');
self.initWithDropzone();
});
},
init: function () {
// First check if field image is defined by checking for js-upload-target class
if (!$dropzone.find('img.js-upload-target')[0]) {
// This ensures there is an image we can hook into to display uploaded image
$dropzone.prepend('<img class="js-upload-target" style="display: none" src="" />');
}
$('.js-button-accept').prop('disabled', false);
if ($dropzone.find('img.js-upload-target').attr('src') === '') {
this.initWithDropzone();
} else {
this.initWithImage();
}
}
});
};
$.fn.upload = function (options) {
var settings = $.extend({
progressbar: true,
editor: false,
fileStorage: true
}, options);
return this.each(function () {
var $dropzone = $(this),
ui;
ui = new UploadUi($dropzone, settings);
ui.init();
});
};
}(jQuery));

View File

@ -1,224 +0,0 @@
/**
* http://www.openjs.com/scripts/events/keyboard_shortcuts/
* Version : 2.01.B
* By Binny V A
* License : BSD
*/
shortcut = {
'all_shortcuts':{},//All the shortcuts are stored in this array
'add': function(shortcut_combination,callback,opt) {
//Provide a set of default options
var default_options = {
'type':'keydown',
'propagate':false,
'disable_in_input':false,
'target':document,
'keycode':false
}
if(!opt) opt = default_options;
else {
for(var dfo in default_options) {
if(typeof opt[dfo] == 'undefined') opt[dfo] = default_options[dfo];
}
}
var ele = opt.target;
if(typeof opt.target == 'string') ele = document.getElementById(opt.target);
var ths = this;
shortcut_combination = shortcut_combination.toLowerCase();
//The function to be called at keypress
var func = function(e) {
e = e || window.event;
if(opt['disable_in_input']) { //Don't enable shortcut keys in Input, Textarea fields
var element;
if(e.target) element=e.target;
else if(e.srcElement) element=e.srcElement;
if(element.nodeType==3) element=element.parentNode;
if(element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') return;
}
//Find Which key is pressed
if (e.keyCode) code = e.keyCode;
else if (e.which) code = e.which;
else return;
var character = String.fromCharCode(code).toLowerCase();
if(code == 188) character=","; //If the user presses , when the type is onkeydown
if(code == 190) character="."; //If the user presses , when the type is onkeydown
var keys = shortcut_combination.split("+");
//Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
var kp = 0;
//Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
var shift_nums = {
"`":"~",
"1":"!",
"2":"@",
"3":"#",
"4":"$",
"5":"%",
"6":"^",
"7":"&",
"8":"*",
"9":"(",
"0":")",
"-":"_",
"=":"+",
";":":",
"'":"\"",
",":"<",
".":">",
"/":"?",
"\\":"|"
}
//Special Keys - and their codes
var special_keys = {
'esc':27,
'escape':27,
'tab':9,
'space':32,
'return':13,
'enter':13,
'backspace':8,
'scrolllock':145,
'scroll_lock':145,
'scroll':145,
'capslock':20,
'caps_lock':20,
'caps':20,
'numlock':144,
'num_lock':144,
'num':144,
'pause':19,
'break':19,
'insert':45,
'home':36,
'delete':46,
'end':35,
'pageup':33,
'page_up':33,
'pu':33,
'pagedown':34,
'page_down':34,
'pd':34,
'left':37,
'up':38,
'right':39,
'down':40,
'f1':112,
'f2':113,
'f3':114,
'f4':115,
'f5':116,
'f6':117,
'f7':118,
'f8':119,
'f9':120,
'f10':121,
'f11':122,
'f12':123
}
var modifiers = {
shift: { wanted:false, pressed:false},
ctrl : { wanted:false, pressed:false},
alt : { wanted:false, pressed:false},
meta : { wanted:false, pressed:false} //Meta is Mac specific
};
if(e.ctrlKey) modifiers.ctrl.pressed = true;
if(e.shiftKey) modifiers.shift.pressed = true;
if(e.altKey) modifiers.alt.pressed = true;
if(e.metaKey) modifiers.meta.pressed = true;
for(var i=0; k=keys[i],i<keys.length; i++) {
//Modifiers
if(k == 'ctrl' || k == 'control') {
kp++;
modifiers.ctrl.wanted = true;
} else if(k == 'shift') {
kp++;
modifiers.shift.wanted = true;
} else if(k == 'alt') {
kp++;
modifiers.alt.wanted = true;
} else if(k == 'meta') {
kp++;
modifiers.meta.wanted = true;
} else if(k.length > 1) { //If it is a special key
if(special_keys[k] == code) kp++;
} else if(opt['keycode']) {
if(opt['keycode'] == code) kp++;
} else { //The special keys did not match
if(character == k) kp++;
else {
if(shift_nums[character] && e.shiftKey) { //Stupid Shift key bug created by using lowercase
character = shift_nums[character];
if(character == k) kp++;
}
}
}
}
if(kp == keys.length &&
modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
modifiers.shift.pressed == modifiers.shift.wanted &&
modifiers.alt.pressed == modifiers.alt.wanted &&
modifiers.meta.pressed == modifiers.meta.wanted) {
callback(e);
if(!opt['propagate']) { //Stop the event
//e.cancelBubble is supported by IE - this will kill the bubbling process.
e.cancelBubble = true;
e.returnValue = false;
//e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return false;
}
}
}
this.all_shortcuts[shortcut_combination] = {
'callback':func,
'target':ele,
'event': opt['type']
};
//Attach the function with the event
if(ele.addEventListener) ele.addEventListener(opt['type'], func, false);
else if(ele.attachEvent) ele.attachEvent('on'+opt['type'], func);
else ele['on'+opt['type']] = func;
},
//Remove the shortcut - just specify the shortcut and I will remove the binding
'remove':function(shortcut_combination) {
shortcut_combination = shortcut_combination.toLowerCase();
var binding = this.all_shortcuts[shortcut_combination];
delete(this.all_shortcuts[shortcut_combination])
if(!binding) return;
var type = binding['event'];
var ele = binding['target'];
var callback = binding['callback'];
if(ele.detachEvent) ele.detachEvent('on'+type, callback);
else if(ele.removeEventListener) ele.removeEventListener(type, callback, false);
else ele['on'+type] = false;
}
};

View File

@ -1,22 +0,0 @@
/*
* To Title Case 2.0.1 http://individed.com/code/to-title-case/
* Copyright © 20082012 David Gouch. Licensed under the MIT License.
*/
String.prototype.toTitleCase = function () {
var smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|vs?\.?|via)$/i;
return this.replace(/([^\W_]+[^\s-]*) */g, function (match, p1, index, title) {
if (index > 0 && index + p1.length !== title.length &&
p1.search(smallWords) > -1 && title.charAt(index - 2) !== ":" &&
title.charAt(index - 1).search(/[^\s-]/) < 0) {
return match.toLowerCase();
}
if (p1.substr(1).search(/[A-Z]|\../) > -1) {
return match;
}
return match.charAt(0).toUpperCase() + match.substr(1);
});
};

View File

@ -1,53 +0,0 @@
/*globals Handlebars, moment, Ghost */
(function () {
'use strict';
Handlebars.registerHelper('date', function (context, options) {
if (!options && context.hasOwnProperty('hash')) {
options = context;
context = undefined;
// set to published_at by default, if it's available
// otherwise, this will print the current date
if (this.published_at) {
context = this.published_at;
}
}
// ensure that context is undefined, not null, as that can cause errors
context = context === null ? undefined : context;
var f = options.hash.format || 'MMM Do, YYYY',
timeago = options.hash.timeago,
date;
if (timeago) {
date = moment(context).fromNow();
} else {
date = moment(context).format(f);
}
return date;
});
Handlebars.registerHelper('admin_url', function () {
return Ghost.paths.subdir + '/ghost';
});
Handlebars.registerHelper('asset', function (context, options) {
var output = '',
isAdmin = options && options.hash && options.hash.ghost;
output += Ghost.paths.subdir + '/';
if (!context.match(/^shared/)) {
if (isAdmin) {
output += 'ghost/';
} else {
output += 'assets/';
}
}
output += context;
return new Handlebars.SafeString(output);
});
}());

View File

@ -1,83 +0,0 @@
/*globals window, $, _, Backbone, validator */
(function () {
'use strict';
function ghostPaths() {
var path = window.location.pathname,
subdir = path.substr(0, path.search('/ghost/'));
return {
subdir: subdir,
apiRoot: subdir + '/ghost/api/v0.1'
};
}
var Ghost = {
Layout : {},
Views : {},
Collections : {},
Models : {},
paths: ghostPaths(),
// This is a helper object to denote legacy things in the
// middle of being transitioned.
temporary: {},
currentView: null,
router: null
};
_.extend(Ghost, Backbone.Events);
Backbone.oldsync = Backbone.sync;
// override original sync method to make header request contain csrf token
Backbone.sync = function (method, model, options, error) {
options.beforeSend = function (xhr) {
xhr.setRequestHeader('X-CSRF-Token', $("meta[name='csrf-param']").attr('content'));
};
/* call the old sync method */
return Backbone.oldsync(method, model, options, error);
};
Backbone.oldModelProtoUrl = Backbone.Model.prototype.url;
//overwrite original url method to add slash to end of the url if needed.
Backbone.Model.prototype.url = function () {
var url = Backbone.oldModelProtoUrl.apply(this, arguments);
return url + (url.charAt(url.length - 1) === '/' ? '' : '/');
};
Ghost.init = function () {
Ghost.router = new Ghost.Router();
// This is needed so Backbone recognizes elements already rendered server side
// as valid views, and events are bound
Ghost.notifications = new Ghost.Views.NotificationCollection({model: []});
Backbone.history.start({
pushState: true,
hashChange: false,
root: Ghost.paths.subdir + '/ghost'
});
};
validator.handleErrors = function (errors) {
Ghost.notifications.clearEverything();
_.each(errors, function (errorObj) {
Ghost.notifications.addItem({
type: 'error',
message: errorObj.message || errorObj,
status: 'passive'
});
if (errorObj.hasOwnProperty('el')) {
errorObj.el.addClass('input-error');
}
});
};
window.Ghost = Ghost;
window.addEventListener("load", Ghost.init, false);
}());

View File

@ -1,149 +0,0 @@
// # Surrounds given text with Markdown syntax
/*global $, CodeMirror, Showdown, moment */
(function () {
'use strict';
var Markdown = {
init : function (options, elem) {
var self = this;
self.elem = elem;
self.style = (typeof options === 'string') ? options : options.style;
self.options = $.extend({}, CodeMirror.prototype.addMarkdown.options, options);
self.replace();
},
replace: function () {
var text = this.elem.getSelection(), pass = true, cursor = this.elem.getCursor(), line = this.elem.getLine(cursor.line), md, word, letterCount, converter, textIndex, position;
switch (this.style) {
case 'h1':
this.elem.setLine(cursor.line, '# ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 2);
pass = false;
break;
case 'h2':
this.elem.setLine(cursor.line, '## ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 3);
pass = false;
break;
case 'h3':
this.elem.setLine(cursor.line, '### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 4);
pass = false;
break;
case 'h4':
this.elem.setLine(cursor.line, '#### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 5);
pass = false;
break;
case 'h5':
this.elem.setLine(cursor.line, '##### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 6);
pass = false;
break;
case 'h6':
this.elem.setLine(cursor.line, '###### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 7);
pass = false;
break;
case 'link':
md = this.options.syntax.link.replace('$1', text);
this.elem.replaceSelection(md, 'end');
if (!text) {
this.elem.setCursor(cursor.line, cursor.ch + 1);
} else {
textIndex = line.indexOf(text, cursor.ch - text.length);
position = textIndex + md.length - 1;
this.elem.setSelection({line: cursor.line, ch: position - 7}, {line: cursor.line, ch: position});
}
pass = false;
break;
case 'image':
md = this.options.syntax.image.replace('$1', text);
if (line !== '') {
md = "\n\n" + md;
}
this.elem.replaceSelection(md, "end");
cursor = this.elem.getCursor();
this.elem.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
pass = false;
break;
case 'uppercase':
md = text.toLocaleUpperCase();
break;
case 'lowercase':
md = text.toLocaleLowerCase();
break;
case 'titlecase':
md = text.toTitleCase();
break;
case 'selectword':
word = this.elem.getTokenAt(cursor);
if (!/\w$/g.test(word.string)) {
this.elem.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
} else {
this.elem.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
}
break;
case 'copyHTML':
converter = new Showdown.converter();
if (text) {
md = converter.makeHtml(text);
} else {
md = converter.makeHtml(this.elem.getValue());
}
$(".modal-copyToHTML-content").text(md).selectText();
pass = false;
break;
case 'list':
md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2');
this.elem.replaceSelection(md, 'end');
pass = false;
break;
case 'currentDate':
md = moment(new Date()).format('D MMMM YYYY');
this.elem.replaceSelection(md, 'end');
pass = false;
break;
case 'newLine':
if (line !== "") {
this.elem.setLine(cursor.line, line + "\n\n");
}
pass = false;
break;
default:
if (this.options.syntax[this.style]) {
md = this.options.syntax[this.style].replace('$1', text);
}
}
if (pass && md) {
this.elem.replaceSelection(md, 'end');
if (!text) {
letterCount = md.length;
this.elem.setCursor({line: cursor.line, ch: cursor.ch - (letterCount / 2)});
}
}
}
};
CodeMirror.prototype.addMarkdown = function (options) {
var markdown = Object.create(Markdown);
markdown.init(options, this);
};
CodeMirror.prototype.addMarkdown.options = {
style: null,
syntax: {
bold: '**$1**',
italic: '*$1*',
strike: '~~$1~~',
code: '`$1`',
link: '[$1](http://)',
image: '![$1](http://)',
blockquote: '> $1'
}
};
}());

View File

@ -1,62 +0,0 @@
// # Ghost Mobile Interactions
/*global window, document, $, FastClick */
(function () {
'use strict';
FastClick.attach(document.body);
// ### general wrapper to handle conditional screen size actions
function responsiveAction(event, mediaCondition, cb) {
if (!window.matchMedia(mediaCondition).matches) {
return;
}
event.preventDefault();
event.stopPropagation();
cb();
}
// ### Show content preview when swiping left on content list
$('.manage').on('click', '.content-list ol li', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300);
$('.content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
});
});
// ### Hide content preview
$('.manage').on('click', '.content-preview .button-back', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300);
});
});
// ### Show settings options page when swiping left on settings menu link
$('.settings').on('click', '.settings-menu li', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.settings-sidebar').animate({right: '100%', left: '-102%', 'margin-right': '15px'}, 300);
$('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
$('.settings-content .button-back, .settings-content .button-save').css('display', 'inline-block');
});
});
// ### Hide settings options page
$('.settings').on('click', '.settings-content .button-back', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300);
$('.settings-content .button-back, .settings-content .button-save').css('display', 'none');
});
});
// ### Toggle the sidebar menu
$('[data-off-canvas]').on('click', function (event) {
responsiveAction(event, '(max-width: 650px)', function () {
$('body').toggleClass('off-canvas');
});
});
}());

View File

@ -1,35 +0,0 @@
/*global Ghost, _, Backbone, NProgress */
(function () {
"use strict";
NProgress.configure({ showSpinner: false });
// 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();
/*jshint validthis:true */
var self = this,
oldSuccess = options.success;
/*jshint validthis:false */
options.success = function () {
NProgress.done();
return oldSuccess.apply(self, arguments);
};
}
/*jshint validthis:true */
return Backbone.sync.call(this, method, model, options);
}
Ghost.ProgressModel = Backbone.Model.extend({
sync: wrapSync
});
Ghost.ProgressCollection = Backbone.Collection.extend({
sync: wrapSync
});
}());

View File

@ -1,83 +0,0 @@
/*global Ghost, _, Backbone, JSON */
(function () {
'use strict';
Ghost.Models.Post = Ghost.ProgressModel.extend({
defaults: {
status: 'draft'
},
blacklist: ['published', 'draft'],
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;
});
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;
}
});
}());

View File

@ -1,33 +0,0 @@
/*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',
parse: function (response) {
var result = _.reduce(response.settings, function (settings, setting) {
settings[setting.key] = setting.value;
return settings;
}, {});
return result;
},
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';
}
return Backbone.Model.prototype.sync.apply(this, arguments);
}
});
}());

View File

@ -1,12 +0,0 @@
/*global Ghost */
(function () {
'use strict';
Ghost.Collections.Tags = Ghost.ProgressCollection.extend({
url: Ghost.paths.apiRoot + '/tags/',
parse: function (resp) {
return resp.tags;
}
});
}());

View File

@ -1,9 +0,0 @@
/*global Ghost, Backbone */
(function () {
'use strict';
Ghost.Models.Themes = Backbone.Model.extend({
url: Ghost.paths.apiRoot + '/themes/'
});
}());

View File

@ -1,38 +0,0 @@
/*global Ghost, Backbone, $ */
(function () {
'use strict';
Ghost.Models.uploadModal = Backbone.Model.extend({
options: {
close: true,
type: 'action',
style: ["wide"],
animation: 'fade',
afterRender: function () {
var filestorage = $('#' + this.options.model.id).data('filestorage');
this.$('.js-drop-zone').upload({fileStorage: filestorage});
},
confirm: {
reject: {
func: function () { // The function called on rejection
return true;
},
buttonClass: true,
text: "Cancel" // The reject button text
}
}
},
content: {
template: 'uploadImage'
},
initialize: function (options) {
this.options.id = options.id;
this.options.key = options.key;
this.options.src = options.src;
this.options.confirm.accept = options.accept;
this.options.acceptEncoding = options.acceptEncoding || 'image/*';
}
});
}());

View File

@ -1,32 +0,0 @@
/*global Ghost,Backbone */
(function () {
'use strict';
Ghost.Models.User = Ghost.ProgressModel.extend({
url: Ghost.paths.apiRoot + '/users/me/',
parse: function (resp) {
// unwrap user from {users: [{...}]}
if (resp.users) {
resp = resp.users[0];
}
return resp;
},
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);
}
});
// Ghost.Collections.Users = Backbone.Collection.extend({
// url: Ghost.paths.apiRoot + '/users/'
// });
}());

View File

@ -1,43 +0,0 @@
/*global Ghost */
(function () {
'use strict';
Ghost.Models.Widget = Ghost.ProgressModel.extend({
defaults: {
title: '',
name: '',
author: '',
applicationID: '',
size: '',
content: {
template: '',
data: {
number: {
count: 0,
sub: {
value: 0,
dir: '', // "up" or "down"
item: '',
period: ''
}
}
}
},
settings: {
settingsPane: false,
enabled: false,
options: [{
title: 'ERROR',
value: 'Widget options not set'
}]
}
}
});
Ghost.Collections.Widgets = Ghost.ProgressCollection.extend({
// url: Ghost.paths.apiRoot + '/widgets/', // What will this be?
model: Ghost.Models.Widget
});
}());

View File

@ -1,78 +0,0 @@
/*global Ghost, Backbone, NProgress */
(function () {
"use strict";
Ghost.Router = Backbone.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'
},
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" });
}
});
}());

View File

@ -1,57 +0,0 @@
// # Toggle Support
/*global document, $, Ghost */
(function () {
'use strict';
Ghost.temporary.hideToggles = function () {
$('[data-toggle]').each(function () {
var toggle = $(this).data('toggle');
$(this).parent().children(toggle + ':visible').fadeOut(150);
});
// Toggle active classes on menu headers
$('[data-toggle].active').removeClass('active');
};
Ghost.temporary.initToggles = function ($el) {
$el.find('[data-toggle]').each(function () {
var toggle = $(this).data('toggle');
$(this).parent().children(toggle).hide();
});
$el.find('[data-toggle]').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var $this = $(this),
toggle = $this.data('toggle'),
isAlreadyActive = $this.is('.active');
// Close all the other open toggle menus
Ghost.temporary.hideToggles();
if (!isAlreadyActive) {
$this.toggleClass('active');
$(this).parent().children(toggle).toggleClass('open').fadeToggle(150);
}
});
};
$(document).ready(function () {
// ## Toggle Up In Your Grill
// Allows for toggling via data-attributes.
// ### Usage
// <nav>
// <a href="#" data-toggle=".toggle-me">Toggle</a>
// <ul class="toggle-me">
// <li>Toggled yo</li>
// </ul>
// </nav>
Ghost.temporary.initToggles($(document));
});
}());

View File

@ -1,6 +0,0 @@
<form id="forgotten" class="forgotten-form" method="post" novalidate="novalidate">
<div class="email-wrap">
<input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off">
</div>
<button class="button-save" type="submit">Send new password</button>
</form>

View File

@ -1,18 +0,0 @@
<a class="permalink{{#if featured}} featured{{/if}}{{#if page}} page{{/if}}" href="#" title="Edit this post">
<h3 class="entry-title">{{{title}}}</h3>
<section class="entry-meta">
<span class="status">
{{#if published}}
{{#if page}}
<span class="page">Page</span>
{{else}}
<time datetime="{{date published_at format="YYYY-MM-DD hh:mm"}}" class="date published">
Published {{date published_at timeago="True"}}
</time>
{{/if}}
{{else}}
<span class="draft">Draft</span>
{{/if}}
</span>
</section>
</a>

View File

@ -1,12 +0,0 @@
<form id="login" class="login-form" method="post" novalidate="novalidate">
<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">Log in</button>
<section class="meta">
<a class="forgotten-password" href="{{admin_url}}/forgotten/">Forgotten password?</a>
</section>
</form>

View File

@ -1,14 +0,0 @@
<article class="modal{{#if options.type}}-{{options.type}}{{/if}} {{#if options.style}}{{#each options.style}}modal-style-{{this}} {{/each}}{{/if}}{{options.animation}} js-modal">
<section class="modal-content">
{{#if content.title}}<header class="modal-header"><h1>{{content.title}}</h1></header>{{/if}}
{{#if options.close}}<a class="close" href="#" title="Close"><span class="hidden">Close</span></a>{{/if}}
<section class="modal-body">
</section>
{{#if options.confirm}}
<footer class="modal-footer">
<button class="js-button-accept {{#if options.confirm.accept.buttonClass}}{{options.confirm.accept.buttonClass}}{{else}}button-add{{/if}}">{{options.confirm.accept.text}}</button>
<button class="js-button-reject {{#if options.confirm.reject.buttonClass}}{{options.confirm.reject.buttonClass}}{{else}}button-delete{{/if}}">{{options.confirm.reject.text}}</button>
</footer>
{{/if}}
</section>
</article>

View File

@ -1 +0,0 @@
{{{content.text}}}

View File

@ -1,4 +0,0 @@
Press Ctrl / Cmd + C to copy the following HTML.
<pre>
<code class="modal-copyToHTML-content"></code>
</pre>

View File

@ -1,69 +0,0 @@
<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>
<tr>
<td><strong>Bold</strong></td>
<td>**text**</td>
<td>Ctrl / Cmd + B</td>
</tr>
<tr>
<td><em>Emphasize</em></td>
<td>*text*</td>
<td>Ctrl / Cmd + I</td>
</tr>
<tr>
<td>Strike-through</td>
<td>~~text~~</td>
<td>Ctrl + Alt + U</td>
</tr>
<tr>
<td><a href="#">Link</a></td>
<td>[title](http://)</td>
<td>Ctrl + Shift + L</td>
</tr>
<tr>
<td>Image</td>
<td>![alt](http://)</td>
<td>Ctrl + Shift + I</td>
</tr>
<tr>
<td>List</td>
<td>* item</td>
<td>Ctrl + L</td>
</tr>
<tr>
<td>Blockquote</td>
<td>> quote</td>
<td>Ctrl + Q</td>
</tr>
<tr>
<td>H1</td>
<td># Heading</td>
<td>Ctrl + Alt + 1</td>
</tr>
<tr>
<td>H2</td>
<td>## Heading</td>
<td>Ctrl + Alt + 2</td>
</tr>
<tr>
<td>H3</td>
<td>### Heading</td>
<td>Ctrl + Alt + 3</td>
</tr>
<tr>
<td><code>Inline Code</code></td>
<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>

View File

@ -1,4 +0,0 @@
<section class="js-drop-zone">
<img class="js-upload-target" src="{{options.src}}"{{#unless options.src}} style="display: none"{{/unless}} alt="logo">
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" {{#if options.acceptEncoding}}accept="{{options.acceptEncoding}}"{{/if}}>
</section>

View File

@ -1,4 +0,0 @@
<section class="notification{{#if type}}-{{type}}{{/if}} notification-{{status}} js-notification">
{{{message}}}
<a class="close" href="#"><span class="hidden">Close</span></a>
</section>

View File

@ -1,58 +0,0 @@
<header class="floatingheader">
<button class="button-back" href="#">Back</button>
<a class="{{#if featured}}featured{{else}}unfeatured{{/if}}" href="#" title="{{#if featured}}Unfeature{{else}}Feature{{/if}} this post">
<span class="hidden">Star</span>
</a>
<small>
<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">
<a class="post-edit" href="#" title="Edit Post"><span class="hidden">Edit Post</span></a>
<a class="post-settings" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-drop-right overlay">
<form>
<table class="plain">
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" value="" />
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" value=""><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
</section>
</header>
<section class="content-preview-content">
<div class="wrapper"><h1>{{{title}}}</h1>{{{html}}}</div>
</section>
{{#unless title}}
<div class="no-posts-box">
<div class="no-posts">
<h3>You Haven't Written Any Posts Yet!</h3>
<form action="{{admin_url}}/editor/"><button class="button-add large" title="New Post">Write a new Post</button></form>
</div>
</div>
{{/unless}}

View File

@ -1,9 +0,0 @@
<form id="reset" class="reset-form" method="post" novalidate="novalidate">
<div class="password-wrap">
<input class="password" type="password" placeholder="Password" name="newpassword" />
</div>
<div class="password-wrap">
<input class="password" type="password" placeholder="Confirm Password" name="ne2password" />
</div>
<button class="button-save" type="submit">Reset Password</button>
</form>

View File

@ -1,15 +0,0 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">Apps</h2>
</header>
<section class="content">
<ul class="js-apps">
{{#each availableApps}}
<li>
{{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}} - package.json missing :({{/if}}
<button data-app="{{name}}" class="{{#if active}}button-delete js-button-deactivate js-button-active">Deactivate{{else}}button-add js-button-activate">Activate{{/if}}</button>
</li>
{{/each}}
</ul>
</section>

View File

@ -1,81 +0,0 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">General</h2>
<section class="page-actions">
<button class="button-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">{{description}}</textarea>
<p>
Describe what your blog is about
<span class="word-count">0</span>
</p>
</div>
</fieldset>
<div class="form-group">
<label for="blog-logo">Blog Logo</label>
{{#if logo}}
<a class="js-modal-logo" href="#"><img id="blog-logo" src="{{logo}}" alt="logo"></a>
{{else}}
<a class="button-add js-modal-logo" >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="#"><img id="blog-cover" src="{{cover}}" alt="cover photo"></a>
{{else}}
<a class="button-add js-modal-cover">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" value='permalink'/>
<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>
{{#unless package}}<script>console.log('Hi! The theme named "{{name}}" does not have a package.json file or it\'s malformed. This will be required in the future. For more info, see http://docs.ghost.org/themes/.');</script>{{/unless}}
{{/each}}
</select>
<p>Select a theme for your blog</p>
</div>
</fieldset>
</form>
</section>

View File

@ -1,10 +0,0 @@
<header>
<h1 class="title">Settings</h1>
</header>
<nav class="settings-menu">
<ul>
<li class="general"><a href="#general">General</a></li>
<li class="users"><a href="#user">User</a></li>
<li class="apps"><a href="#apps">Apps</a></li>
</ul>
</nav>

View File

@ -1,90 +0,0 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">Your Profile</h2>
<section class="page-actions">
<button class="button-save">Save</button>
</section>
</header>
<section class="content no-padding">
<header class="user-profile-header">
<img id="user-cover" class="cover-image" src="{{#if cover}}{{cover}}{{else}}{{asset "shared/img/user-cover.png"}}{{/if}}" title="{{name}}'s Cover Image"/>
<a class="edit-cover-image js-modal-cover button" href="#">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" style="background-image: url({{#if image}}{{image}}{{else}}{{asset "shared/img/user-image.png"}}{{/if}});" href="#"><span class="hidden">{{name}}'s Picture</span></div>
<a href="#" 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 type="text" value="{{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="{{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="{{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="{{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">{{bio}}</textarea>
<p>
Write about you, in 200 characters or less.
<span class="word-count">0</span>
</p>
</div>
<hr />
</fieldset>
<fieldset>
<div class="form-group">
<label for="user-password-old">Old Password</label>
<input type="password" id="user-password-old" />
</div>
<div class="form-group">
<label for="user-password-new">New Password</label>
<input type="password" id="user-password-new" />
</div>
<div class="form-group">
<label for="user-new-password-verification">Verify Password</label>
<input type="password" id="user-new-password-verification" />
</div>
<div class="form-group">
<button type="button" class="button-delete button-change-password">Change Password</button>
</div>
</fieldset>
</form>
</section>

View File

@ -1,12 +0,0 @@
<form id="signup" class="signup-form" method="post" novalidate="novalidate">
<div class="name-wrap">
<input class="name" type="text" placeholder="Full Name" name="name" 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>

View File

@ -1,407 +0,0 @@
/*global window, document, setTimeout, Ghost, $, _, Backbone, JST, shortcut */
(function () {
"use strict";
Ghost.TemplateView = Backbone.View.extend({
templateName: "widget",
template: function (data) {
return JST[this.templateName](data);
},
templateData: function () {
if (this.model) {
return this.model.toJSON();
}
if (this.collection) {
return this.collection.toJSON();
}
return {};
},
render: function () {
if (_.isFunction(this.beforeRender)) {
this.beforeRender();
}
this.$el.html(this.template(this.templateData()));
if (_.isFunction(this.afterRender)) {
this.afterRender();
}
return this;
}
});
Ghost.View = Ghost.TemplateView.extend({
// Adds a subview to the current view, which will
// ensure its removal when this view is removed,
// or when view.removeSubviews is called
addSubview: function (view) {
if (!(view instanceof Backbone.View)) {
throw new Error("Subview must be a Backbone.View");
}
this.subviews = this.subviews || [];
this.subviews.push(view);
return view;
},
// Removes any subviews associated with this view
// by `addSubview`, which will in-turn remove any
// children of those views, and so on.
removeSubviews: function () {
var children = this.subviews;
if (!children) {
return this;
}
_(children).invoke("remove");
this.subviews = [];
return this;
},
// Extends the view's remove, by calling `removeSubviews`
// if any subviews exist.
remove: function () {
if (this.subviews) {
this.removeSubviews();
}
return Backbone.View.prototype.remove.apply(this, arguments);
}
});
Ghost.Views.Utils = {
// Used in API request fail handlers to parse a standard api error
// response json for the message to display
getRequestErrorMessage: function (request) {
var message,
msgDetail;
// Can't really continue without a request
if (!request) {
return null;
}
// Seems like a sensible default
message = request.statusText;
// If a non 200 response
if (request.status !== 200) {
try {
// Try to parse out the error, or default to "Unknown"
if (request.responseJSON.errors && _.isArray(request.responseJSON.errors)) {
message = '';
_.each(request.responseJSON.errors, function (errorItem) {
message += '<br/>' + errorItem.message;
});
} else {
message = request.responseJSON.error || "Unknown Error";
}
} catch (e) {
msgDetail = request.status ? request.status + " - " + request.statusText : "Server was not available";
message = "The server returned an error (" + msgDetail + ").";
}
}
return message;
},
// Getting URL vars
getUrlVariables: function () {
var vars = [],
hash,
hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'),
i;
for (i = 0; i < hashes.length; i += 1) {
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}
};
/**
* This is the view to generate the markup for the individual
* notification. Will be included into #notifications.
*
* States can be
* - persistent
* - passive
*
* Types can be
* - error
* - success
* - alert
* - (empty)
*
*/
Ghost.Views.Notification = Ghost.View.extend({
templateName: 'notification',
className: 'js-bb-notification',
template: function (data) {
return JST[this.templateName](data);
},
render: function () {
var html = this.template(this.model);
this.$el.html(html);
return this;
}
});
/**
* This handles Notification groups
*/
Ghost.Views.NotificationCollection = Ghost.View.extend({
el: '#notifications',
initialize: function () {
var self = this;
this.render();
Ghost.on('urlchange', function () {
self.clearEverything();
});
shortcut.add("ESC", function () {
// Make sure there isn't currently an open modal, as the escape key should close that first.
// This is a temporary solution to enable closing extra-long notifications, and should be refactored
// into something more robust in future
if ($('.js-modal').length < 1) {
self.clearEverything();
}
});
},
events: {
'animationend .js-notification': 'removeItem',
'webkitAnimationEnd .js-notification': 'removeItem',
'oanimationend .js-notification': 'removeItem',
'MSAnimationEnd .js-notification': 'removeItem',
'click .js-notification.notification-passive .close': 'closePassive',
'click .js-notification.notification-persistent .close': 'closePersistent'
},
render: function () {
_.each(this.model, function (item) {
this.renderItem(item);
}, this);
},
renderItem: function (item) {
var itemView = new Ghost.Views.Notification({ model: item }),
height,
$notification = $(itemView.render().el);
this.$el.append($notification);
height = $notification.hide().outerHeight(true);
$notification.animate({height: height}, 250, function () {
$(this)
.css({height: "auto"})
.fadeIn(250);
});
},
addItem: function (item) {
this.model.push(item);
this.renderItem(item);
},
clearEverything: function () {
this.$el.find('.js-notification.notification-passive').parent().remove();
},
removeItem: function (e) {
e.preventDefault();
var self = e.currentTarget,
bbSelf = this;
if (self.className.indexOf('notification-persistent') !== -1) {
$.ajax({
type: "DELETE",
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
url: Ghost.paths.apiRoot + '/notifications/' + $(self).find('.close').data('id')
}).done(function (result) {
/*jshint unused:false*/
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
$(self).remove();
});
});
} else {
$(self).slideUp(250, function () {
$(this)
.show()
.css({height: "auto"})
.parent()
.remove();
});
}
},
closePassive: function (e) {
$(e.currentTarget)
.parent()
.fadeOut(250)
.slideUp(250, function () {
$(this).remove();
});
},
closePersistent: function (e) {
var self = e.currentTarget,
bbSelf = this;
$.ajax({
type: "DELETE",
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
url: Ghost.paths.apiRoot + '/notifications/' + $(self).data('id')
}).done(function (result) {
/*jshint unused:false*/
var height = bbSelf.$('.js-notification').outerHeight(true),
$parent = $(self).parent();
bbSelf.$el.css({height: height});
if ($parent.parent().hasClass('js-bb-notification')) {
$parent.parent().fadeOut(200, function () {
$(this).remove();
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
});
});
} else {
$parent.fadeOut(200, function () {
$(this).remove();
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
});
});
}
});
}
});
// ## Modals
Ghost.Views.Modal = Ghost.View.extend({
el: '#modal-container',
templateName: 'modal',
className: 'js-bb-modal',
// Render and manages modal dismissal
initialize: function () {
this.render();
var self = this;
if (this.model.options.close) {
shortcut.add("ESC", function () {
self.removeElement();
});
$(document).on('click', '.modal-background', function () {
self.removeElement();
});
} else {
shortcut.remove("ESC");
$(document).off('click', '.modal-background');
}
if (this.model.options.confirm) {
// Initiate functions for buttons here so models don't get tied up.
this.acceptModal = function () {
this.model.options.confirm.accept.func.call(this);
self.removeElement();
};
this.rejectModal = function () {
this.model.options.confirm.reject.func.call(this);
self.removeElement();
};
}
},
templateData: function () {
return this.model;
},
events: {
'click .close': 'removeElement',
'click .js-button-accept': 'acceptModal',
'click .js-button-reject': 'rejectModal'
},
afterRender: function () {
this.$el.fadeIn(50);
$(".modal-background").show(10, function () {
$(this).addClass("in");
});
if (this.model.options.confirm) {
this.$('.close').remove();
}
this.$(".modal-body").html(this.addSubview(new Ghost.Views.Modal.ContentView({model: this.model})).render().el);
// if (document.body.style.webkitFilter !== undefined) { // Detect webkit filters
// $("body").addClass("blur"); // Removed due to poor performance in Chrome
// }
if (_.isFunction(this.model.options.afterRender)) {
this.model.options.afterRender.call(this);
}
if (this.model.options.animation) {
this.animate(this.$el.children(".js-modal"));
}
},
// #### remove
// Removes Backbone attachments from modals
remove: function () {
this.undelegateEvents();
this.$el.empty();
this.stopListening();
return this;
},
// #### removeElement
// Visually removes the modal from the user interface
removeElement: function (e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
var self = this,
$jsModal = $('.js-modal'),
removeModalDelay = $jsModal.transitionDuration(),
removeBackgroundDelay = self.$el.transitionDuration();
$jsModal.removeClass('in');
if (!this.model.options.animation) {
removeModalDelay = removeBackgroundDelay = 0;
}
setTimeout(function () {
if (document.body.style.filter !== undefined) {
$("body").removeClass("blur");
}
$(".modal-background").removeClass('in');
setTimeout(function () {
self.remove();
self.$el.hide();
$(".modal-background").hide();
}, removeBackgroundDelay);
}, removeModalDelay);
},
// #### animate
// Animates the animation
animate: function (target) {
setTimeout(function () {
target.addClass('in');
}, target.transitionDuration());
}
});
// ## Modal Content
Ghost.Views.Modal.ContentView = Ghost.View.extend({
template: function (data) {
return JST['modals/' + this.model.content.template](data);
},
templateData: function () {
return this.model;
}
});
}());

View File

@ -1,284 +0,0 @@
/*global window, Ghost, $, _, Backbone, NProgress */
(function () {
"use strict";
var ContentList,
ContentItem,
PreviewContainer;
// Base view
// ----------
Ghost.Views.Blog = Ghost.View.extend({
initialize: function (options) {
/*jshint unused:false*/
var self = this,
finishProgress = function () {
NProgress.done();
};
// Basic collection request/sync flow progress bar handlers
this.listenTo(this.collection, 'request', function () {
NProgress.start();
});
this.listenTo(this.collection, 'sync', finishProgress);
// A special case because models that are destroyed are removed from the
// collection before the sync event fires and bubbles up
this.listenTo(this.collection, 'destroy', function (model) {
self.listenToOnce(model, 'sync', finishProgress);
});
this.addSubview(new PreviewContainer({ el: '.js-content-preview', collection: this.collection })).render();
this.addSubview(new ContentList({ el: '.js-content-list', collection: this.collection })).render();
}
});
// Content list (sidebar)
// -----------------------
ContentList = Ghost.View.extend({
isLoading: false,
events: {
'click .content-list-content' : 'scrollHandler'
},
initialize: function () {
this.$('.content-list-content').scrollClass({target: '.content-list', offset: 10});
this.listenTo(this.collection, 'remove', this.showNext);
this.listenTo(this.collection, 'add', this.renderPost);
// Can't use backbone event bind (see: http://stackoverflow.com/questions/13480843/backbone-scroll-event-not-firing)
this.$('.content-list-content').scroll($.proxy(this.checkScroll, this));
},
showNext: function () {
if (this.isLoading) { return; }
if (!this.collection.length) {
return Backbone.trigger('blog:activeItem', null);
}
var id = this.collection.at(0) ? this.collection.at(0).id : false;
if (id) {
Backbone.trigger('blog:activeItem', id);
}
},
reportLoadError: function (response) {
var message = 'A problem was encountered while loading more posts';
if (response) {
// Get message from response
message += '; ' + Ghost.Views.Utils.getRequestErrorMessage(response);
} else {
message += '.';
}
Ghost.notifications.addItem({
type: 'error',
message: message,
status: 'passive'
});
},
checkScroll: function (event) {
var self = this,
element = event.target,
triggerPoint = 100;
// If we haven't passed our threshold, exit
if (this.isLoading || (element.scrollTop + element.clientHeight + triggerPoint <= element.scrollHeight)) {
return;
}
// If we've loaded the max number of pages, exit
if (this.collection.currentPage >= this.collection.totalPages) {
return;
}
// Load moar posts!
this.isLoading = true;
this.collection.fetch({
update: true,
remove: false,
data: {
status: 'all',
page: (self.collection.currentPage + 1),
staticPages: 'all'
}
}).then(function onSuccess(response) {
/*jshint unused:false*/
self.render();
self.isLoading = false;
}, function onError(e) {
self.reportLoadError(e);
});
},
renderPost: function (model) {
this.$('ol').append(this.addSubview(new ContentItem({model: model})).render().el);
},
render: function () {
var $list = this.$('ol');
// Clear out any pre-existing subviews.
this.removeSubviews();
this.collection.each(function (model) {
$list.append(this.addSubview(new ContentItem({model: model})).render().el);
}, this);
this.showNext();
}
});
// Content Item
// -----------------------
ContentItem = Ghost.View.extend({
tagName: 'li',
events: {
'click a': 'setActiveItem'
},
active: false,
initialize: function () {
this.listenTo(Backbone, 'blog:activeItem', this.checkActive);
this.listenTo(this.model, 'change:page change:featured', this.render);
this.listenTo(this.model, 'destroy', this.removeItem);
},
removeItem: function () {
var self = this;
$.when(this.$el.slideUp()).then(function () {
self.remove();
});
},
// If the current item isn't active, we trigger the event to
// notify a change in which item we're viewing.
setActiveItem: function (e) {
e.preventDefault();
if (this.active !== true) {
Backbone.trigger('blog:activeItem', this.model.id);
this.render();
}
},
// Checks whether this item is active and doesn't match the current id.
checkActive: function (id) {
if (this.model.id !== id) {
if (this.active) {
this.active = false;
this.$el.removeClass('active');
this.render();
}
} else {
this.active = true;
this.$el.addClass('active');
}
},
showPreview: function (e) {
var item = $(e.currentTarget);
this.$('.content-list-content li').removeClass('active');
item.addClass('active');
Backbone.trigger('blog:activeItem', item.data('id'));
},
templateName: "list-item",
templateData: function () {
return _.extend({active: this.active}, this.model.toJSON());
}
});
// Content preview
// ----------------
PreviewContainer = Ghost.View.extend({
activeId: null,
events: {
'click .post-controls .post-edit' : 'editPost',
'click .featured' : 'toggleFeatured',
'click .unfeatured' : 'toggleFeatured'
},
initialize: function () {
this.listenTo(Backbone, 'blog:activeItem', this.setActivePreview);
},
setActivePreview: function (id) {
if (this.activeId !== id) {
this.activeId = id;
this.render();
}
},
editPost: function (e) {
e.preventDefault();
// for now this will disable "open in new tab", but when we have a Router implemented
// it can go back to being a normal link to '#/ghost/editor/X'
window.location = Ghost.paths.subdir + '/ghost/editor/' + this.model.get('id') + '/';
},
toggleFeatured: function (e) {
e.preventDefault();
var self = this,
featured = !self.model.get('featured'),
featuredEl = $(e.currentTarget),
model = this.collection.get(this.activeId);
model.save({
featured: featured
}, {
success : function () {
featuredEl.removeClass("featured unfeatured").addClass(featured ? "featured" : "unfeatured");
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'success',
message: "Post successfully marked as " + (featured ? "featured" : "not featured") + ".",
status: 'passive'
});
},
error : function (model, xhr) {
/*jshint unused:false*/
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
},
templateName: "preview",
render: function () {
var self = this;
this.model = this.collection.get(this.activeId);
this.$el.html(this.template(this.templateData()));
this.$('.content-preview-content').scrollClass({target: '.content-preview', offset: 10});
this.$('.wrapper').on('click', 'a', function (e) {
$(e.currentTarget).attr('target', '_blank');
});
if (this.model !== undefined) {
this.model.fetch({ data: { status: 'all', include: 'tags,author' } }).then(function () {
self.addSubview(new Ghost.View.PostSettings({ el: $('.post-controls'), model: self.model })).render();
});
}
Ghost.temporary.initToggles(this.$el);
return this;
}
});
}());

View File

@ -1,189 +0,0 @@
/*global Ghost, $ */
(function () {
"use strict";
Ghost.Views.Debug = Ghost.View.extend({
events: {
"click .settings-menu a": "handleMenuClick",
"click #startupload": "handleUploadClick",
"click .js-delete": "handleDeleteClick",
"click #sendtestmail": "handleSendTestMailClick"
},
initialize: function () {
var view = this;
this.uploadButton = this.$el.find('#startupload');
// Disable import button and initizalize BlueImp file upload
this.uploadButton.prop('disabled', 'disabled');
$('#importfile').fileupload({
url: Ghost.paths.apiRoot + '/db/',
limitMultiFileUploads: 1,
replaceFileInput: false,
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
dataType: 'json',
add: function (e, data) {
/*jshint unused:false*/
// Bind the upload data to the view, so it is
// available to the click handler, and enable the
// upload button.
view.fileUploadData = data;
data.context = view.uploadButton.removeProp('disabled');
},
done: function (e, data) {
/*jshint unused:false*/
$('#startupload').text('Import');
if (!data.result) {
throw new Error('No response received from server.');
}
if (!data.result.message) {
throw new Error('Unknown error');
}
Ghost.notifications.addItem({
type: 'success',
message: data.result.message,
status: 'passive'
});
},
error: function (response) {
$('#startupload').text('Import');
var responseJSON = response.responseJSON,
message = responseJSON && responseJSON.errors[0].message ? responseJSON.errors[0].message : 'unknown';
Ghost.notifications.addItem({
type: 'error',
message: ['A problem was encountered while importing new content to your blog. Error: ', message].join(''),
status: 'passive'
});
}
});
},
handleMenuClick: function (ev) {
ev.preventDefault();
var $target = $(ev.currentTarget);
// Hide the current content
this.$(".settings-content").hide();
// Show the clicked content
this.$("#debug-" + $target.attr("class")).show();
return false;
},
handleUploadClick: function (ev) {
ev.preventDefault();
if (!this.uploadButton.prop('disabled')) {
this.fileUploadData.context = this.uploadButton.text('Importing');
this.fileUploadData.submit();
}
// Prevent double post by disabling the button.
this.uploadButton.prop('disabled', 'disabled');
},
handleDeleteClick: function (ev) {
ev.preventDefault();
this.addSubview(new Ghost.Views.Modal({
model: {
options: {
close: true,
confirm: {
accept: {
func: function () {
$.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.errors[0].message ? responseText.errors[0].message : '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"
}
},
type: "action",
style: ["wide", "centered"],
animation: 'fade'
},
content: {
template: 'blank',
title: 'Would you really like to delete all content from your blog?',
text: '<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>'
}
}
}));
},
handleSendTestMailClick: function (ev) {
ev.preventDefault();
$.ajax({
url: Ghost.paths.apiRoot + '/mail/test/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
success: function onSuccess(response) {
Ghost.notifications.addItem({
type: 'success',
message: ['Check your email for the test message: ', response.message].join(''),
status: 'passive'
});
},
error: function onError(response) {
var responseText = JSON.parse(response.responseText),
message = responseText && responseText.errors[0].message ? responseText.errors[0].message : 'unknown';
Ghost.notifications.addItem({
type: 'error',
message: ['A problem was encountered while sending the test email: ', message].join(''),
status: 'passive'
});
}
});
},
});
}());

View File

@ -1,255 +0,0 @@
// The Save / Publish button
/*global $, _, Ghost, shortcut */
(function () {
'use strict';
// The Publish, Queue, Publish Now buttons
// ----------------------------------------
Ghost.View.EditorActionsWidget = Ghost.View.extend({
events: {
'click [data-set-status]': 'handleStatus',
'click .js-publish-button': 'handlePostButton'
},
statusMap: null,
createStatusMap: {
'draft': 'Save Draft',
'published': 'Publish Now'
},
updateStatusMap: {
'draft': 'Unpublish',
'published': 'Update Post'
},
//TODO: This has to be moved to the I18n localization file.
//This structure is supposed to be close to the i18n-localization which will be used soon.
messageMap: {
errors: {
post: {
published: {
'published': 'Your post could not be updated.',
'draft': 'Your post could not be saved as a draft.'
},
draft: {
'published': 'Your post could not be published.',
'draft': 'Your post could not be saved as a draft.'
}
}
},
success: {
post: {
published: {
'published': 'Your post has been updated.',
'draft': 'Your post has been saved as a draft.'
},
draft: {
'published': 'Your post has been published.',
'draft': 'Your post has been saved as a draft.'
}
}
}
},
initialize: function () {
var self = this;
// Toggle publish
shortcut.add('Ctrl+Alt+P', function () {
self.toggleStatus();
});
shortcut.add('Ctrl+S', function () {
self.updatePost();
});
shortcut.add('Meta+S', function () {
self.updatePost();
});
this.listenTo(this.model, 'change:status', this.render);
},
toggleStatus: function () {
var self = this,
keys = Object.keys(this.statusMap),
model = self.model,
prevStatus = model.get('status'),
currentIndex = keys.indexOf(prevStatus),
newIndex,
status;
newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
status = keys[newIndex];
this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
this.savePost({
status: keys[newIndex]
}).then(function () {
self.reportSaveSuccess(status, prevStatus);
}, function (xhr) {
// Show a notification about the error
self.reportSaveError(xhr, model, status, prevStatus);
});
},
setActiveStatus: function (newStatus, displayText, currentStatus) {
var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
// Controls when background of button has the splitbutton-delete/button-delete classes applied
isImportantStatus = (isPublishing || isUnpublishing);
$('.js-publish-splitbutton')
.removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
.addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
// Set the publish button's action and proper coloring
$('.js-publish-button')
.attr('data-status', newStatus)
.text(displayText)
.removeClass(isImportantStatus ? 'button-save' : 'button-delete')
.addClass(isImportantStatus ? 'button-delete' : 'button-save');
// Remove the animated popup arrow
$('.js-publish-splitbutton > a')
.removeClass('active');
// Set the active action in the popup
$('.js-publish-splitbutton .editor-options li')
.removeClass('active')
.filter(['li[data-set-status="', newStatus, '"]'].join(''))
.addClass('active');
},
handleStatus: function (e) {
if (e) { e.preventDefault(); }
var status = $(e.currentTarget).attr('data-set-status'),
currentStatus = this.model.get('status');
this.setActiveStatus(status, this.statusMap[status], currentStatus);
// Dismiss the popup menu
$('body').find('.overlay:visible').fadeOut();
},
handlePostButton: function (e) {
if (e) { e.preventDefault(); }
var status = $(e.currentTarget).attr('data-status');
this.updatePost(status);
},
updatePost: function (status) {
var self = this,
model = this.model,
prevStatus = model.get('status');
// Default to same status if not passed in
status = status || prevStatus;
model.trigger('willSave');
this.savePost({
status: status
}).then(function () {
self.reportSaveSuccess(status, prevStatus);
// Refresh publish button and all relevant controls with updated status.
self.render();
}, function (xhr) {
// Set the model status back to previous
model.set({ status: prevStatus });
// Set appropriate button status
self.setActiveStatus(status, self.statusMap[status], prevStatus);
// Show a notification about the error
self.reportSaveError(xhr, model, status, prevStatus);
});
},
savePost: function (data) {
var publishButton = $('.js-publish-button'),
saved,
enablePublish = function (deferred) {
deferred.always(function () {
publishButton.prop('disabled', false);
});
return deferred;
};
publishButton.prop('disabled', true);
_.each(this.model.blacklist, function (item) {
this.model.unset(item);
}, this);
saved = this.model.save(_.extend({
title: this.options.$title.val(),
markdown: this.options.editor.value()
}, data));
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
// ourselves for more consistent promises.
if (saved) {
return enablePublish(saved);
}
return enablePublish($.Deferred().reject());
},
reportSaveSuccess: function (status, prevStatus) {
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'success',
message: this.messageMap.success.post[prevStatus][status],
status: 'passive'
});
this.options.editor.setDirty(false);
},
reportSaveError: function (response, model, status, prevStatus) {
var message = this.messageMap.errors.post[prevStatus][status];
if (response) {
// Get message from response
message += ' ' + Ghost.Views.Utils.getRequestErrorMessage(response);
} else if (model.validationError) {
// Grab a validation error
message += ' ' + model.validationError;
}
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: message,
status: 'passive'
});
},
setStatusLabels: function (statusMap) {
_.each(statusMap, function (label, status) {
$('li[data-set-status="' + status + '"] > a').text(label);
});
},
render: function () {
var status = this.model.get('status');
// Assume that we're creating a new post
if (status !== 'published') {
this.statusMap = this.createStatusMap;
} else {
this.statusMap = this.updateStatusMap;
}
// Populate the publish menu with the appropriate verbiage
this.setStatusLabels(this.statusMap);
// Default the selected publish option to the current status of the post.
this.setActiveStatus(status, this.statusMap[status], status);
}
});
}());

View File

@ -1,303 +0,0 @@
// The Tag UI area associated with a post
/*global window, document, setTimeout, $, _, Ghost */
(function () {
"use strict";
Ghost.View.EditorTagWidget = Ghost.View.extend({
events: {
'keyup [data-input-behaviour="tag"]': 'handleKeyup',
'keydown [data-input-behaviour="tag"]': 'handleKeydown',
'keypress [data-input-behaviour="tag"]': 'handleKeypress',
'click ul.suggestions li': 'handleSuggestionClick',
'click .tags .tag': 'handleTagClick',
'click .tag-label': 'mobileTags'
},
keys: {
UP: 38,
DOWN: 40,
ESC: 27,
ENTER: 13,
BACKSPACE: 8
},
initialize: function () {
var self = this,
tagCollection = new Ghost.Collections.Tags();
tagCollection.fetch().then(function () {
self.allGhostTags = tagCollection.toJSON();
});
this.listenTo(this.model, 'willSave', this.completeCurrentTag, this);
},
render: function () {
var tags = this.model.get('tags'),
$tags = $('.tags'),
tagOffset,
self = this;
$tags.empty();
if (tags) {
_.forEach(tags, function (tag) {
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + _.escape(tag.name) + '</span>');
$tags.append($tag);
$("[data-tag-id=" + tag.id + "]")[0].scrollIntoView(true);
});
}
this.$suggestions = $("ul.suggestions").hide(); // Initialise suggestions overlay
if ($tags.length) {
tagOffset = $('.tag-input').offset().left;
$('.tag-blocks').css({'left': tagOffset + 'px'});
}
$(window).on('resize', self.resize).trigger('resize');
$('.tag-label').on('touchstart', function () {
$(this).addClass('touch');
});
return this;
},
mobileTags: function () {
var mq = window.matchMedia("(max-width: 400px)"),
publishBar = $("#publish-bar");
if (mq.matches) {
if (publishBar.hasClass("extended-tags")) {
publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
$(this).removeClass("extended-tags");
$(".tag-input").blur();
});
} else {
publishBar.animate({"top": 0, "height": $(window).height()}, 300, "swing", function () {
$(this).addClass("extended-tags");
$(".tag-input").focus();
});
}
$(".tag-input").one("blur", function () {
if (publishBar.hasClass("extended-tags") && !$(':hover').last().hasClass("tag")) {
publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
$(this).removeClass("extended-tags");
$(document.activeElement).blur();
document.documentElement.style.display = "none";
setTimeout(function () { document.documentElement.style.display = 'block'; }, 0);
});
}
});
window.scrollTo(0, 1);
}
},
showSuggestions: function ($target, _searchTerm) {
var searchTerm = _searchTerm.toLowerCase(),
matchingTags = this.findMatchingTags(searchTerm),
styles = {
left: $target.position().left
},
// Limit the suggestions number
maxSuggestions = 5,
// Escape regex special characters
escapedTerm = searchTerm.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'),
regexTerm = escapedTerm.replace(/(\s+)/g, "(<[^>]+>)*$1(<[^>]+>)*"),
regexPattern = new RegExp("(" + regexTerm + ")", "i");
this.$suggestions.css(styles);
this.$suggestions.html("");
matchingTags = _.first(matchingTags, maxSuggestions);
if (matchingTags.length > 0) {
this.$suggestions.show();
}
_.each(matchingTags, function (matchingTag) {
var highlightedName,
suggestionHTML;
highlightedName = matchingTag.name.replace(regexPattern, function (match, p1) {
return "<mark>" + _.escape(p1) + "</mark>";
});
/*jslint regexp: true */ // - would like to remove this
highlightedName = highlightedName.replace(/(<mark>[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, function (match, p1, p2, p3, p4) {
return _.escape(p1) + '</mark>' + _.escape(p2) + '<mark>' + _.escape(p4);
});
suggestionHTML = "<li data-tag-id='" + matchingTag.id + "' data-tag-name='" + _.escape(matchingTag.name) + "'><a href='#'>" + highlightedName + "</a></li>";
this.$suggestions.append(suggestionHTML);
}, this);
},
handleKeyup: function (e) {
var $target = $(e.currentTarget),
searchTerm = $.trim($target.val());
if (e.keyCode === this.keys.UP) {
e.preventDefault();
if (this.$suggestions.is(":visible")) {
if (this.$suggestions.children(".selected").length === 0) {
this.$suggestions.find("li:last-child").addClass('selected');
} else {
this.$suggestions.children(".selected").removeClass('selected').prev().addClass('selected');
}
}
} else if (e.keyCode === this.keys.DOWN) {
e.preventDefault();
if (this.$suggestions.is(":visible")) {
if (this.$suggestions.children(".selected").length === 0) {
this.$suggestions.find("li:first-child").addClass('selected');
} else {
this.$suggestions.children(".selected").removeClass('selected').next().addClass('selected');
}
}
} else if (e.keyCode === this.keys.ESC) {
this.$suggestions.hide();
} else {
if (searchTerm) {
this.showSuggestions($target, searchTerm);
} else {
this.$suggestions.hide();
}
}
if (e.keyCode === this.keys.UP || e.keyCode === this.keys.DOWN) {
return false;
}
},
handleKeydown: function (e) {
var $target = $(e.currentTarget),
lastBlock,
tag;
// Delete character tiggers on Keydown, so needed to check on that event rather than Keyup.
if (e.keyCode === this.keys.BACKSPACE && !$target.val()) {
lastBlock = this.$('.tags').find('.tag').last();
lastBlock.remove();
tag = {id: lastBlock.data('tag-id'), name: lastBlock.text()};
this.model.removeTag(tag);
}
},
handleKeypress: function (e) {
var $target = $(e.currentTarget),
searchTerm = $.trim($target.val()),
tag,
$selectedSuggestion,
isComma = ",".localeCompare(String.fromCharCode(e.keyCode || e.charCode)) === 0,
hasAlreadyBeenAdded;
// use localeCompare in case of international keyboard layout
if ((e.keyCode === this.keys.ENTER || isComma) && searchTerm) {
// Submit tag using enter or comma key
e.preventDefault();
$selectedSuggestion = this.$suggestions.children(".selected");
if (this.$suggestions.is(":visible") && $selectedSuggestion.length !== 0) {
tag = {id: $selectedSuggestion.data('tag-id'), name: _.unescape($selectedSuggestion.data('tag-name'))};
hasAlreadyBeenAdded = this.hasTagBeenAdded(tag.name);
if (!hasAlreadyBeenAdded) {
this.addTag(tag);
}
} else {
if (isComma) {
// Remove comma from string if comma is used to submit.
searchTerm = searchTerm.replace(/,/g, "");
}
hasAlreadyBeenAdded = this.hasTagBeenAdded(searchTerm);
if (!hasAlreadyBeenAdded) {
this.addTag({id: null, name: searchTerm});
}
}
$target.val('').focus();
searchTerm = ""; // Used to reset search term
this.$suggestions.hide();
}
},
completeCurrentTag: function () {
var $target = this.$('.tag-input'),
tagName = $target.val(),
hasAlreadyBeenAdded;
hasAlreadyBeenAdded = this.hasTagBeenAdded(tagName);
if (tagName.length > 0 && !hasAlreadyBeenAdded) {
this.addTag({id: null, name: tagName});
}
},
handleSuggestionClick: function (e) {
var $target = $(e.currentTarget);
if (e) { e.preventDefault(); }
this.addTag({id: $target.data('tag-id'), name: _.unescape($target.data('tag-name'))});
},
handleTagClick: function (e) {
var $tag = $(e.currentTarget),
tag = {id: $tag.data('tag-id'), name: $tag.text()};
$tag.remove();
window.scrollTo(0, 1);
this.model.removeTag(tag);
},
resize: _.throttle(function () {
var $tags = $('.tags');
if ($(window).width() > 400) {
$tags.css("max-width", $("#entry-tags").width() - 320);
} else {
$tags.css("max-width", "inherit");
}
}, 50),
findMatchingTags: function (searchTerm) {
var matchingTagModels,
self = this;
if (!this.allGhostTags) {
return [];
}
searchTerm = searchTerm.toUpperCase();
matchingTagModels = _.filter(this.allGhostTags, function (tag) {
var tagNameMatches,
hasAlreadyBeenAdded;
tagNameMatches = tag.name.toUpperCase().indexOf(searchTerm) !== -1;
hasAlreadyBeenAdded = self.hasTagBeenAdded(tag.name);
return tagNameMatches && !hasAlreadyBeenAdded;
});
return matchingTagModels;
},
addTag: function (tag) {
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + _.escape(tag.name) + '</span>');
this.$('.tags').append($tag);
$(".tag").last()[0].scrollIntoView(true);
window.scrollTo(0, 1);
this.model.addTag(tag);
this.$('.tag-input').val('').focus();
this.$suggestions.hide();
},
hasTagBeenAdded: function (tagName) {
return _.some(this.model.get('tags'), function (usedTag) {
return tagName.toUpperCase() === usedTag.name.toUpperCase();
});
}
});
}());

View File

@ -1,158 +0,0 @@
// # 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; }
});
}());

View File

@ -1,276 +0,0 @@
/*global window, Ghost, $, validator */
(function () {
"use strict";
Ghost.Views.Login = Ghost.View.extend({
initialize: function () {
this.render();
},
templateName: "login",
events: {
'submit #login': 'submitHandler'
},
afterRender: function () {
var self = this;
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
self.$("[name='email']").focus();
});
},
submitHandler: function (event) {
event.preventDefault();
var email = this.$el.find('.email').val(),
password = this.$el.find('.password').val(),
redirect = Ghost.Views.Utils.getUrlVariables().r,
validationErrors = [];
if (!validator.isEmail(email)) {
validationErrors.push("Invalid Email");
}
if (!validator.isLength(password, 0)) {
validationErrors.push("Please enter a password");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/signin/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
email: email,
password: password,
redirect: redirect
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (xhr) {
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}
}
});
Ghost.Views.Signup = Ghost.View.extend({
initialize: function () {
this.submitted = "no";
this.render();
},
templateName: "signup",
events: {
'submit #signup': 'submitHandler'
},
afterRender: function () {
var self = this;
this.$el
.css({"opacity": 0})
.animate({"opacity": 1}, 500, function () {
self.$("[name='name']").focus();
});
},
submitHandler: function (event) {
event.preventDefault();
var name = this.$('.name').val(),
email = this.$('.email').val(),
password = this.$('.password').val(),
validationErrors = [],
self = this;
if (!validator.isLength(name, 1)) {
validationErrors.push("Please enter a name.");
}
if (!validator.isEmail(email)) {
validationErrors.push("Please enter a correct email address.");
}
if (!validator.isLength(password, 0)) {
validationErrors.push("Please enter a password");
}
if (!validator.equals(this.submitted, "no")) {
validationErrors.push("Ghost is signing you up. Please wait...");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.submitted = "yes";
$.ajax({
url: Ghost.paths.subdir + '/ghost/signup/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
name: name,
email: email,
password: password
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (xhr) {
self.submitted = "no";
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}
}
});
Ghost.Views.Forgotten = Ghost.View.extend({
initialize: function () {
this.render();
},
templateName: "forgotten",
events: {
'submit #forgotten': 'submitHandler'
},
afterRender: function () {
var self = this;
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
self.$("[name='email']").focus();
});
},
submitHandler: function (event) {
event.preventDefault();
var email = this.$el.find('.email').val(),
validationErrors = [];
if (!validator.isEmail(email)) {
validationErrors.push("Please enter a correct email address.");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/forgotten/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
email: email
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (xhr) {
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}
}
});
Ghost.Views.ResetPassword = Ghost.View.extend({
templateName: 'reset',
events: {
'submit #reset': 'submitHandler'
},
initialize: function (attrs) {
attrs = attrs || {};
this.token = attrs.token;
this.render();
},
afterRender: function () {
var self = this;
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
self.$("[name='newpassword']").focus();
});
},
submitHandler: function (ev) {
ev.preventDefault();
var self = this,
newPassword = this.$('input[name="newpassword"]').val(),
ne2Password = this.$('input[name="ne2password"]').val();
if (newPassword !== ne2Password) {
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: "Your passwords do not match.",
status: 'passive'
});
return;
}
this.$('input, button').prop('disabled', true);
$.ajax({
url: Ghost.paths.subdir + '/ghost/reset/' + this.token + '/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
newpassword: newPassword,
ne2password: ne2Password
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (xhr) {
self.$('input, button').prop('disabled', false);
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
return false;
}
});
}());

View File

@ -1,359 +0,0 @@
// The Post Settings Menu available in the content preview screen, as well as the post editor.
/*global window, $, _, Ghost, moment */
(function () {
"use strict";
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';
Ghost.View.PostSettings = Ghost.View.extend({
events: {
'blur .post-setting-slug' : 'editSlug',
'click .post-setting-slug' : 'selectSlug',
'blur .post-setting-date' : 'editDate',
'click .post-setting-static-page' : 'toggleStaticPage',
'click .delete' : 'deletePost'
},
initialize: function () {
if (this.model) {
// These three items can be updated outside of the post settings menu, so have to be listened to.
this.listenTo(this.model, 'change:id', this.render);
this.listenTo(this.model, 'change:title', this.updateSlugPlaceholder);
this.listenTo(this.model, 'change:published_at', this.updatePublishedDate);
}
},
render: function () {
var slug = this.model ? this.model.get('slug') : '',
pubDate = this.model ? this.model.get('published_at') : 'Not Published',
$pubDateEl = this.$('.post-setting-date'),
$postSettingSlugEl = this.$('.post-setting-slug');
$postSettingSlugEl.val(slug);
// Update page status test if already a page.
if (this.model && this.model.get('page')) {
$('.post-setting-static-page').prop('checked', this.model.get('page'));
}
// Insert the published date, and make it editable if it exists.
if (this.model && this.model.get('published_at')) {
pubDate = moment(pubDate).format(displayDateFormat);
$pubDateEl.attr('placeholder', '');
} else {
$pubDateEl.attr('placeholder', moment().format(displayDateFormat));
}
if (this.model && this.model.get('id')) {
this.$('.post-setting-page').removeClass('hidden');
this.$('.delete').removeClass('hidden');
}
// Apply different style for model's that aren't
// yet persisted to the server.
// Mostly we're hiding the delete post UI
if (this.model.id === undefined) {
this.$el.addClass('unsaved');
} else {
this.$el.removeClass('unsaved');
}
$pubDateEl.val(pubDate);
},
// Requests a new slug when the title was changed
updateSlugPlaceholder: function () {
var title = this.model.get('title'),
$postSettingSlugEl = this.$('.post-setting-slug');
// 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) {
$.ajax({
url: Ghost.paths.apiRoot + '/slugs/post/' + encodeURIComponent(title) + '/',
success: function (result) {
$postSettingSlugEl.attr('placeholder', result);
}
});
} 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).
$postSettingSlugEl.attr('placeholder', '');
return;
}
},
selectSlug: function (e) {
e.currentTarget.select();
},
editSlug: _.debounce(function (e) {
e.preventDefault();
var self = this,
slug = self.model.get('slug'),
slugEl = e.currentTarget,
newSlug = slugEl.value,
placeholder = slugEl.placeholder;
newSlug = (_.isEmpty(newSlug) && placeholder) ? placeholder : newSlug;
// If the model doesn't currently
// exist on the server (aka has no id)
// then just update the model's value
if (self.model.id === undefined) {
this.model.set({
slug: newSlug
});
return;
}
// Ignore unchanged slugs
if (slug === newSlug) {
slugEl.value = slug === undefined ? '' : slug;
return;
}
this.model.save({
slug: newSlug
}, {
success : function (model, response, options) {
/*jshint unused:false*/
// Repopulate slug in case it changed on the server (e.g. 'new-slug-2')
slugEl.value = model.get('slug');
Ghost.notifications.addItem({
type: 'success',
message: "Permalink successfully changed to <strong>" + model.get('slug') + '</strong>.',
status: 'passive'
});
},
error : function (model, xhr) {
/*jshint unused:false*/
slugEl.value = model.previous('slug');
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}, 500),
updatePublishedDate: function () {
var pubDate = this.model.get('published_at') ? moment(this.model.get('published_at'))
.format(displayDateFormat) : '',
$pubDateEl = this.$('.post-setting-date');
// Only change the date if it's different
if (pubDate && $pubDateEl.val() !== pubDate) {
$pubDateEl.val(pubDate);
}
},
editDate: _.debounce(function (e) {
e.preventDefault();
var self = this,
errMessage = '',
pubDate = self.model.get('published_at') ? moment(self.model.get('published_at'))
.format(displayDateFormat) : '',
pubDateEl = e.currentTarget,
newPubDate = pubDateEl.value,
pubDateMoment,
newPubDateMoment;
// if there is no new pub date do nothing
if (!newPubDate) {
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 = moment(newPubDate, parseDateFormats);
// 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 = moment(pubDate, parseDateFormats);
// Ensure the published date has changed
if (newPubDate.length === 0 || pubDateMoment.isSame(newPubDateMoment)) {
// If it wasn't, reset it and return
pubDateEl.value = pubDateMoment.format(displayDateFormat);
return;
}
}
// Validate new Published date
if (!newPubDateMoment.isValid()) {
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.length) {
// Show error message
Ghost.notifications.addItem({
type: 'error',
message: errMessage,
status: 'passive'
});
// Reset back to original value and return
pubDateEl.value = pubDateMoment ? pubDateMoment.format(displayDateFormat) : '';
return;
}
// If the model doesn't currently
// exist on the server (aka has no id)
// then just update the model's value
if (self.model.id === undefined) {
this.model.set({
published_at: newPubDateMoment.toDate()
});
return;
}
// Save new 'Published' date
this.model.save({
published_at: newPubDateMoment.toDate()
}, {
success : function (model) {
pubDateEl.value = moment(model.get('published_at')).format(displayDateFormat);
Ghost.notifications.addItem({
type: 'success',
message: 'Publish date successfully changed to <strong>' + pubDateEl.value + '</strong>.',
status: 'passive'
});
},
error : function (model, xhr) {
/*jshint unused:false*/
// Reset back to original value
pubDateEl.value = pubDateMoment ? pubDateMoment.format(displayDateFormat) : '';
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}, 500),
toggleStaticPage: _.debounce(function (e) {
var pageEl = $(e.currentTarget),
page = pageEl.prop('checked');
// Don't try to save
// if the model doesn't currently
// exist on the server
if (this.model.id === undefined) {
this.model.set({
page: page
});
return;
}
this.model.save({
page: page
}, {
success : function (model, response, options) {
/*jshint unused:false*/
pageEl.prop('checked', page);
Ghost.notifications.addItem({
type: 'success',
message: "Successfully converted " + (page ? "to static page" : "to post") + '.',
status: 'passive'
});
},
error : function (model, xhr) {
/*jshint unused:false*/
pageEl.prop('checked', model.previous('page'));
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}, 500),
deletePost: function (e) {
e.preventDefault();
var self = this;
// You can't delete a post
// that hasn't yet been saved
if (this.model.id === undefined) {
return;
}
this.addSubview(new Ghost.Views.Modal({
model: {
options: {
close: false,
confirm: {
accept: {
func: function () {
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"
}
},
type: "action",
style: ["wide", "centered"],
animation: 'fade'
},
content: {
template: 'blank',
title: 'Are you sure you want to delete this post?',
text: '<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>'
}
}
}));
}
});
}());

View File

@ -1,521 +0,0 @@
/*global document, Ghost, $, _, Countable, validator */
(function () {
"use strict";
var Settings = {};
// Base view
// ----------
Ghost.Views.Settings = Ghost.View.extend({
initialize: function (options) {
$(".settings-content").removeClass('active');
this.sidebar = new Settings.Sidebar({
el: '.settings-sidebar',
pane: options.pane,
model: this.model
});
this.addSubview(this.sidebar);
this.listenTo(Ghost.router, 'route:settings', this.changePane);
},
changePane: function (pane) {
if (!pane) {
// Can happen when trying to load /settings with no pane specified
// let the router navigate itself to /settings/general
return;
}
this.sidebar.showContent(pane);
}
});
// Sidebar (tabs)
// ---------------
Settings.Sidebar = Ghost.View.extend({
initialize: function (options) {
this.render();
this.menu = this.$('.settings-menu');
// Hides apps UI unless config.js says otherwise
// This will stay until apps UI is ready to ship
if ($(this.el).attr('data-apps') !== "true") {
this.menu.find('.apps').hide();
}
this.showContent(options.pane);
},
models: {},
events: {
'click .settings-menu li' : 'switchPane'
},
switchPane: function (e) {
e.preventDefault();
var item = $(e.currentTarget),
id = item.find('a').attr('href').substring(1);
this.showContent(id);
},
showContent: function (id) {
var self = this,
model;
Ghost.router.navigate('/settings/' + id + '/');
Ghost.trigger('urlchange');
if (this.pane && id === this.pane.id) {
return;
}
_.result(this.pane, 'destroy');
this.setActive(id);
this.pane = new Settings[id]({ el: '.settings-content'});
if (!this.models.hasOwnProperty(this.pane.options.modelType)) {
model = this.models[this.pane.options.modelType] = new Ghost.Models[this.pane.options.modelType]();
model.fetch().then(function () {
self.renderPane(model);
});
} else {
model = this.models[this.pane.options.modelType];
self.renderPane(model);
}
},
renderPane: function (model) {
this.pane.model = model;
this.pane.render();
},
setActive: function (id) {
this.menu.find('li').removeClass('active');
this.menu.find('a[href=#' + id + ']').parent().addClass('active');
},
templateName: 'settings/sidebar'
});
// Content panes
// --------------
Settings.Pane = Ghost.View.extend({
options: {
modelType: 'Settings'
},
destroy: function () {
this.$el.removeClass('active');
this.undelegateEvents();
},
render: function () {
this.$el.hide();
Ghost.View.prototype.render.call(this);
this.$el.fadeIn(300);
},
afterRender: function () {
this.$el.attr('id', this.id);
this.$el.addClass('active');
},
saveSuccess: function (model, response, options) {
/*jshint unused:false*/
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'success',
message: 'Saved',
status: 'passive'
});
},
saveError: function (model, xhr) {
/*jshint unused:false*/
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
},
validationError: function (message) {
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',
message: message,
status: 'passive'
});
}
});
// ### General settings
Settings.general = Settings.Pane.extend({
id: "general",
events: {
'click .button-save': 'saveSettings',
'click .js-modal-logo': 'showLogo',
'click .js-modal-cover': 'showCover'
},
saveSettings: function () {
var self = this,
title = this.$('#blog-title').val(),
description = this.$('#blog-description').val(),
email = this.$('#email-address').val(),
postsPerPage = this.$('#postsPerPage').val(),
permalinks = this.$('#permalinks').is(':checked') ? '/:year/:month/:day/:slug/' : '/:slug/',
validationErrors = [];
if (!validator.isLength(title, 0, 150)) {
validationErrors.push({message: "Title is too long", el: $('#blog-title')});
}
if (!validator.isLength(description, 0, 200)) {
validationErrors.push({message: "Description is too long", el: $('#blog-description')});
}
if (!validator.isEmail(email) || !validator.isLength(email, 0, 254)) {
validationErrors.push({message: "Please supply a valid email address", el: $('#email-address')});
}
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')});
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.model.save({
title: title,
description: description,
email: email,
postsPerPage: postsPerPage,
activeTheme: this.$('#activeTheme').val(),
permalinks: permalinks
}, {
success: this.saveSuccess,
error: this.saveError
}).then(function () { self.render(); });
}
},
showLogo: function (e) {
e.preventDefault();
var settings = this.model.toJSON();
this.showUpload('logo', settings.logo);
},
showCover: function (e) {
e.preventDefault();
var settings = this.model.toJSON();
this.showUpload('cover', settings.cover);
},
showUpload: function (key, src) {
var self = this,
upload = new Ghost.Models.uploadModal({'key': key, 'src': src, 'id': this.id, 'accept': {
func: function () { // The function called on acceptance
var data = {};
if (this.$('.js-upload-url').val()) {
data[key] = this.$('.js-upload-url').val();
} else {
data[key] = this.$('.js-upload-target').attr('src');
}
self.model.set(data);
self.saveSettings();
return true;
},
buttonClass: "button-save right",
text: "Save" // The accept button text
}});
this.addSubview(new Ghost.Views.Modal({
model: upload
}));
},
templateName: 'settings/general',
afterRender: function () {
var self = this;
this.$('#permalinks').prop('checked', this.model.get('permalinks') !== '/:slug/');
this.$('.js-drop-zone').upload();
Countable.live(document.getElementById('blog-description'), function (counter) {
var descriptionContainer = self.$('.description-container .word-count');
if (counter.all > 180) {
descriptionContainer.css({color: "#e25440"});
} else {
descriptionContainer.css({color: "#9E9D95"});
}
descriptionContainer.text(200 - counter.all);
});
Settings.Pane.prototype.afterRender.call(this);
}
});
// ### User profile
Settings.user = Settings.Pane.extend({
templateName: 'settings/user-profile',
id: 'user',
options: {
modelType: 'User'
},
events: {
'click .button-save': 'saveUser',
'click .button-change-password': 'changePassword',
'click .js-modal-cover': 'showCover',
'click .js-modal-image': 'showImage',
'keyup .user-profile': 'handleEnterKeyOnForm'
},
showCover: function (e) {
e.preventDefault();
var user = this.model.toJSON();
this.showUpload('cover', user.cover);
},
showImage: function (e) {
e.preventDefault();
var user = this.model.toJSON();
this.showUpload('image', user.image);
},
showUpload: function (key, src) {
var self = this, upload = new Ghost.Models.uploadModal({'key': key, 'src': src, 'id': this.id, 'accept': {
func: function () { // The function called on acceptance
var data = {};
if (this.$('.js-upload-url').val()) {
data[key] = this.$('.js-upload-url').val();
} else {
data[key] = this.$('.js-upload-target').attr('src');
}
self.model.set(data);
self.saveUser(data);
return true;
},
buttonClass: "button-save right",
text: "Save" // The accept button text
}});
this.addSubview(new Ghost.Views.Modal({
model: upload
}));
},
handleEnterKeyOnForm: function (ev) {
// Don't worry about it unless it's an enter key
if (ev.which !== 13) {
return;
}
var $target = $(ev.target);
if ($target.is("textarea")) {
// Allow enter key on user bio text area.
return;
}
if ($target.is('input[type=password]')) {
// Change password if on a password input
return this.changePassword(ev);
}
// Simulate clicking save otherwise
ev.preventDefault();
this.saveUser(ev);
return false;
},
saveUser: function () {
var self = this,
userName = this.$('#user-name').val(),
userEmail = this.$('#user-email').val(),
userLocation = this.$('#user-location').val(),
userWebsite = this.$('#user-website').val(),
userBio = this.$('#user-bio').val(),
validationErrors = [];
if (!validator.isLength(userName, 0, 150)) {
validationErrors.push({message: "Name is too long", el: $('#user-name')});
}
if (!validator.isLength(userBio, 0, 200)) {
validationErrors.push({message: "Bio is too long", el: $('#user-bio')});
}
if (!validator.isEmail(userEmail)) {
validationErrors.push({message: "Please supply a valid email address", el: $('#user-email')});
}
if (!validator.isLength(userLocation, 0, 150)) {
validationErrors.push({message: "Location is too long", el: $('#user-location')});
}
if (userWebsite.length) {
if (!validator.isURL(userWebsite) || !validator.isLength(userWebsite, 0, 2000)) {
validationErrors.push({message: "Please use a valid url", el: $('#user-website')});
}
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.model.save({
'name': userName,
'email': userEmail,
'location': userLocation,
'website': userWebsite,
'bio': userBio
}, {
success: this.saveSuccess,
error: this.saveError
}).then(function () {
self.render();
});
}
},
changePassword: function (event) {
event.preventDefault();
var self = this,
oldPassword = this.$('#user-password-old').val(),
newPassword = this.$('#user-password-new').val(),
ne2Password = this.$('#user-new-password-verification').val(),
validationErrors = [];
if (!validator.equals(newPassword, ne2Password)) {
validationErrors.push("Your new passwords do not match");
}
if (!validator.isLength(newPassword, 8)) {
validationErrors.push("Your password is not long enough. It must be at least 8 characters long.");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/api/v0.1/users/password/',
type: 'PUT',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {password: [{
oldPassword: oldPassword,
newPassword: newPassword,
ne2Password: ne2Password
}]},
success: function (msg) {
Ghost.notifications.addItem({
type: 'success',
message: msg.password[0].message,
status: 'passive',
id: 'success-98'
});
self.$('#user-password-old, #user-password-new, #user-new-password-verification').val('');
},
error: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
}).then(function () {
self.render();
});
}
},
afterRender: function () {
var self = this;
Countable.live(document.getElementById('user-bio'), function (counter) {
var bioContainer = self.$('.bio-container .word-count');
if (counter.all > 180) {
bioContainer.css({color: "#e25440"});
} else {
bioContainer.css({color: "#9E9D95"});
}
bioContainer.text(200 - counter.all);
});
Settings.Pane.prototype.afterRender.call(this);
}
});
// ### Apps page
Settings.apps = Settings.Pane.extend({
id: "apps",
events: {
'click .js-button-activate': 'activateApp',
'click .js-button-deactivate': 'deactivateApp'
},
beforeRender: function () {
this.availableApps = this.model.toJSON().availableApps;
},
activateApp: function (event) {
var button = $(event.currentTarget);
button.removeClass('button-add').addClass('button js-button-active').text('Working');
this.saveStates();
},
deactivateApp: function (event) {
var button = $(event.currentTarget);
button.removeClass('button-delete js-button-active').addClass('button').text('Working');
this.saveStates();
},
saveStates: function () {
var activeButtons = this.$el.find('.js-apps .js-button-active'),
toSave = [],
self = this;
_.each(activeButtons, function (app) {
toSave.push($(app).data('app'));
});
this.model.save({
activeApps: JSON.stringify(toSave)
}, {
success: this.saveSuccess,
error: this.saveError
}).then(function () { self.render(); });
},
saveSuccess: function () {
Ghost.notifications.addItem({
type: 'success',
message: 'Active applications updated.',
status: 'passive',
id: 'success-1100'
});
},
saveError: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
},
templateName: 'settings/apps'
});
}());

View File

@ -38,7 +38,7 @@ authentication = {
return dataProvider.User.generateResetToken(email, expires, dbHash).then(function (resetToken) {
var baseUrl = config().forceAdminSSL ? (config().urlSSL || config().url) : config().url,
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/ember/reset/' + resetToken + '/',
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/',
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
payload = {
mail: [{

View File

@ -6,65 +6,25 @@ var config = require('../config'),
errors = require('../errors'),
storage = require('../storage'),
updateCheck = require('../update-check'),
adminNavbar,
adminControllers,
loginSecurity = [];
adminNavbar = {
content: {
name: 'Content',
navClass: 'content',
key: 'admin.navbar.content',
path: '/'
},
add: {
name: 'New Post',
navClass: 'editor',
key: 'admin.navbar.editor',
path: '/editor/'
},
settings: {
name: 'Settings',
navClass: 'settings',
key: 'admin.navbar.settings',
path: '/settings/'
}
};
// TODO: make this a util or helper
function setSelected(list, name) {
_.each(list, function (item, key) {
item.selected = key === name;
});
return list;
}
adminControllers;
adminControllers = {
// Route: index
// Path: /ghost/
// Method: GET
'index': function (req, res) {
/*jslint unparam:true*/
var userData,
// config we need on the frontend
// config we need on the frontend
frontConfig = {
apps: config().apps,
fileStorage: config().fileStorage
};
res.render('default-ember', {
user: userData,
config: JSON.stringify(frontConfig)
});
},
// Route: index
// Path: /ghost/
// Method: GET
'indexold': function (req, res) {
/*jslint unparam:true*/
function renderIndex() {
res.render('content', {
bodyClass: 'manage',
adminNav: setSelected(adminNavbar, 'content')
res.render('default', {
user: userData,
config: JSON.stringify(frontConfig)
});
}
@ -74,90 +34,6 @@ adminControllers = {
// an error here should just get logged
).otherwise(errors.logError);
},
'content': function (req, res) {
/*jslint unparam:true*/
res.render('content', {
bodyClass: 'manage',
adminNav: setSelected(adminNavbar, 'content')
});
},
// Route: editor
// Path: /ghost/editor(/:id)?(/:action)?/
// Method: GET
'editor': function (req, res) {
if (req.params.id && req.params.action) {
if (req.params.action !== 'view') {
return errors.error404(req, res);
}
api.posts.read({ id: req.params.id }).then(function (result) {
return config.urlForPost(api.settings, result.posts[0]).then(function (url) {
return res.redirect(url);
});
}, function () {
return errors.error404(req, res);
});
} else if (req.params.id !== undefined) {
res.render('editor', {
bodyClass: 'editor',
adminNav: setSelected(adminNavbar, 'content')
});
} else {
res.render('editor', {
bodyClass: 'editor',
adminNav: setSelected(adminNavbar, 'add')
});
}
},
// Route: settings
// path: /ghost/settings/(*/)?
// Method: GET
'settings': function (req, res, next) {
// TODO: Centralise list/enumeration of settings panes, so we don't run into trouble in future.
var allowedSections = ['', 'general', 'user', 'apps'],
section = req.url.replace(/(^\/ghost\/settings[\/]*|\/$)/ig, '');
if (allowedSections.indexOf(section) < 0) {
return next();
}
res.render('settings', {
bodyClass: 'settings',
adminNav: setSelected(adminNavbar, 'settings')
});
},
// Route: debug
// path: /ghost/debug/
// Method: GET
'debug': {
index: function (req, res) {
/*jslint unparam:true*/
res.render('debug', {
bodyClass: 'settings',
adminNav: setSelected(adminNavbar, 'settings')
});
},
// frontend route for downloading a file
exportContent: function (req, res) {
api.db.exportContent({context: {user: req.user.id}}).then(function (exportData) {
// send a file to the client
res.set('Content-Disposition', 'attachment; filename="GhostData.json"');
res.json(exportData);
}).otherwise(function (err) {
var notification = {
type: 'error',
message: 'Your export file could not be generated. Error: ' + err.message
};
errors.logError(err, 'admin.js', "Your export file could not be generated.");
return api.notifications.add({ notifications: [notification]}).then(function () {
res.redirect(config().paths.subdir + '/ghost/debug');
});
});
}
},
// Route: upload
// Path: /ghost/upload/
// Method: POST
@ -181,122 +57,9 @@ adminControllers = {
return res.send(500, e.message);
});
},
// Route: signout
// Path: /ghost/signout/
// Method: GET
'signout': function (req, res) {
var notification = {
type: 'success',
message: 'You were successfully signed out',
status: 'passive'
};
return api.notifications.add({ notifications: [notification] }).then(function () {
res.redirect(config().paths.subdir + '/ghost/signin/');
});
},
// Route: doSignout
// Path: /ghost/signout/
// Method: POST
'doSignout': function (req, res) {
req.session.destroy();
var statusCode,
redirectUrl,
errorMessage,
notification = {
type: 'success',
message: 'You were successfully signed out.',
status: 'passive'
};
if (_.isUndefined(req.session)) {
statusCode = 200;
redirectUrl = config().paths.subdir + '/ghost/signin/';
} else {
notification.type = 'error';
notification.message = 'Unable to sign out.';
statusCode = 500;
errorMessage = 'There was a problem logging out. Please try again.';
}
return api.notifications.add({ notifications: [notification] }).then(function () {
res.json(statusCode, {error: errorMessage, redirect: redirectUrl});
});
},
// Route: signin
// Path: /ghost/signin/
// Method: GET
'signin': function (req, res) {
/*jslint unparam:true*/
res.render('login', {
bodyClass: 'ghost-login',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'login')
});
},
// Route: doSignin
// Path: /ghost/signin/
// Method: POST
'doSignin': function (req, res) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
denied = '';
loginSecurity = _.filter(loginSecurity, function (ipTime) {
return (ipTime.time + 2 > currentTime);
});
denied = _.find(loginSecurity, function (ipTime) {
return (ipTime.ip === remoteAddress);
});
if (!denied) {
loginSecurity.push({ip: remoteAddress, time: currentTime});
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
// Carry over the csrf secret
var existingSecret = req.session.csrfSecret;
req.session.regenerate(function (err) {
if (!err) {
req.session.csrfSecret = existingSecret;
req.session.user = user.id;
req.session.userData = user.attributes;
var redirect = config().paths.subdir + '/ghost/';
if (req.body.redirect) {
redirect += decodeURIComponent(req.body.redirect);
}
// If this IP address successfully logs in we
// can remove it from the array of failed login attempts.
loginSecurity = _.reject(loginSecurity, function (ipTime) {
return ipTime.ip === remoteAddress;
});
res.json(200, {redirect: redirect, userData: req.session.userData});
}
});
}, function (error) {
res.json(401, {error: error.message});
});
} else {
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
}
},
// Route: signup
// Path: /ghost/signup/
// Method: GET
'signup': function (req, res) {
/*jslint unparam:true*/
res.render('signup', {
bodyClass: 'ghost-signup',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'login')
});
},
// Route: doSignup
// Path: /ghost/signup/
// Path: /ghost/setup/
// Method: POST
'doSignup': function (req, res) {
var name = req.body.name,
@ -308,7 +71,7 @@ adminControllers = {
email: email,
password: password
}];
api.users.register({users: users}).then(function () {
var settings = [];
@ -356,117 +119,6 @@ adminControllers = {
}).otherwise(function (error) {
res.json(401, {error: error.message});
});
},
// Route: forgotten
// Path: /ghost/forgotten/
// Method: GET
'forgotten': function (req, res) {
/*jslint unparam:true*/
res.render('forgotten', {
bodyClass: 'ghost-forgotten',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'login')
});
},
// TODO: remove when old admin is removed, functionality lives now in api/authentication
// Route: doForgotten
// Path: /ghost/forgotten/
// Method: POST
'doForgotten': function (req, res) {
var email = req.body.email;
api.users.generateResetToken(email).then(function (token) {
var baseUrl = config().forceAdminSSL ? (config().urlSSL || config().url) : config().url,
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + token + '/',
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
payload = {
mail: [{
message: {
to: email,
subject: 'Reset Password',
html: '<p><strong>Hello!</strong></p>' +
'<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
'<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
'<p>Ghost</p>'
},
options: {}
}]
};
return api.mail.send(payload);
}).then(function success() {
// TODO: note that this function takes a response as an
// argument but jshint complains of it not being used
var notification = {
type: 'success',
message: 'Check your email for further instructions',
status: 'passive'
};
return api.notifications.add({ notifications: [notification] }).then(function () {
res.json(200, {redirect: config().paths.subdir + '/ghost/signin/'});
});
}, function failure(error) {
// TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
if (error && error.message === 'EmptyResponse') {
error.message = "Invalid email address";
}
res.json(401, {error: error.message});
});
},
// Route: reset
// Path: /ghost/reset/:token
// Method: GET
'reset': function (req, res) {
// Validate the request token
var token = req.params.token;
api.users.validateToken(token).then(function () {
// Render the reset form
res.render('reset', {
bodyClass: 'ghost-reset',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'reset')
});
}).otherwise(function (err) {
// Redirect to forgotten if invalid token
var notification = {
type: 'error',
message: 'Invalid or expired token'
};
errors.logError(err, 'admin.js', "Please check the provided token for validity and expiration.");
return api.notifications.add({ notifications: [notification] }).then(function () {
res.redirect(config().paths.subdir + '/ghost/forgotten');
});
});
},
// TODO: remove when old admin is removed, functionality lives now in api/authentication
// Route: doReset
// Path: /ghost/reset/:token
// Method: POST
'doReset': function (req, res) {
var token = req.params.token,
newPassword = req.param('newpassword'),
ne2Password = req.param('ne2password');
api.users.resetPassword(token, newPassword, ne2Password).then(function () {
var notification = {
type: 'success',
message: 'Password changed successfully.',
status: 'passive'
};
return api.notifications.add({ notifications: [notification] }).then(function () {
res.json(200, {redirect: config().paths.subdir + '/ghost/signin/'});
});
}).otherwise(function (err) {
res.json(401, {error: err.message});
});
}
};

View File

@ -163,21 +163,17 @@ coreHelpers.url = function (options) {
// *Usage example:*
// `{{asset "css/screen.css"}}`
// `{{asset "css/screen.css" ghost="true"}}`
// `{{asset "css/screen.css" ember="true"}}`
// Returns the path to the specified asset. The ghost
// flag outputs the asset path for the Ghost admin
coreHelpers.asset = function (context, options) {
var output = '',
isAdmin = options && options.hash && options.hash.ghost,
isEmberAdmin = options && options.hash && options.hash.ember;
isAdmin = options && options.hash && options.hash.ghost;
output += config().paths.subdir + '/';
if (!context.match(/^favicon\.ico$/) && !context.match(/^shared/) && !context.match(/^asset/)) {
if (isAdmin) {
output += 'ghost/';
} else if (isEmberAdmin) {
output += 'ghost/ember/';
} else {
output += 'assets/';
}
@ -350,19 +346,6 @@ coreHelpers.apps = function (context, options) {
};
coreHelpers.ghost_script_tags = function () {
var scriptList = isProduction ? scriptFiles.production : scriptFiles.development;
scriptList = _.map(scriptList, function (fileName) {
return scriptTemplate({
source: config().paths.subdir + '/ghost/scripts/' + fileName,
version: coreHelpers.assetHash
});
});
return scriptList.join('');
};
coreHelpers.ember_script_tags = function () {
var scriptList = scriptFiles.ember;
scriptList = _.map(scriptList, function (fileName) {
@ -825,18 +808,12 @@ registerHelpers = function (adminHbs, assetHash) {
// Register admin helpers
registerAdminHelper('asset', coreHelpers.asset);
registerAdminHelper('ghost_script_tags', coreHelpers.ghost_script_tags);
registerAdminHelper('ember_script_tags', coreHelpers.ember_script_tags);
registerAdminHelper('file_storage', coreHelpers.file_storage);
registerAdminHelper('apps', coreHelpers.apps);
registerAdminHelper('admin_url', coreHelpers.admin_url);
registerAdminHelper('asset', coreHelpers.asset);
// TODO: Make sure this works #3160
// we probably don't need this code for it, but it needs to work still
registerAsyncAdminHelper('update_notification', coreHelpers.update_notification);
};

View File

@ -77,8 +77,7 @@ function builtFilesExist() {
var deferreds = [],
location = config().paths.builtScriptPath,
fileNames = process.env.NODE_ENV === 'production' ?
helpers.scriptFiles.production : helpers.scriptFiles.development;
fileNames = helpers.scriptFiles.ember;
function checkExist(fileName) {
var deferred = when.defer(),
@ -256,7 +255,7 @@ function init(server) {
server.set('view engine', 'hbs');
// Create a hbs instance for admin and init view engine
server.set('admin view engine', adminHbs.express3({partialsDir: config().paths.adminViews + 'partials'}));
server.set('admin view engine', adminHbs.express3({}));
// Load helpers
helpers.loadCoreHelpers(adminHbs, assetHash);

View File

@ -136,28 +136,13 @@ function updateActiveTheme(req, res, next) {
});
}
// Redirect to signup if no user exists
// TODO Remove this when
function redirectToSignup(req, res, next) {
/*jslint unparam:true*/
api.users.doesUserExist().then(function (exists) {
if (!exists) {
return res.redirect(config().paths.subdir + '/ghost/signup/');
}
next();
}).otherwise(function (err) {
return next(new Error(err));
});
}
// Redirect to setup if no user exists
function redirectToSetup(req, res, next) {
/*jslint unparam:true*/
api.users.doesUserExist().then(function (exists) {
if (!exists && !req.path.match(/\/ghost\/ember\/setup\//)) {
return res.redirect(config().paths.subdir + '/ghost/ember/setup/');
if (!exists && !req.path.match(/\/ghost\/setup\//)) {
return res.redirect(config().paths.subdir + '/ghost/setup/');
}
next();
}).otherwise(function (err) {
@ -278,8 +263,7 @@ module.exports = function (server) {
expressServer.use(decideContext);
// Admin only config
expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/clientold/assets'), {maxAge: ONE_YEAR_MS})));
expressServer.use(subdir + '/ghost/ember', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS})));
expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS})));
// Force SSL
// NOTE: Importantly this is _after_ the check above for admin-theme static resources,
@ -339,5 +323,4 @@ module.exports = function (server) {
// Export middleware functions directly
module.exports.middleware = middleware;
// Expose middleware functions in this file as well
module.exports.middleware.redirectToSignup = redirectToSignup;
module.exports.middleware.redirectToSetup = redirectToSetup;

View File

@ -37,12 +37,7 @@ var middleware = {
// exceptions for signin, signout, signup, forgotten, reset only
// api and frontend use different authentication mechanisms atm
authenticate: function (req, res, next) {
var noAuthNeeded = [
'/ghost/signin/', '/ghost/signout/', '/ghost/signup/',
'/ghost/forgotten/', '/ghost/reset/', '/ghost/ember/',
'/ghost/setup/'
],
path,
var path,
subPath;
// SubPath is the url path starting after any default subdirectories
@ -80,46 +75,10 @@ var middleware = {
}
)(req, res, next);
}
if (noAuthNeeded.indexOf(subPath) < 0 && subPath.indexOf('/ghost/api/') !== 0) {
return middleware.auth(req, res, next);
}
}
next();
},
// ### Auth Middleware
// Authenticate a request by redirecting to login if not logged in.
// We strip /ghost/ out of the redirect parameter for neatness
auth: function (req, res, next) {
if (!req.user) {
var subPath = req.path.substring(config().paths.subdir.length),
reqPath = subPath.replace(/^\/ghost\/?/gi, ''),
redirect = '';
if (reqPath !== '') {
redirect = '?r=' + encodeURIComponent(reqPath);
}
if (subPath.indexOf('/ember') > -1) {
return res.redirect(config().paths.subdir + '/ghost/ember/signin/');
}
return res.redirect(config().paths.subdir + '/ghost/signin/' + redirect);
}
next();
},
// ## AuthApi Middleware
// Authenticate a request to the API by responding with a 401 and json error details
authAPI: function (req, res, next) {
if (!req.user) {
res.json(401, { error: 'Please sign in' });
return;
}
next();
},
// Check if we're logged in, and if so, redirect people back to dashboard
// Login and signup forms in particular
redirectToDashboard: function (req, res, next) {

View File

@ -11,65 +11,38 @@ adminRoutes = function (middleware) {
var router = express.Router(),
subdir = config().paths.subdir;
// Have ember route look for hits first
// to prevent conflicts with pre-existing routes
router.get('/ghost/ember/*', middleware.redirectToSetup, admin.index);
// ### Admin routes
router.get('/logout/', function redirect(req, res) {
router.get('^/logout/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
router.get('/signout/', function redirect(req, res) {
router.get('^/signout/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
router.get('/signin/', function redirect(req, res) {
router.get('^/signin/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signin/');
});
router.get('/signup/', function redirect(req, res) {
router.get('^/signup/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signup/');
});
router.post('/ghost/setup/', admin.doSignup);
router.get('/ghost/signout/', admin.signout);
router.post('/ghost/signout/', admin.doSignout);
router.get('/ghost/signin/', middleware.redirectToSignup, middleware.redirectToDashboard, admin.signin);
router.post('/ghost/signin/', admin.doSignin);
router.get('/ghost/signup/', middleware.redirectToDashboard, admin.signup);
router.post('/ghost/signup/', admin.doSignup);
router.get('/ghost/forgotten/', middleware.redirectToDashboard, admin.forgotten);
router.post('/ghost/forgotten/', admin.doForgotten);
router.get('/ghost/reset/:token', admin.reset);
router.post('/ghost/reset/:token', admin.doReset);
router.get('/ghost/editor/:id/:action', admin.editor);
router.get('/ghost/editor/:id/', admin.editor);
router.get('/ghost/editor/', admin.editor);
router.get('/ghost/content/', admin.content);
router.get('/ghost/settings*', admin.settings);
router.get('/ghost/debug/', admin.debug.index);
router.get('/ghost/export/', admin.debug.exportContent);
router.post('/ghost/upload/', middleware.busboy, admin.upload);
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
router.get(/\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) {
router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) {
/*jslint unparam:true*/
res.redirect(subdir + '/ghost/');
});
router.get(/\/ghost$/, function (req, res) {
/*jslint unparam:true*/
res.redirect(subdir + '/ghost/');
});
router.get('/ghost/', admin.indexold);
router.get('/ghost/*', middleware.redirectToSetup, admin.index);
return router;
};

View File

@ -1,17 +0,0 @@
{{!< default}}
<section class="content-view-container">
<section class="content-list js-content-list">
<header class="floatingheader">
<section class="content-filter">
<small>All Posts</small>
</section>
<a href="{{admin_url}}/editor/" class="button button-add" title="New Post"><span class="hidden">New Post</span></a>
</header>
<section class="content-list-content">
<ol></ol>
</section>
</section>
<section class="content-preview js-content-preview">
</section>
</section>

View File

@ -1,58 +0,0 @@
{{!< default}}
<div id="debug-page" class="wrapper">
<aside class="settings-sidebar" role="complementary">
<header>
<h1 class="title">Ugly Debug Tools</h1>
</header>
<nav class="settings-menu">
<ul>
<li><a class="general" href="javascript:void(0);">General</a></li>
</ul>
</nav>
</aside>
<section id="debug-general" class="settings-content" style="display: block">
<header>
<h2 class="title">General</h2>
</header>
<section class="content">
<form id="settings-export">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<fieldset>
<div class="form-group">
<label>Export</label>
<a href="{{admin_url}}/export/" class="button-save">Export</a>
<p>Export the blog settings and data.</p>
</div>
</fieldset>
</form>
<form id="settings-import" enctype="multipart/form-data">
<fieldset>
<div class="form-group">
<label>Import</label>
<input type="file" class="button-add" name="importfile" id="importfile" />
<button type="submit" class="button-save" value="Import" id="startupload" >Import</button>
<p>Import from another Ghost installation. If you import a user, this will replace the current user & log you out.</p>
</div>
</fieldset>
</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">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">Send</button>
<p>Sends a test email to your address.</p>
</div>
</fieldset>
</form>
</section>
</section>
</div>

View File

@ -1,47 +0,0 @@
<!doctype html>
<!--[if (IE 8)&!(IEMobile)]><html class="no-js lt-ie9" lang="en"><![endif]-->
<!--[if (gte IE 9)| IEMobile |!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="csrf-param" content="{{csrfToken}}" />
<title>Ghost Admin</title>
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui" />
<meta http-equiv="cleartype" content="on" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Ghost" />
<link rel="shortcut icon" href="{{asset "favicon.ico"}}" />
<link rel="apple-touch-icon-precomposed" href="{{asset "img/touch-icon-iphone.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{{asset "img/touch-icon-ipad.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{{asset "img/small.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{asset "img/medium.png" ember="true"}}" />
<meta name="application-name" content="Ghost" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-square70x70logo" content="{{asset "img/small.png" ember="true"}}" />
<meta name="msapplication-square150x150logo" content="{{asset "img/medium.png" ember="true"}}" />
<meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ember="true"}}" />
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700" />
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ember="true"}}" />
</head>
<body class="{{bodyClass}}{{update_notification classOnly="true"}}">
{{{ember_script_tags}}}
<script>
window.ENV = {
{{#user}}user: {{{this}}},{{/user}}
config: {{{config}}}
};
window.App = require('ghost/app')['default'].create(window.ENV);
</script>
</body>
</html>

View File

@ -33,23 +33,15 @@
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ghost="true"}}" />
</head>
<body class="{{bodyClass}}{{update_notification classOnly="true"}}">
{{#unless hideNavbar}}
{{> navbar}}
{{/unless}}
<main role="main" id="main">
{{update_notification}}
<aside id="notifications" class="notifications">
{{> notifications}}
</aside>
{{{body}}}
</main>
<div id="modal-container"></div>
<div class="modal-background fade"></div>
{{{ghost_script_tags}}}
<script>
window.ENV = {
{{#user}}user: {{{this}}},{{/user}}
config: {{{config}}}
};
window.App = require('ghost/app')['default'].create(window.ENV);
</script>
</body>
</html>

View File

@ -1,100 +0,0 @@
{{!< default}}
<style>
/* Additional styling for touch editor, will be moved in future */
.touch-editor #entry-markdown {
padding: 15px;
margin-bottom: 40px;
font-family: monospace;
font-size: 1.4em;
line-height: 1.3em;
color: #242628;
}
</style>
<section class="entry-container">
<header>
<section class="box entry-title">
<input type="text" id="entry-title"
placeholder="Your Post Title"
value="" tabindex="1">
</section>
</header>
<section class="entry-markdown active">
<header class="floatingheader">
<small>Markdown</small>
<a class="markdown-help" href="#"><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content" data-filestorage={{file_storage}}>
<textarea id="entry-markdown"></textarea>
</section>
</section>{{!.entry-markdown}}
<section class="entry-preview">
<header class="floatingheader">
<small>Preview <span class="entry-word-count js-entry-word-count">0 words</span></small>
</header>
<section class="entry-preview-content">
<div class="rendered-markdown">
{{!The content gets inserted in here, fuckers!}}
</div>
</section>
</section>{{!.entry-preview}}
</section>
<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" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-right overlay">
<form>
<table class="plain">
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" placeholder="" value="" />
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" placeholder="Now" value=""><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
</section>
<section id="entry-actions" class="js-publish-splitbutton splitbutton-save">
<button type="button" class="js-publish-button button-save"></button>
<a class="options up" data-toggle="ul" href="#" title="Post Settings"><span class="hidden">Post Settings</span></a>
<ul class="editor-options overlay" style="display:none">
<li data-set-status="published"><a href="#"></a></li>
<li data-set-status="draft"><a href="#"></a></li>
</ul>
</section>
</div>
</nav>
</footer>

View File

@ -1,4 +0,0 @@
{{!< default}}
<section class="forgotten-box js-forgotten-box">
</section>

View File

@ -1,4 +0,0 @@
{{!< default}}
<section class="login-box js-login-box">
</section>

View File

@ -1,26 +0,0 @@
<header id="global-header" class="navbar">
<a class="ghost-logo" href="{{admin_url absolute="true" frontend="true"}}" data-off-canvas="left" title="{{admin_url absolute="true" frontend="true"}}">
<span class="hidden">Ghost </span>
</a>
<nav id="global-nav" role="navigation">
<ul id="main-menu" >
{{#each adminNav}}
<li class="{{navClass}}{{#if selected}} active{{/if}}"><a href="{{admin_url}}{{path}}">{{name}}</a></li>
{{/each}}
<li id="usermenu" class="usermenu subnav">
<a href="#" data-toggle="ul" class="dropdown">
<img class="avatar" src="{{#if currentUser.image}}{{currentUser.image}}{{else}}{{asset "shared/img/user-image.png"}}{{/if}}" alt="Avatar" />
<span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.email}}{{/if}}</span>
</a>
<ul class="overlay">
<li class="usermenu-profile"><a href="{{admin_url}}/settings/user/">Your Profile</a></li>
<li class="divider"></li>
<li class="usermenu-help"><a href="http://support.ghost.org/">Help / Support</a></li>
<li class="divider"></li>
<li class="usermenu-signout"><a href="{{admin_url}}/signout/">Sign Out</a></li>
</ul>
</li>
</ul>
</nav>
</header>

View File

@ -1,10 +0,0 @@
{{#if messages}}
{{#each messages}}
<div class="js-bb-notification">
<section class="notification{{#if type}}-{{type}}{{/if}} notification-{{status}} js-notification">
{{{message}}}
<a class="close" href="#" data-id="{{id}}"><span class="hidden">Close</span></a>
</section>
</div>
{{/each}}
{{/if}}

View File

@ -1,4 +0,0 @@
{{!< default}}
<section class="reset-box js-reset-box">
</section>

View File

@ -1,8 +0,0 @@
{{!< default}}
<div class="wrapper">
<aside class="settings-sidebar" role="complementary" data-apps={{apps}}>
</aside>
<section class="settings-content" data-filestorage={{file_storage}}></section>
</div>

View File

@ -1,4 +0,0 @@
{{!< default}}
<section class="signup-box js-signup-box">
</section>

View File

@ -1,11 +1,11 @@
// Posts
var blanket = require("blanket")({
"pattern": ["/core/server/", "/core/clientold/", "/core/shared/"],
"data-cover-only": ["/core/server/", "/core/clientold/", "/core/shared/"]
var blanket = require('blanket')({
'pattern': ['/core/server/', '/core/client/', '/core/shared/'],
'data-cover-only': ['/core/server/', '/core/client/', '/core/shared/']
}),
requireDir = require("require-dir");
requireDir = require('require-dir');
requireDir("./unit");
requireDir("./integration");
requireDir("./functional/routes");
requireDir('./unit');
requireDir('./integration');
requireDir('./functional/routes');

View File

@ -1,4 +1,4 @@
/*globals Ghost, casper, __utils__ */
/*globals casper */
/**
* Casper Tests
@ -50,7 +50,58 @@ var DEBUG = false, // TOGGLE THIS TO GET MORE SCREENSHOTS
testPost = {
title: 'Bacon ipsum dolor sit amet',
html: 'I am a test post.\n#I have some small content'
};
},
screens;
screens = {
'root': {
url: 'ghost/',
linkSelector: '#main-menu > li.content a',
selector: '#main-menu .content.active'
},
'content': {
url: 'ghost/content/',
linkSelector: '#main-menu > li.content a',
selector: '#main-menu .content.active'
},
'editor': {
url: 'ghost/editor/',
linkSelector: '#main-menu > li.editor a',
selector: '#entry-title'
},
'settings': {
url: 'ghost/settings/',
linkSelector: '#main-menu > li.settings a',
selector: '.settings-content'
},
'settings.general': {
url: 'ghost/settings/general',
selector: '.settings-content .settings-general'
},
'settings.user': {
url: 'ghost/settings/user',
linkSelector: '#user-menu li.usermenu-profile a',
selector: '.settings-content .settings-user'
},
'signin': {
url: 'ghost/signin/',
selector: '.button-save'
},
'signout': {
url: 'ghost/signout/',
linkSelector: '#user-menu li.usermenu-signout a',
// When no user exists we get redirected to setup which has button-add
selector: '.button-save, .button-add'
},
'signup': {
url: 'ghost/signup/',
selector: '.button-save'
},
'setup': {
url: 'ghost/setup/',
selector: '.button-add'
}
};
casper.writeContentToCodeMirror = function (content) {
var lines = content.split('\n');
@ -101,50 +152,6 @@ casper.thenOpenAndWaitForPageLoad = function (screen, then, timeout) {
then = then || function () {};
timeout = timeout || casper.failOnTimeout(casper.test, 'Unable to load ' + screen);
var screens = {
'root': {
url: 'ghost/ember/',
selector: '#main-menu .content.active'
},
'content': {
url: 'ghost/ember/content/',
selector: '#main-menu .content.active'
},
'editor': {
url: 'ghost/ember/editor/',
selector: '#entry-title'
},
'settings': {
url: 'ghost/ember/settings/',
selector: '.settings-content'
},
'settings.general': {
url: 'ghost/ember/settings/general',
selector: '.settings-content .settings-general'
},
'settings.user': {
url: 'ghost/ember/settings/user',
selector: '.settings-content .settings-user'
},
'signin': {
url: 'ghost/ember/signin/',
selector: '.button-save'
},
'signout': {
url: 'ghost/ember/signout/',
// When no user exists we get redirected to setup which has button-add
selector: '.button-save, .button-add'
},
'signup': {
url: 'ghost/ember/signup/',
selector: '.button-save'
},
'setup': {
url: 'ghost/ember/setup/',
selector: '.button-add'
}
};
return casper.thenOpen(url + screens[screen].url).then(function () {
// Some screens fade in
return casper.waitForOpaque(screens[screen].selector, then, timeout, 10000);
@ -155,34 +162,6 @@ casper.thenTransitionAndWaitForScreenLoad = function (screen, then, timeout) {
then = then || function () {};
timeout = timeout || casper.failOnTimeout(casper.test, 'Unable to load ' + screen);
var screens = {
'root': {
linkSelector: '#main-menu > li.content a',
selector: '#main-menu .content.active'
},
'content': {
linkSelector: '#main-menu > li.content a',
selector: '#main-menu .content.active'
},
'editor': {
linkSelector: '#main-menu > li.editor a',
selector: '#entry-title'
},
'settings': {
linkSelector: '#main-menu > li.settings a',
selector: '.settings-content'
},
'settings.user': {
linkSelector: '#user-menu li.usermenu-profile a',
selector: '.settings-content .settings-user'
},
'signout': {
linkSelector: '#user-menu li.usermenu-signout a',
// When no user exists we get redirected to setup which has button-add
selector: '.button-save, .button-add'
},
};
return casper.thenClick(screens[screen].linkSelector).then(function () {
// Some screens fade in
return casper.waitForOpaque(screens[screen].selector, then, timeout, 10000);
@ -323,46 +302,6 @@ var CasperTest = (function () {
});
// Wrapper around `casper.test.begin`
function oldBegin(testName, expect, suite, doNotAutoLogin) {
_beforeDoneHandler = _noop;
var runTest = function (test) {
test.filename = testName.toLowerCase().replace(/ /g, '-').concat('.png');
casper.start('about:blank').viewport(1280, 1024);
if (!doNotAutoLogin) {
// Only call register once for the lifetime of Mindless
if (!_isUserRegistered) {
CasperTest.Routines.oldLogout.run(test);
CasperTest.Routines.oldRegister.run(test);
_isUserRegistered = true;
}
/* Ensure we're logged out at the start of every test or we may get
unexpected failures. */
CasperTest.Routines.oldLogout.run(test);
CasperTest.Routines.oldLogin.run(test);
}
suite.call(casper, test);
casper.run(function () {
test.done();
});
};
if (typeof expect === 'function') {
doNotAutoLogin = suite;
suite = expect;
casper.test.begin(testName, runTest);
} else {
casper.test.begin(testName, expect, runTest);
}
}
function begin(testName, expect, suite, doNotAutoLogin) {
_beforeDoneHandler = _noop;
@ -415,7 +354,6 @@ var CasperTest = (function () {
}
return {
oldBegin: oldBegin,
begin: begin,
beforeDone: beforeDone
};
@ -424,32 +362,15 @@ var CasperTest = (function () {
CasperTest.Routines = (function () {
function oldRegister(test) {
casper.thenOpen(url + 'ghost/signup/');
casper.waitForOpaque('.signup-box', function then() {
this.fill('#signup', newUser, true);
});
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
var errorText = casper.evaluate(function () {
return document.querySelector('.notification-error').innerText;
});
casper.echoConcise('It appears as though a user is already registered. Error text: ' + errorText);
}, function onTimeout() {
casper.echoConcise('It appears as though a user was not already registered.');
}, 2000);
}
function setup() {
casper.thenOpenAndWaitForPageLoad('setup', function then() {
casper.captureScreenshot('ember_setting_up1.png');
casper.captureScreenshot('setting_up1.png');
casper.waitForOpaque('.setup-box', function then() {
this.fillAndAdd('#setup', newSetup);
});
casper.captureScreenshot('ember_setting_up2.png');
casper.captureScreenshot('setting_up2.png');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
var errorText = casper.evaluate(function () {
@ -460,57 +381,28 @@ CasperTest.Routines = (function () {
casper.echoConcise('It appears as though a user was not already registered.');
}, 2000);
casper.captureScreenshot('ember_setting_up3.png');
casper.captureScreenshot('setting_up3.png');
});
}
function oldLogin(test) {
casper.thenOpen(url + 'ghost/signin/');
casper.waitForResource(/ghost\/signin/);
casper.waitForSelector('.login-box', function () {}, function () {
console.log(casper.getHTML());
});
casper.waitForOpaque('.login-box', function then() {
casper.captureScreenshot('got_sign_in.png');
this.fill('#login', user, true);
casper.captureScreenshot('filled_sign_in.png');
});
casper.waitForResource(/ghost\/$/).then(function () {
casper.captureScreenshot('have_logged_in.png');
});
}
function signin() {
casper.thenOpenAndWaitForPageLoad('signin', function then() {
casper.waitForOpaque('.login-box', function then() {
casper.captureScreenshot('ember_signing_in.png');
casper.captureScreenshot('signing_in.png');
this.fillAndSave('#login', user);
casper.captureScreenshot('ember_signing_in2.png');
casper.captureScreenshot('signing_in2.png');
});
casper.waitForResource(/posts\/\?status=all&staticPages=all/, function then() {
casper.captureScreenshot('ember_signing_in3.png');
casper.captureScreenshot('signing_in.png');
}, function timeout() {
casper.test.fail('Unable to signin and load admin panel');
});
});
}
function oldLogout(test) {
casper.thenOpen(url + 'ghost/signout/');
casper.captureScreenshot('logging_out.png');
// Wait for signin or signup
casper.waitForResource(/ghost\/sign/);
}
function signout() {
casper.thenOpenAndWaitForPageLoad('signout', function then() {
casper.captureScreenshot('ember_signing_out.png');
@ -578,14 +470,11 @@ CasperTest.Routines = (function () {
}
return {
oldRegister: _createRunner(oldRegister),
oldLogin: _createRunner(oldLogin),
oldLogout: _createRunner(oldLogout),
togglePermalinks: _createRunner(togglePermalinks),
setup: _createRunner(setup),
signin: _createRunner(signin),
signout: _createRunner(signout),
createTestPost: _createRunner(createTestPost)
createTestPost: _createRunner(createTestPost),
togglePermalinks: _createRunner(togglePermalinks)
};
}());

View File

@ -6,7 +6,7 @@
CasperTest.begin('Admin navigation bar is correct', 27, function suite(test) {
casper.thenOpenAndWaitForPageLoad('root', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.then(function testNavItems() {
@ -22,29 +22,29 @@ CasperTest.begin('Admin navigation bar is correct', 27, function suite(test) {
// Content
test.assertExists('#main-menu li.content a', 'Content nav item exists');
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
test.assertEquals(contentHref, '/ghost/ember/', 'Content href is correct');
test.assertEquals(contentHref, '/ghost/', 'Content href is correct');
test.assertExists('#main-menu li.content.active', 'Content nav item is not marked active');
// Editor
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
test.assertEquals(editorHref, '/ghost/ember/editor/', 'Editor href is correct');
test.assertEquals(editorHref, '/ghost/editor/', 'Editor href is correct');
test.assertDoesntExist('#main-menu li.editor.active', 'Editor nav item is not marked active');
// Settings
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
test.assertEquals(settingsHref, '/ghost/ember/settings/', 'Settings href is correct');
test.assertEquals(settingsHref, '/ghost/settings/', 'Settings href is correct');
test.assertDoesntExist('#main-menu li.settings.active', 'Settings nav item is marked active');
});
casper.then(function testUserMenuNotVisible() {
test.assertExists('#usermenu', 'User menu nav item exists');
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
test.assertNotExists('#usermenu ul.overlay.open', 'User menu should not be visible');
});
casper.thenClick('#usermenu a');
casper.waitForSelector('#usermenu ul.overlay', function then() {
casper.waitForSelector('#usermenu ul.overlay.open', function then() {
var profileHref = this.getElementAttribute('#usermenu li.usermenu-profile a', 'href'),
helpHref = this.getElementAttribute('#usermenu li.usermenu-help a', 'href'),
signoutHref = this.getElementAttribute('#usermenu li.usermenu-signout a', 'href');
@ -54,7 +54,7 @@ CasperTest.begin('Admin navigation bar is correct', 27, function suite(test) {
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile',
'Profile menu item has correct text');
test.assertEquals(profileHref, '/ghost/ember/settings/user/', 'Profile href is correct');
test.assertEquals(profileHref, '/ghost/settings/user/', 'Profile href is correct');
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
@ -62,23 +62,23 @@ CasperTest.begin('Admin navigation bar is correct', 27, function suite(test) {
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Signout menu item has correct text');
// test.assertEquals(signoutHref, '/ghost/ember/signout/', 'Sign Out href is correct');
// test.assertEquals(signoutHref, '/ghost/signout/', 'Sign Out href is correct');
}, casper.failOnTimeout(test, 'WaitForSelector #usermenu ul.overlay failed'));
});
CasperTest.begin('Can transition to the editor and back', 6, function suite(test) {
casper.thenOpenAndWaitForPageLoad('root', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.thenTransitionAndWaitForScreenLoad('editor', function testTransitionToEditor() {
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
test.assertExists('.entry-markdown', 'Ghost editor is present');
test.assertExists('.entry-preview', 'Ghost preview is present');
});
casper.thenTransitionAndWaitForScreenLoad('content', function testTransitionToContent() {
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
});

View File

@ -10,7 +10,7 @@ CasperTest.begin('Content screen is correct', 21, function suite(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.then(function testViews() {
@ -19,7 +19,7 @@ CasperTest.begin('Content screen is correct', 21, function suite(test) {
test.assertExists('.content-list .floatingheader a.button.button-add', 'add new post button exists');
test.assertEquals(
this.getElementAttribute('.content-list .floatingheader a.button.button-add', 'href'),
'/ghost/ember/editor/', 'add new post href is correct'
'/ghost/editor/', 'add new post href is correct'
);
test.assertExists('.content-list-content li .entry-title', 'Content list view has at least one item');
test.assertSelectorHasText(
@ -72,7 +72,7 @@ CasperTest.begin('Content list shows correct post status', 7, function testStati
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
// Select first non-draft, non-static post. Should be second in the list at this stage of testing.
@ -117,7 +117,7 @@ CasperTest.begin('Delete post modal', 7, function testDeleteModal(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
// Open post settings menu
@ -163,7 +163,7 @@ CasperTest.begin('Delete post modal', 7, function testDeleteModal(test) {
//
// casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
// test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
// });
//});
@ -174,7 +174,7 @@ CasperTest.begin('Posts can be marked as featured', 10, function suite(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
// Mark as featured
@ -223,7 +223,7 @@ CasperTest.begin('Post url can be changed', 7, function suite(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.thenClick('a.post-settings');
@ -262,7 +262,7 @@ CasperTest.begin('Post published date can be changed', 7, function suite(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.thenClick('a.post-settings');
@ -301,7 +301,7 @@ CasperTest.begin('Post can be changed to static page', 7, function suite(test) {
// Begin test
casper.thenOpenAndWaitForPageLoad('content', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
});
casper.thenClick('.content-preview a.post-settings');

View File

@ -5,7 +5,7 @@
CasperTest.begin('Ghost editor functions correctly', 19, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
test.assertExists('.entry-markdown', 'Ghost editor is present');
test.assertExists('.entry-preview', 'Ghost preview is present');
});
@ -42,7 +42,7 @@ CasperTest.begin('Ghost editor functions correctly', 19, function suite(test) {
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function onSuccess() {
test.assertUrlMatch(/ghost\/ember\/editor\/\d+\/$/, 'got an id on our URL');
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
test.assertEvalEquals(function () {
return document.querySelector('#entry-title').value;
}, testPost.title, 'Title is correct');
@ -118,7 +118,7 @@ CasperTest.begin('Ghost editor functions correctly', 19, function suite(test) {
CasperTest.begin('Markdown in editor works', 4, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
casper.then(function testImage() {
@ -141,7 +141,7 @@ CasperTest.begin('Markdown in editor works', 4, function suite(test) {
CasperTest.begin('Image Uploads', 17, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
// Test standard image upload modal
@ -169,7 +169,7 @@ CasperTest.begin('Image Uploads', 17, function suite(test) {
// Test image source location
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
var testFileLocation = 'test/file/location';
@ -192,7 +192,7 @@ CasperTest.begin('Image Uploads', 17, function suite(test) {
// Test image url source location
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
casper.then(function () {
@ -218,7 +218,7 @@ CasperTest.begin('Image Uploads', 17, function suite(test) {
CasperTest.begin('Tag editor', 7, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
var tagName = 'someTagName';
@ -243,10 +243,10 @@ casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
});
});
CasperTest.begin('Post settings menu', 30, function suite(test) {
CasperTest.begin('Post settings menu', 31, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
casper.then(function () {
@ -282,8 +282,7 @@ CasperTest.begin('Post settings menu', 30, function suite(test) {
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
// TODO: Uncomment when the post save notifications are correct #2850
// test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
@ -401,7 +400,7 @@ CasperTest.begin('Post settings menu', 30, function suite(test) {
casper.thenClick('#modal-container .js-button-accept');
});
casper.waitForUrl(/ghost\/ember\/\d+\/$/, function onSuccess() {
casper.waitForUrl(/ghost\/\d+\/$/, function onSuccess() {
test.assert(true, 'clicking the delete post button should bring us to the content page');
});
});
@ -409,7 +408,7 @@ CasperTest.begin('Post settings menu', 30, function suite(test) {
CasperTest.begin('Publish menu - new post', 11, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
// ... check default option status, label, class
@ -455,7 +454,7 @@ CasperTest.begin('Publish menu - existing post', 21, function suite(test) {
// Create a post, save it and test refreshed editor
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
casper.then(function createTestPost() {
@ -471,7 +470,7 @@ CasperTest.begin('Publish menu - existing post', 21, function suite(test) {
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function checkPostWasCreated() {
test.assertUrlMatch(/ghost\/ember\/editor\/\d+\/$/, 'got an id on our URL');
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
});
// ... check option status, label, class now that we're *saved* as 'draft'
@ -505,7 +504,7 @@ CasperTest.begin('Publish menu - existing post', 21, function suite(test) {
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function checkPostWasCreated() {
test.assertUrlMatch(/ghost\/ember\/editor\/\d+\/$/, 'got an id on our URL');
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
});
// ... check option status, label, class for saved as 'published'
@ -553,7 +552,7 @@ CasperTest.begin('Publish menu - existing post', 21, function suite(test) {
CasperTest.begin('Markdown help modal', 5, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/editor\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
});
// open markdown help modal

View File

@ -11,7 +11,7 @@ var generalTabDetector = '.settings-content form#settings-general',
CasperTest.begin('Settings screen is correct', 17, function suite(test) {
casper.thenOpenAndWaitForPageLoad('settings', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/settings\/general\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
});
casper.then(function testViews() {
@ -49,7 +49,7 @@ CasperTest.begin('Settings screen is correct', 17, function suite(test) {
CasperTest.begin('General settings pane is correct', 8, function suite(test) {
casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/settings\/general\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
});
function assertImageUploaderModalThenClose() {
@ -113,7 +113,7 @@ CasperTest.begin('General settings pane is correct', 8, function suite(test) {
CasperTest.begin('General settings validation is correct', 7, function suite(test) {
casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/settings\/general\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
});
// Ensure general blog title field length validation
@ -176,7 +176,7 @@ CasperTest.begin('General settings validation is correct', 7, function suite(tes
//CasperTest.begin('Can save settings', 6, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('settings.user', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/settings\/user\/$/, 'Landed on the correct URL');
// test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Landed on the correct URL');
// });
//
// function handleUserRequest(requestData) {
@ -291,7 +291,7 @@ CasperTest.begin('General settings validation is correct', 7, function suite(tes
// CasperTest.begin('User settings screen shows remaining characters for Bio properly', 4, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('settings.user', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
// test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
// });
// function getRemainingBioCharacterCount() {

View File

@ -1,11 +1,11 @@
// # Signup Test
// Test that signup works correctly
// # Setup Test
// Test that setup works correctly
/*global CasperTest, casper, email */
CasperTest.begin('Ghost setup fails properly', 5, function suite(test) {
casper.thenOpenAndWaitForPageLoad('setup', function then() {
test.assertUrlMatch(/ghost\/ember\/setup\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/setup\/$/, 'Landed on the correct URL');
});
casper.then(function setupWithShortPassword() {

View File

@ -3,40 +3,41 @@
/*globals CasperTest, casper, url, newUser, user, falseUser */
CasperTest.begin('Ensure a User is Registered', 3, function suite(test) {
casper.thenOpenAndWaitForPageLoad('signup', function checkUrl() {
test.assertUrlMatch(/ghost\/ember\/signup\/$/, 'Landed on the correct URL');
});
casper.waitForOpaque('.signup-box',
function then() {
this.fillAndSave('#signup', newUser);
},
function onTimeout() {
test.fail('Sign up form didn\'t fade in.');
});
casper.captureScreenshot('login_register_test.png');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'already registered');
// If the previous assert succeeds, then we should skip the next check and just pass.
casper.echoConcise('Already registered!');
casper.captureScreenshot('already_registered.png');
}, function onTimeout() {
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'If we\'re not already registered, we should be logged in.');
casper.echoConcise('Successfully registered.');
}, 2000);
casper.thenOpenAndWaitForPageLoad('signout', function then() {
test.assertUrlMatch(/ghost\/ember\/signin/, 'We got redirected to signin page.');
});
}, true);
// TODO fix signup vs setup testing
//CasperTest.begin('Ensure a User is Registered', 3, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signup', function checkUrl() {
// test.assertUrlMatch(/ghost\/signup\/$/, 'Landed on the correct URL');
// });
//
// casper.waitForOpaque('.signup-box',
// function then() {
// this.fillAndSave('#signup', newUser);
// },
// function onTimeout() {
// test.fail('Sign up form didn\'t fade in.');
// });
//
// casper.captureScreenshot('login_register_test.png');
//
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
// test.assertSelectorHasText('.notification-error', 'already registered');
// // If the previous assert succeeds, then we should skip the next check and just pass.
// casper.echoConcise('Already registered!');
// casper.captureScreenshot('already_registered.png');
// }, function onTimeout() {
// test.assertUrlMatch(/ghost\/\d+\/$/, 'If we\'re not already registered, we should be logged in.');
// casper.echoConcise('Successfully registered.');
// }, 2000);
//
// casper.thenOpenAndWaitForPageLoad('signout', function then() {
// test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page.');
// });
//}, true);
CasperTest.begin('Ghost admin will load login page', 3, function suite(test) {
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/signin\/$/, 'We should be presented with the signin page.');
test.assertUrlMatch(/ghost\/signin\/$/, 'We should be presented with the signin page.');
casper.then(function testLink() {
var link = this.evaluate(function (selector) {
@ -44,7 +45,7 @@ CasperTest.begin('Ghost admin will load login page', 3, function suite(test) {
}, '.forgotten-password');
casper.echoConcise('LINK' + link);
test.assert(link === '/ghost/ember/forgotten/', 'Has correct forgotten password link');
test.assert(link === '/ghost/forgotten/', 'Has correct forgotten password link');
});
});
}, true);
@ -62,7 +63,7 @@ CasperTest.begin('Redirects login to signin', 2, function suite(test) {
// CasperTest.begin('Can\'t spam it', 4, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitle() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/signin\/$/, 'Landed on the correct URL');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
// casper.waitForOpaque('.login-box',
@ -98,7 +99,7 @@ CasperTest.begin('Redirects login to signin', 2, function suite(test) {
// CasperTest.begin('Login limit is in place', 4, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/signin\/$/, 'Landed on the correct URL');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
// casper.waitForOpaque('.login-box',
@ -127,7 +128,7 @@ CasperTest.begin('Redirects login to signin', 2, function suite(test) {
CasperTest.begin('Can login to Ghost', 5, function suite(test) {
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/signin\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
});
casper.waitForOpaque('.login-box', function then() {
@ -137,7 +138,7 @@ CasperTest.begin('Can login to Ghost', 5, function suite(test) {
casper.wait(2000);
casper.waitForResource(/posts/, function testForDashboard() {
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
test.assertExists('#global-header', 'Global admin header is present');
test.assertExists('.manage', 'We\'re now on content');
}, function onTimeOut() {
@ -149,7 +150,7 @@ CasperTest.begin('Can login to Ghost', 5, function suite(test) {
// CasperTest.begin('Ensure email field form validation', 3, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/ember\/signin\/$/, 'Landed on the correct URL');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
// casper.waitForOpaque('.js-login-box',

View File

@ -9,7 +9,7 @@ CasperTest.begin('Ghost signout works correctly', 3, function suite(test) {
casper.thenOpenAndWaitForPageLoad('root', function then() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/ember\/\d+\/$/, 'Landed on the correct URL without signing in');
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL without signing in');
});
casper.thenClick('#usermenu a').waitFor(function checkOpaque() {

View File

@ -7,7 +7,7 @@
CasperTest.begin('Ghost signup fails properly', 0, function suite(test) {
/*
casper.thenOpenAndWaitForPageLoad('signup', function then() {
test.assertUrlMatch(/ghost\/ember\/signup\/$/, 'Landed on the correct URL');
test.assertUrlMatch(/ghost\/signup\/$/, 'Landed on the correct URL');
});
casper.then(function signupWithShortPassword() {

View File

@ -1,771 +0,0 @@
/*globals casper, __utils__, url, testPost, newUser */
CasperTest.begin("Content screen is correct", 22, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
test.assertExists('.notification-success', 'got success notification');
});
// Begin test
casper.thenOpen(url + "ghost/content/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
});
casper.then(function testViews() {
test.assertExists(".content-view-container", "Content main view is present");
test.assertExists(".content-list-content", "Content list view is present");
test.assertExists('.content-list .floatingheader a.button.button-add', 'add new post button exists');
test.assertEquals(this.getElementAttribute('.content-list .floatingheader a.button.button-add', 'href'), '/ghost/editor/', 'add new post href is correct');
test.assertExists(".content-list-content li .entry-title", "Content list view has at least one item");
test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "title is present and has content");
test.assertSelectorHasText(".content-list-content li:first-child .entry-meta .status .draft", 'Draft', "status is present has content");
test.assertExists(".content-preview", "Content preview is present");
test.assertSelectorHasText('.content-preview header .status', 'Written', 'preview header contains "Written" when post is a draft');
test.assertSelectorHasText('.content-preview header .author', newUser.name, 'preview header contains author name');
});
casper.then(function testEditPostButton() {
test.assertExists('.content-preview a.post-edit', 'edit post button exists');
});
casper.then(function testPostSettingsMenu() {
test.assertExists('.content-preview a.post-settings', 'post settings button exists');
this.click('.content-preview a.post-settings');
});
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
casper.then(function postSettingsMenuItems() {
test.assertExists('.post-settings-menu #static-page', 'post settings static page exists');
test.assertExists('.post-settings-menu a.delete', 'post settings delete this post exists');
});
casper.then(function testActiveItem() {
test.assertEvalEquals(function () {
return document.querySelector('.content-list-content li').className;
}, "active", "first item is active");
}).thenClick(".content-list-content li:nth-child(2) a", function then() {
test.assertEvalEquals(function () {
return document.querySelectorAll('.content-list-content li')[1].className;
}, "active", "second item is active");
});
});
CasperTest.begin('Content list shows correct post status', 8, function testStaticPageStatus(test) {
// Make sure we have at least one post in the published state
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
// Open the publish options menu;
casper.thenClick('.js-publish-splitbutton .options.up');
// Select the publish post button
casper.thenClick('.js-publish-splitbutton li[data-set-status="published"]');
casper.waitForSelectorTextChange('.js-publish-button', function onSuccess() {
this.click('.js-publish-button');
}, function onTimeout() {
test.assert(false, 'publish button did not change to published');
});
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
test.assertExists('.notification-success', 'got success notification');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
});
// Select first non-draft, non-static post. Should be second in the list
// at this stage of testing.
casper.thenClick('.content-list-content li:nth-child(2) a');
// Test for status of 'Published'
casper.then(function checkStatus() {
test.assertSelectorHasText('.content-list-content li.active .entry-meta .status time', 'Published', 'status is present and labeled as published');
});
// Change post to static page
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
casper.thenClick('.post-settings-menu #static-page');
casper.waitForSelector('.content-list-content li .entry-meta .status .page', function waitForSuccess() {
test.assertSelectorHasText('.content-list-content li .entry-meta .status .page', 'Page', 'status is Page');
}, function onTimeout() {
test.assert(false, 'status did not change');
});
});
CasperTest.begin('Preview shows correct header for published post', 7, function testPublishedHeader(test) {
// Make sure we have at least one post in the published state
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
// Open the publish options menu;
casper.thenClick('.js-publish-splitbutton .options.up');
// Select the publish post button
casper.thenClick('.js-publish-splitbutton li[data-set-status="published"]');
casper.waitForSelectorTextChange('.js-publish-button', function onSuccess() {
this.click('.js-publish-button');
}, function onTimeout() {
test.assert(false, 'publish button did not change to published');
});
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
test.assertExists('.notification-success', 'got success notification');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
});
// Select first non-draft, non-static post. Should be second in the list
// at this stage of testing.
casper.thenClick('.content-list-content li:nth-child(2) a');
casper.then(function testHeader() {
test.assertSelectorHasText('.content-preview header .status', 'Published', 'preview header contains "Published" when post is published');
test.assertSelectorHasText('.content-preview header .author', newUser.name, 'preview header contains author name');
});
});
CasperTest.begin('Delete post modal', 9, function testDeleteModal(test) {
// Create a post that can be deleted
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
test.assertExists('.notification-success', 'got success notification');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
// Test cancel delete
casper.thenClick('.content-preview a.post-settings');
casper.thenClick('.post-settings-menu a.delete');
casper.waitUntilVisible('#modal-container', function onSuccess() {
test.assertSelectorHasText(
'.modal-content .modal-header',
'Are you sure you want to delete this post?',
'delete modal has correct text');
});
casper.thenClick('.js-button-reject');
casper.waitWhileVisible("#modal-container", function onSuccess() {
test.assert(true, "clicking cancel should close the delete post modal");
});
// Test delete
casper.thenClick('.content-preview a.post-settings');
casper.thenClick('.post-settings-menu a.delete');
casper.waitForSelector('#modal-container .modal-content', function onSuccess() {
test.assertExists('.modal-content .js-button-accept', 'delete button exists');
// Delete the post
this.click('.modal-content .js-button-accept');
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification from delete post');
test.assertSelectorHasText('.notification-success', 'Your post has been deleted.');
}, function onTimeout() {
test.fail('No success notification from delete post');
});
});
});
CasperTest.begin('Infinite scrolling', 1, function suite(test) {
// Placeholder for infinite scrolling/pagination tests (will need to setup 16+ posts).
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
});
CasperTest.begin("Posts can be marked as featured", 12, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + "ghost/content/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
// Mark as featured
casper.waitForSelector('.content-preview .unfeatured', function () {
this.click('.content-preview .unfeatured');
}, function onTimeOut() {
test.assert(false, 'The first post can\'t be marked as featured');
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Post successfully marked as featured.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
casper.waitForSelector('.content-list-content li:first-child .featured', function () {
test.assertExists('.content-preview .featured');
test.assert(true, 'got a featured star');
this.click('.notification-success .close');
}, function onTimeout() {
test.assert(false, 'No featured star appeared in the left pane');
});
// Mark as not featured
casper.waitWhileSelector('.notification-success', function waitForNoSuccess() {
this.click('.content-preview .featured');
}, function onTimeout() {
test.assert(false, 'Success notification wont go away:(');
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Post successfully marked as not featured.');
test.assertDoesntExist('.content-preview .featured');
test.assertDoesntExist('.content-list-content li:first-child .featured');
}, function onTimeout() {
test.assert(false, 'Success notification wont go away:(');
});
});
CasperTest.begin("Posts with tags can be marked as featured", 12, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
casper.sendKeys('#entry-tags input.tag-input', 'TestTag');
casper.sendKeys('#entry-tags input.tag-input', casper.page.event.key.Enter);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + "ghost/content/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
// Mark as featured
casper.waitForSelector('.content-preview .unfeatured', function () {
this.click('.content-preview .unfeatured');
}, function onTimeOut() {
test.assert(false, 'The first post can\'t be marked as featured');
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Post successfully marked as featured.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
casper.waitForSelector('.content-list-content li:first-child .featured', function () {
test.assertExists('.content-preview .featured');
test.assert(true, 'got a featured star');
this.click('.notification-success .close');
}, function onTimeout() {
test.assert(false, 'No featured star appeared in the left pane');
});
// Mark as not featured
casper.waitWhileSelector('.notification-success', function waitForNoSuccess() {
this.click('.content-preview .featured');
}, function onTimeout() {
test.assert(false, 'Success notification wont go away:(');
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Post successfully marked as not featured.');
test.assertDoesntExist('.content-preview .featured');
test.assertDoesntExist('.content-list-content li:first-child .featured');
}, function onTimeout() {
test.assert(false, 'Success notification wont go away:(');
});
});
CasperTest.begin('Post url can be changed', 9, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change permalink
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#url': 'new-url'
}, false);
this.click('a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Permalink successfully changed to new-url.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success', function () {
test.assert(true, 'notification cleared.');
test.assertNotVisible('.notification-success', 'success notification should not still exist');
});
});
CasperTest.begin('Post url can be changed on Posts with tags', 9, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
casper.sendKeys('#entry-tags input.tag-input', 'TestTag');
casper.sendKeys('#entry-tags input.tag-input', casper.page.event.key.Enter);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change permalink
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#url': 'new-url-with-tags'
}, false);
this.click('a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Permalink successfully changed to new-url-with-tags.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success', function () {
test.assert(true, 'notification cleared.');
test.assertNotVisible('.notification-success', 'success notification should not still exist');
});
});
CasperTest.begin('Post published date can be changed', 9, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change published date
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#pub-date': '22 May 14 @ 23:39'
}, false);
this.click('a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Publish date successfully changed to 22 May 14 @ 23:39.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success', function () {
test.assert(true, 'notification cleared.');
test.assertNotVisible('.notification-success', 'success notification should not still exist');
});
});
CasperTest.begin('Post published date can be changed on Posts with tags', 9, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
casper.sendKeys('#entry-tags input.tag-input', 'TestTag');
casper.sendKeys('#entry-tags input.tag-input', casper.page.event.key.Enter);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change published date
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#pub-date': '21 May 14 @ 23:39'
}, false);
this.click('a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Publish date successfully changed to 21 May 14 @ 23:39.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success', function () {
test.assert(true, 'notification cleared.');
test.assertNotVisible('.notification-success', 'success notification should not still exist');
});
});
CasperTest.begin('Post can be changed to static page', 8, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change to static page
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
casper.thenClick('.post-settings-menu #static-page');
var staticPageConversionText = "Successfully converted to static page.";
casper.waitForText(staticPageConversionText, function onSuccess() {
test.assertSelectorHasText(
'.notification-success', staticPageConversionText, 'correct static page conversion notification appears');
});
casper.thenClick('.post-settings-menu #static-page');
var postConversionText = 'Successfully converted to post.';
casper.waitForText(postConversionText, function onSuccess() {
test.assertSelectorHasText(
'.notification-success', postConversionText, 'correct post conversion notification appears');
});
});
CasperTest.begin('Post with tags can be changed to static page', 8, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
casper.sendKeys('#entry-tags input.tag-input', 'TestTag');
casper.sendKeys('#entry-tags input.tag-input', casper.page.event.key.Enter);
});
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
// Begin test
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
// Test change to static page
casper.thenClick('a.post-settings');
casper.waitUntilVisible('.post-settings-menu', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
casper.thenClick('.post-settings-menu #static-page');
var staticPageConversionText = "Successfully converted to static page.";
casper.waitForText(staticPageConversionText, function onSuccess() {
test.assertSelectorHasText(
'.notification-success', staticPageConversionText, 'correct static page conversion notification appears');
});
casper.thenClick('.post-settings-menu #static-page');
var postConversionText = 'Successfully converted to post.';
casper.waitForText(postConversionText, function onSuccess() {
test.assertSelectorHasText(
'.notification-success', postConversionText, 'correct post conversion notification appears');
});
});
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
});
casper.then(function testNavItems() {
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
test.assertExists('#main-menu li.content a', 'Content nav item exists');
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/', 'Content href is correct');
test.assertEval(function testContentIsNotActive() {
return document.querySelector('#main-menu li.content').classList.contains('active');
}, 'Content nav item is marked active');
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/', 'Editor href is correct');
test.assertEval(function testEditorIsNotActive() {
return !document.querySelector('#main-menu li.editor').classList.contains('active');
}, 'Editor nav item is not marked active');
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/', 'Settings href is correct');
test.assertEval(function testSettingsIsActive() {
return !document.querySelector('#main-menu li.settings').classList.contains('active');
}, 'Settings nav item is not marked active');
});
casper.then(function testUserMenuNotVisible() {
test.assertExists('#usermenu', 'User menu nav item exists');
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
});
casper.thenClick('#usermenu a');
casper.waitForSelector('#usermenu ul.overlay', function then() {
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile', 'Profile menu item has correct text');
test.assertEquals(this.getElementAttribute('li.usermenu-profile a', 'href'), '/ghost/settings/user/', 'Profile href is correct');
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
test.assertEquals(this.getElementAttribute('li.usermenu-help a', 'href'), 'http://support.ghost.org/', 'Help href is correct');
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Sign Out menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/', 'Sign Out href is correct');
});
});

View File

@ -1,596 +0,0 @@
/*globals casper, __utils__, url, testPost */
var escapedUrl = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
CasperTest.begin("Ghost editor is correct", 10, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
test.assertExists(".entry-markdown", "Ghost editor is present");
test.assertExists(".entry-preview", "Ghost preview is present");
});
// test saving with no data
casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Save without title results in error notification as expected');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'Save without title did not result in an error notification');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct.');
}, function onTimeout() {
test.assert('false', 'markdown did not re-render');
});
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*");
test.assertUrlMatch(urlRegExp, 'got an id on our URL');
test.assertExists('.notification-success', 'got success notification');
test.assertEvalEquals(function () {
return document.querySelector('#entry-title').value;
}, testPost.title, 'Title is correct');
}, function onTimeout() {
test.assert('false', 'post was not created');
});
});
CasperTest.begin("Markdown in editor works", 3, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function testImage() {
casper.writeContentToCodeMirror("![sometext]()");
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertEvalEquals(function () {
return document.querySelector('.CodeMirror-wrap textarea').value;
}, '![sometext]()', 'Editor value is correct');
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'Add image of sometext', 'Editor value is correct');
}, function onTimeout() {
test.assert('false', 'markdown did not re-render');
});
});
CasperTest.begin("Word count and plurality", 4, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function checkZeroPlural() {
test.assertSelectorHasText('.entry-word-count', '0 words', 'count of 0 produces plural "words".');
});
casper.then(function () {
casper.writeContentToCodeMirror('test');
});
casper.waitForSelectorTextChange('.entry-word-count', function onSuccess() {
test.assertSelectorHasText('.entry-word-count', '1 word', 'count of 1 produces singular "word".');
});
casper.then(function () {
casper.writeContentToCodeMirror('test'); // append another word, assumes newline
});
casper.waitForSelectorTextChange('.entry-word-count', function onSuccess() {
test.assertSelectorHasText('.entry-word-count', '2 words', 'count of 2 produces plural "words".');
});
});
CasperTest.begin("Image Uploads", 14, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
// Test standard image upload modal
casper.then(function () {
casper.writeContentToCodeMirror("![]()");
});
function assertEmptyImageUploaderDisplaysCorrectly() {
test.assertExists(".entry-preview .js-upload-target", "Upload target exists");
test.assertExists(".entry-preview .js-fileupload", "File upload target exists");
test.assertExists(".entry-preview .image-url", "Image URL button exists");
};
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly);
// Test image URL upload modal
casper.thenClick(".entry-preview .image-uploader a.image-url");
casper.waitForSelector(".image-uploader-url", function onSuccess() {
test.assertExists(".image-uploader-url .url.js-upload-url", "Image URL uploader exists")
test.assertExists(".image-uploader-url .button-save.js-button-accept", "Image URL accept button exists")
test.assertExists(".image-uploader-url .image-upload", "Back to normal image upload style button exists")
});
// Test image source location
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
var testFileLocation = "test/file/location";
casper.then(function () {
var markdownImageString = "![](" + testFileLocation + ")";
casper.writeContentToCodeMirror(markdownImageString);
});
casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() {
var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + testFileLocation + "']"
test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to source location");
});
// Test cancel image button
casper.thenClick(".pre-image-uploader a.image-cancel.js-cancel");
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly);
// Test image url source location
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function () {
casper.writeContentToCodeMirror("![]()");
});
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", function onSuccess() {
casper.thenClick(".entry-preview .image-uploader a.image-url");
});
var imageURL = "random.url";
casper.waitForSelector(".image-uploader-url", function onSuccess() {
casper.sendKeys(".image-uploader-url input.url.js-upload-url", imageURL);
casper.thenClick(".js-button-accept.button-save");
});
casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() {
var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + imageURL + "']"
test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to inputted image URL");
});
});
CasperTest.begin('Required Title', 4, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#entry-title', function then() {
test.assertEvalEquals(function() {
return document.getElementById('entry-title').value;
}, '', 'Title is empty');
});
casper.thenClick('.js-publish-button'); // Safe to assume draft mode?
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'must specify a title');
}, function onTimeout() {
test.fail('Title required error did not appear');
}, 2000);
});
CasperTest.begin('Title Trimming', 2, function suite(test) {
var untrimmedTitle = ' test title ',
trimmedTitle = 'test title';
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
});
casper.then(function populateTitle() {
casper.sendKeys('#entry-title', untrimmedTitle);
test.assertEvalEquals(function () {
return $('#entry-title').val();
}, trimmedTitle, 'Entry title should match expected value.');
});
});
CasperTest.begin("Tag editor", 6, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
var tagName = "someTagName";
casper.then(function () {
test.assertExists("#entry-tags", "should have tag label area");
test.assertExists("#entry-tags .tag-label", "should have tag label icon");
test.assertExists("#entry-tags input.tag-input", "should have tag input area");
casper.sendKeys("#entry-tags input.tag-input", tagName);
casper.sendKeys("#entry-tags input.tag-input", casper.page.event.key.Enter);
});
var createdTagSelector = "#entry-tags .tags .tag";
casper.waitForSelector(createdTagSelector, function onSuccess() {
test.assertSelectorHasText(createdTagSelector, tagName, "typing enter after tag name should create tag");
});
casper.thenClick(createdTagSelector);
casper.waitWhileSelector(createdTagSelector, function onSuccess() {
test.assert(true, "clicking the tag should delete the tag");
});
});
CasperTest.begin("Post settings menu", 28, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function () {
test.assertExists("#publish-bar a.post-settings", "icon toggle should exist");
test.assertNotVisible("#publish-bar .post-settings-menu", "popup menu should not be visible at startup");
test.assertExists(".post-settings-menu input#url", "url field exists");
test.assertExists(".post-settings-menu input#pub-date", "publication date field exists");
test.assertExists(".post-settings-menu input#static-page", "static page checkbox field exists");
test.assertExists(".post-settings-menu a.delete", "delete post button exists")
});
casper.thenClick("#publish-bar a.post-settings");
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "popup menu should be visible after clicking post-settings icon");
test.assertNotVisible(".post-settings-menu a.delete", "delete post btn shouldn't be visible on unsaved drafts");
});
casper.thenClick("#publish-bar a.post-settings");
casper.waitWhileVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "popup menu should not be visible after clicking post-settings icon");
});
// Enter a title and save draft so converting to/from static post
// will result in notifications and 'Delete This Post' button appears
casper.then(function (){
casper.sendKeys("#entry-title", "aTitle");
casper.thenClick(".js-publish-button");
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Your post has been saved as a draft.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success');
casper.thenClick("#publish-bar a.post-settings");
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "post settings menu should be visible after clicking post-settings icon");
});
casper.waitUntilVisible(".post-settings-menu a.delete", function onSuccess() {
test.assert(true, "delete post button should be visible for saved drafts");
});
// Test change permalink
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#url': 'new-url-editor'
}, false);
this.click('#publish-bar a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Permalink successfully changed to new-url-editor.');
casper.click('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success', function () {
test.assert(true, 'notification cleared.');
test.assertNotVisible('.notification-success', 'success notification should not still exist');
});
// Test change pub date
casper.thenClick('#publish-bar a.post-settings');
casper.waitUntilVisible('#publish-bar .post-settings-menu #pub-date', function onSuccess() {
test.assert(true, 'post settings menu should be visible after clicking post-settings icon');
});
casper.then(function () {
this.fillSelectors('.post-settings-menu form', {
'#pub-date': '10 May 14 @ 00:17'
}, false);
this.click('#publish-bar a.post-settings')
});
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Publish date successfully changed to 10 May 14 @ 00:17.');
casper.thenClick('.notification-success a.close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success');
// Test Static Page conversion
casper.thenClick("#publish-bar a.post-settings");
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "post settings menu should be visible after clicking post-settings icon");
});
casper.thenClick(".post-settings-menu #static-page");
var staticPageConversionText = "Successfully converted to static page.";
casper.waitForText(staticPageConversionText, function onSuccess() {
test.assertSelectorHasText(
".notification-success", staticPageConversionText, "correct static page conversion notification appears");
});
casper.thenClick(".post-settings-menu #static-page");
var postConversionText = "Successfully converted to post.";
casper.waitForText(postConversionText, function onSuccess() {
test.assertSelectorHasText(
".notification-success", postConversionText, "correct post conversion notification appears");
});
// Test Delete Post Modal
casper.thenClick(".post-settings-menu a.delete");
casper.waitUntilVisible("#modal-container", function onSuccess() {
test.assert(true, "delete post modal is visible after clicking delete");
test.assertSelectorHasText(
"#modal-container .modal-header",
"Are you sure you want to delete this post?",
"delete post modal header has correct text");
});
casper.thenClick("#modal-container .js-button-reject");
casper.waitWhileVisible("#modal-container", function onSuccess() {
test.assert(true, "clicking cancel should close the delete post modal");
});
casper.thenClick("#publish-bar a.post-settings");
casper.thenClick(".post-settings-menu a.delete");
casper.waitUntilVisible("#modal-container", function onSuccess() {
casper.thenClick("#modal-container .js-button-accept");
});
casper.waitForUrl(/ghost\/content\/$/, function onSuccess() {
test.assert(true, "clicking the delete post button should bring us to the content page");
});
});
CasperTest.begin('Publish menu - new post', 10, function suite(test) {
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
});
// ... check default option status, label, class
casper.then(function () {
test.assertExists('.js-publish-splitbutton');
test.assertExists('.js-publish-splitbutton.splitbutton-save');
test.assertExists('.js-publish-button');
test.assertExists('.js-publish-button.button-save');
test.assertSelectorHasText('.js-publish-button', 'Save Draft');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'draft');
}, 'Publish button\'s initial status should be "draft"');
});
casper.then(function () {
// ... click the menu
this.click('.js-publish-splitbutton .options.up');
// ... click publish
this.click('.js-publish-splitbutton li[data-set-status="published"]');
});
// ... check status, label, class
casper.waitForSelector('.js-publish-splitbutton.splitbutton-delete', function onSuccess() {
test.assertExists('.js-publish-button.button-delete', 'Publish button should have .button-delete');
test.assertSelectorHasText('.js-publish-button', 'Publish Now');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'published');
}, 'Publish button\'s updated status should be "published"');
}, function onTimeout() {
test.assert(false, 'Publish split button should have .splitbutton-delete');
});
});
CasperTest.begin('Publish menu - existing post', 22, function suite(test) {
// Create a post, save it and test refreshed editor
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
// Create a post in draft status
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*");
test.assertUrlMatch(urlRegExp, 'got an id on our URL');
});
// ... check option status, label, class now that we're *saved* as 'draft'
casper.then(function () {
test.assertExists('.js-publish-splitbutton');
test.assertExists('.js-publish-splitbutton.splitbutton-save');
test.assertExists('.js-publish-button');
test.assertExists('.js-publish-button.button-save');
test.assertSelectorHasText('.js-publish-button', 'Save Draft');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'draft');
}, 'Publish button\'s initial status should be "draft"');
});
// Open the publish options menu;
casper.thenClick('.js-publish-splitbutton .options.up');
// Select the publish post button
casper.thenClick('.js-publish-splitbutton li[data-set-status="published"]');
// ... check status, label, class
casper.waitForSelector('.js-publish-splitbutton.splitbutton-delete', function onSuccess() {
test.assertExists('.js-publish-button.button-delete', 'Publish button should have .button-delete');
test.assertSelectorHasText('.js-publish-button', 'Publish Now');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'published');
}, 'Publish button\'s updated status should be "published"');
}, function onTimeout() {
test.assert(false, 'Publish split button should have .splitbutton-delete');
});
// Publish the post
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() {
var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*");
test.assertUrlMatch(urlRegExp, 'got an id on our URL');
});
// ... check option status, label, class for saved as 'published'
casper.then(function () {
test.assertExists('.js-publish-splitbutton');
test.assertExists('.js-publish-splitbutton.splitbutton-save');
test.assertExists('.js-publish-button');
test.assertExists('.js-publish-button.button-save');
test.assertSelectorHasText('.js-publish-button', 'Update Post');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'published');
}, 'Publish button\'s initial status on an already published post should be "published"');
});
// Open the publish options menu
casper.thenClick('.js-publish-splitbutton .options.up');
casper.waitForOpaque('.js-publish-splitbutton .editor-options.overlay', function onSuccess() {
// Click the 'unpublish' option
casper.thenClick('.js-publish-splitbutton li[data-set-status="draft"]');
}, function onTimeout() {
test.assert(false, 'Publish split button menu should have opened');
});
// ... check status, label, class
casper.waitForSelector('.js-publish-splitbutton.splitbutton-delete', function onSuccess() {
test.assertExists('.js-publish-button.button-delete', 'Publish button should have .button-delete');
test.assertSelectorHasText('.js-publish-button', 'Unpublish');
test.assertEval(function () {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'draft');
}, 'Publish button\'s updated status should be "draft"');
}, function onTimeout() {
test.assert(false, 'Publish split button should have .splitbutton-delete');
});
});
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
});
casper.then(function testNavItems() {
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
test.assertExists('#main-menu li.content a', 'Content nav item exists');
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/', 'Content href is correct');
test.assertEval(function testContentIsNotActive() {
return !document.querySelector('#main-menu li.content').classList.contains('active');
}, 'Content nav item is not marked active');
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/', 'Editor href is correct');
test.assertEval(function testEditorIsNotActive() {
return document.querySelector('#main-menu li.editor').classList.contains('active');
}, 'Editor nav item is marked active');
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/', 'Settings href is correct');
test.assertEval(function testSettingsIsActive() {
return !document.querySelector('#main-menu li.settings').classList.contains('active');
}, 'Settings nav item is not marked active');
});
casper.then(function testUserMenuNotVisible() {
test.assertExists('#usermenu', 'User menu nav item exists');
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
});
casper.thenClick('#usermenu a');
casper.waitForSelector('#usermenu ul.overlay', function then() {
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile', 'Profile menu item has correct text');
test.assertEquals(this.getElementAttribute('li.usermenu-profile a', 'href'), '/ghost/settings/user/', 'Profile href is correct');
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-help a', 'href'), 'http://support.ghost.org/', 'Help href is correct');
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Sign Out menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/', 'Sign Out href is correct');
});
});
// test the markdown help modal
CasperTest.begin('Markdown help modal', 4, function suite(test) {
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
// open markdown help modal
casper.thenClick('a.markdown-help');
casper.waitUntilVisible('#modal-container', function onSuccess() {
test.assertSelectorHasText(
'.modal-content .modal-header',
'Markdown Help',
'delete modal has correct text');
test.assertExists('.modal-content .close');
});
casper.thenClick('.modal-content .close');
casper.waitWhileVisible('#modal-container', function onSuccess() {
test.assert(true, 'clicking close should remove the markdown help modal');
});
});

View File

@ -1,60 +0,0 @@
/**
* Tests the flow of creating, editing and publishing tests
*/
/*globals casper, __utils__, url, testPost */
CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function then() {
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/\?include=tags$/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
casper.thenOpen(url + 'ghost/content/', function then() {
test.assertUrlMatch(/ghost\/content\//, "Ghost successfully loaded the content page");
});
casper.then(function then() {
test.assertEvalEquals(function () {
return document.querySelector('.content-list-content li').className;
}, "active", "first item is active");
test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "first item is the post we created");
});
casper.thenClick('.post-edit').waitForResource(/editor/, function then() {
test.assertUrlMatch(/editor/, "Ghost successfully loaded the editor page again");
});
casper.thenClick('.js-publish-button');
casper.waitForResource(/posts\/[0-9]+\/\?include=tags$/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
});
// TODO: test publishing, editing, republishing, unpublishing etc
//CasperTest.begin("Ghost edit published flow works correctly", 6, function suite(test) {
//
//
//
//});

View File

@ -1,153 +0,0 @@
/*globals casper, __utils__, url, newUser, user, falseUser */
CasperTest.begin('Ensure Session is Killed', 1, function suite(test) {
casper.thenOpen(url + 'logout/', function (response) {
test.assertUrlMatch(/ghost\/sign/, 'We got redirected to signin or signup page');
});
}, true);
CasperTest.begin('Ensure a User is Registered', 2, function suite(test) {
casper.thenOpen(url + 'ghost/signup/');
casper.waitForOpaque(".signup-box",
function then() {
this.fill("#signup", newUser, true);
},
function onTimeout() {
test.fail('Sign up form didn\'t fade in.');
});
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'already registered');
// If the previous assert succeeds, then we should skip the next check and just pass.
casper.echoConcise('Already registered!');
}, function onTimeout() {
test.assertUrlMatch(/\/ghost\/$/, 'If we\'re not already registered, we should be logged in.');
casper.echoConcise('Successfully registered.');
}, 2000);
casper.thenOpen(url + 'logout/', function then() {
test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page.');
});
}, true);
CasperTest.begin("Ghost admin will load login page", 3, function suite(test) {
casper.thenOpen(url + "ghost", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/signin\/$/, 'We should be presented with the signin page.');
casper.then(function testLink() {
var link = this.evaluate(function (selector) {
return document.querySelector(selector).getAttribute('href');
}, '.forgotten-password');
casper.echoConcise('LINK' + link);
test.assert(link === '/ghost/forgotten/', 'Has correct forgotten password link');
});
});
}, true);
CasperTest.begin('Redirects login to signin', 2, function suite(test) {
casper.start(url + 'ghost/login/', function testRedirect(response) {
test.assertEqual(response.status, 200, 'Response status should be 200.');
test.assertUrlMatch(/ghost\/signin\//, 'Should be redirected to /signin/.');
});
}, true);
CasperTest.begin("Can't spam it", 3, function suite(test) {
casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.waitForOpaque(".login-box",
function then() {
this.fill("#login", falseUser, true);
},
function onTimeout() {
test.fail('Sign in form didn\'t fade in.');
});
casper.wait(200, function doneWait() {
this.fill("#login", falseUser, true);
});
casper.waitForText('Slow down, there are way too many login attempts!', function onSuccess() {
test.assert(true, 'Spamming the login did result in an error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'Spamming the login did not result in an error notification');
});
// This test causes the spam notification
// add a wait to ensure future tests don't get tripped up by this.
casper.wait(2000);
}, true);
CasperTest.begin("Login limit is in place", 3, function suite(test) {
casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.waitForOpaque(".login-box",
function then() {
this.fill("#login", falseUser, true);
},
function onTimeout() {
test.fail('Sign in form didn\'t fade in.');
});
casper.wait(2100, function doneWait() {
this.fill("#login", falseUser, true);
});
casper.waitForText('remaining', function onSuccess() {
test.assert(true, 'The login limit is in place.');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'We did not trip the login limit.');
});
// This test used login, add a wait to
// ensure future tests don't get tripped up by this.
casper.wait(2000);
}, true);
CasperTest.begin("Can login to Ghost", 4, function suite(test) {
casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.waitForOpaque(".login-box",
function then() {
this.fill("#login", user, true);
});
casper.waitForResource(/ghost\/$/, function testForDashboard() {
test.assertUrlMatch(/ghost\/$/, 'We got redirected to the Ghost page');
test.assertExists("#global-header", "Global admin header is present");
test.assertExists(".manage", "We're now on content");
}, function onTimeOut() {
test.fail('Failed to load ghost/ resource');
});
}, true);
CasperTest.begin('Ensure email field form validation', 1, function suite(test) {
casper.thenOpen(url + 'ghost/signin/');
casper.waitForOpaque(".js-login-box",
function then() {
this.fill("form.login-form", {
'email': 'notanemail'
}, true);
},
function onTimeout() {
test.fail('Login form didn\'t fade in.');
});
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'Invalid Email');
}, function onTimeout() {
test.fail('Email validation error did not appear');
}, 2000);
}, true);

View File

@ -1,92 +0,0 @@
/**
* Tests logging out and attempting to sign up
*/
/*globals casper, __utils__, url, testPost, falseUser, email */
CasperTest.begin("Ghost logout works correctly", 2, function suite(test) {
CasperTest.Routines.oldRegister.run(test);
CasperTest.Routines.oldLogout.run(test);
CasperTest.Routines.oldLogin.run(test);
casper.thenOpen(url + "ghost/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/", "Ghost doesn't require login this time");
});
casper.thenClick('#usermenu a').waitFor(function checkOpaque() {
return this.evaluate(function () {
var loginBox = document.querySelector('#usermenu .overlay.open');
return window.getComputedStyle(loginBox).getPropertyValue('display') === "block"
&& window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1";
});
});
casper.waitForSelector('.usermenu-signout a');
casper.thenClick('.usermenu-signout a');
casper.waitForResource(/ghost\/signin/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
}, true);
// has to be done after signing out
CasperTest.begin("Can't spam signin", 3, function suite(test) {
casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.waitFor(function checkOpaque() {
return this.evaluate(function () {
var loginBox = document.querySelector('.login-box');
return window.getComputedStyle(loginBox).getPropertyValue('display') === "table"
&& window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1";
});
}, function then() {
this.fill("#login", falseUser, true);
casper.wait(200, function doneWait() {
this.fill("#login", falseUser, true);
});
});
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
}, true);
CasperTest.begin("Ghost signup fails properly", 5, function suite(test) {
casper.thenOpen(url + "ghost/signup/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/signup/", "Reached signup page");
});
casper.then(function signupWithShortPassword() {
this.fill("#signup", {email: email, password: 'test'}, true);
});
// should now throw a short password error
casper.waitForResource(/signup/);
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
casper.then(function signupWithLongPassword() {
this.fill("#signup", {email: email, password: 'testing1234'}, true);
});
// should now throw a 1 user only error
casper.waitForResource(/signup/);
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
}, true);

View File

@ -1,418 +0,0 @@
/*globals casper, CasperTest, url */
CasperTest.begin('Settings screen is correct', 20, function suite(test) {
casper.thenOpen(url + 'ghost/settings/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.then(function testViews() {
test.assertExists('.wrapper', 'Settings main view is present');
test.assertExists('.settings-sidebar', 'Settings sidebar view is present');
test.assertExists('.settings-menu', 'Settings menu is present');
test.assertExists('.settings-menu .general', 'General tab is present');
test.assertExists('.settings-menu .users', 'Users tab is present');
test.assertExists('.settings-menu .apps', 'Apps is present');
test.assertExists('.wrapper', 'Settings main view is present');
test.assertExists('.settings-content', 'Settings content view is present');
test.assertEval(function testGeneralIsActive() {
return document.querySelector('.settings-menu .general').classList.contains('active');
}, 'general tab is marked active');
test.assertEval(function testContentIsGeneral() {
return document.querySelector('.settings-content').id === 'general';
}, 'loaded content is general screen');
});
// test the user tab
casper.thenClick('.settings-menu .users');
casper.waitForSelector('#user', function then() {
test.assertEval(function testGeneralIsNotActive() {
return !document.querySelector('.settings-menu .general').classList.contains('active');
}, 'general tab is not marked active');
test.assertEval(function testUserIsActive() {
return document.querySelector('.settings-menu .users').classList.contains('active');
}, 'user tab is marked active');
test.assertEval(function testContentIsUser() {
return document.querySelector('.settings-content').id === 'user';
}, 'loaded content is user screen');
}, casper.failOnTimeout(test, 'waitForSelector #user timed out'));
function handleUserRequest(requestData) {
// make sure we only get requests from the user pane
if (requestData.url.indexOf('settings/') !== -1) {
test.fail('Saving the user pane triggered another settings pane to save');
}
}
function handleSettingsRequest(requestData) {
// make sure we only get requests from the user pane
if (requestData.url.indexOf('users/') !== -1) {
test.fail('Saving a settings pane triggered the user pane to save');
}
}
casper.then(function listenForRequests() {
casper.on('resource.requested', handleUserRequest);
});
casper.thenClick('#user .button-save');
casper.waitFor(function successNotification() {
return this.evaluate(function () {
return document.querySelectorAll('.js-bb-notification section').length > 0;
});
}, function doneWaiting() {
test.pass('Waited for notification');
}, casper.failOnTimeout(test, 'Saving the user pane did not result in a notification'));
casper.then(function checkUserWasSaved() {
casper.removeListener('resource.requested', handleUserRequest);
});
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification :('));
casper.thenClick('#main-menu .settings a').then(function testOpeningSettingsTwice() {
casper.on('resource.requested', handleSettingsRequest);
test.assertEval(function testUserIsActive() {
return document.querySelector('.settings-menu .general').classList.contains('active');
}, 'general tab is marked active');
});
casper.thenClick('#general .button-save').waitFor(function successNotification() {
return this.evaluate(function () {
return document.querySelectorAll('.js-bb-notification section').length > 0;
});
}, function doneWaiting() {
test.pass('Waited for notification');
}, casper.failOnTimeout(test, 'Saving the general pane did not result in a notification'));
casper.then(function checkSettingsWereSaved() {
casper.removeListener('resource.requested', handleSettingsRequest);
});
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification :('));
CasperTest.beforeDone(function () {
casper.removeListener('resource.requested', handleUserRequest);
casper.removeListener('resource.requested', handleSettingsRequest);
});
});
CasperTest.begin('Ensure general blog title field length validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#general', function then() {
this.fill('form#settings-general', {
'general[title]': new Array(152).join('a')
});
}, casper.failOnTimeout(test, 'waitForSelector #general timed out'));
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'too long');
}, casper.failOnTimeout(test, 'Blog title length error did not appear'), 2000);
});
CasperTest.begin('Ensure general blog description field length validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#general', function then() {
this.fillSelectors('form#settings-general', {
'#blog-description': new Array(202).join('a')
});
}, casper.failOnTimeout(test, 'waitForSelector #general timed out'));
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'too long');
}, casper.failOnTimeout(test, 'Blog description length error did not appear'));
});
CasperTest.begin('Ensure image upload modals display correctly', 6, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
function assertImageUploaderModalThenClose() {
test.assertSelectorHasText('.description', 'Add image');
this.click('#modal-container .js-button-accept');
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification'));
}
// Test Blog Logo Upload Button
casper.waitForOpaque('#general', function then() {
this.click('#general .js-modal-logo');
}, casper.failOnTimeout(test, 'waitForOpaque #general timed out'));
casper.waitForSelector('#modal-container .modal-content .js-drop-zone .description', assertImageUploaderModalThenClose,
casper.failOnTimeout(test, 'No upload logo modal container appeared'));
// Test Blog Cover Upload Button
casper.waitForOpaque('#general', function then() {
this.click('#general .js-modal-cover');
}, casper.failOnTimeout(test, 'waitForOpaque #general timed out'));
casper.waitForSelector('#modal-container .modal-content .js-drop-zone .description', assertImageUploaderModalThenClose,
casper.failOnTimeout(test, 'No upload cover modal container appeared'));
});
CasperTest.begin('User settings screen validates email', 6, function suite(test) {
var email, brokenEmail;
casper.thenOpen(url + 'ghost/settings/user/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
});
casper.then(function setEmailToInvalid() {
email = casper.getElementInfo('#user-email').attributes.value;
brokenEmail = email.replace('.', '-');
casper.fillSelectors('.user-profile', {
'#user-email': brokenEmail
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource('/users/');
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, casper.failOnTimeout(test, 'No error notification :('));
casper.then(function resetEmailToValid() {
casper.fillSelectors('.user-profile', {
'#user-email': email
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource(/users/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
test.assertSelectorDoesntHaveText('.notification-success', '[object Object]');
}, casper.failOnTimeout(test, 'No success notification :('));
});
CasperTest.begin('Ensure postsPerPage number field form validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#general', function then() {
this.fill('form#settings-general', {
'general[postsPerPage]': 'notaninteger'
});
}, casper.failOnTimeout(test, 'waitForSelector #general timed out'));
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number');
}, casper.failOnTimeout(test, 'postsPerPage error did not appear'), 2000);
});
CasperTest.begin('Ensure postsPerPage max of 1000', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#general', function then() {
this.fill('form#settings-general', {
'general[postsPerPage]': '1001'
});
}, casper.failOnTimeout(test, 'waitForSelector #general timed out'));
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number less than 1000');
}, casper.failOnTimeout(test, 'postsPerPage max error did not appear', 2000));
});
CasperTest.begin('Ensure postsPerPage min of 0', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/general/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#general', function then() {
this.fill('form#settings-general', {
'general[postsPerPage]': '-1'
});
}, casper.failOnTimeout(test, 'waitForSelector #general timed out'));
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number greater than 0');
}, casper.failOnTimeout(test, 'postsPerPage min error did not appear', 2000));
});
CasperTest.begin('User settings screen shows remaining characters for Bio properly', 4, function suite(test) {
function getRemainingBioCharacterCount() {
return casper.getHTML('.word-count');
}
casper.thenOpen(url + 'ghost/settings/user/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
});
casper.then(function checkCharacterCount() {
test.assert(getRemainingBioCharacterCount() === '200', 'Bio remaining characters is 200');
});
casper.then(function setBioToValid() {
casper.fillSelectors('.user-profile', {
'#user-bio': 'asdf\n' // 5 characters
}, false);
});
casper.then(function checkCharacterCount() {
test.assert(getRemainingBioCharacterCount() === '195', 'Bio remaining characters is 195');
});
});
CasperTest.begin('Ensure user bio field length validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/user/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#user', function then() {
this.fillSelectors('form.user-profile', {
'#user-bio': new Array(202).join('a')
});
}, casper.failOnTimeout(test, 'waitForSelector #user timed out'));
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'is too long');
}, casper.failOnTimeout(test, 'Bio field length error did not appear', 2000));
});
CasperTest.begin('Ensure user url field validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/user/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#user', function then() {
this.fillSelectors('form.user-profile', {
'#user-website': 'notaurl'
});
}, casper.failOnTimeout(test, 'waitForSelector #user timed out'));
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a valid url');
}, casper.failOnTimeout(test, 'Url validation error did not appear', 2000));
});
CasperTest.begin('Ensure user location field length validation', 3, function suite(test) {
casper.thenOpen(url + 'ghost/settings/user/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/user\/$/, 'Ghost doesn\'t require login this time');
});
casper.waitForSelector('#user', function then() {
this.fillSelectors('form.user-profile', {
'#user-location': new Array(1002).join('a')
});
}, casper.failOnTimeout(test, 'waitForSelector #user timed out'));
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'is too long');
}, casper.failOnTimeout(test, 'Location field length error did not appear', 2000));
});
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
casper.thenOpen(url + 'ghost/settings/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Ghost doesn\'t require login this time');
});
casper.then(function testNavItems() {
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
test.assertExists('#main-menu li.content a', 'Content nav item exists');
test.assertSelectorHasText('#main-menu li.content a', 'Content',
'Content nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/',
'Content href is correct');
test.assertEval(function testContentIsNotActive() {
return !document.querySelector('#main-menu li.content').classList.contains('active');
}, 'Content nav item is not marked active');
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/',
'Editor href is correct');
test.assertEval(function testEditorIsNotActive() {
return !document.querySelector('#main-menu li.editor').classList.contains('active');
}, 'Editor nav item is not marked active');
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/',
'Settings href is correct');
test.assertEval(function testSettingsIsActive() {
return document.querySelector('#main-menu li.settings').classList.contains('active');
}, 'Settings nav item is marked active');
});
casper.then(function testUserMenuNotVisible() {
test.assertExists('#usermenu', 'User menu nav item exists');
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
});
casper.thenClick('#usermenu a');
casper.waitForSelector('#usermenu ul.overlay', function then() {
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile',
'Profile menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-profile a', 'href'), '/ghost/settings/user/',
'Profile href is correct');
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-help a', 'href'), 'http://support.ghost.org/',
'Help href is correct');
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out',
'Sign Out menu item has correct text');
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/',
'Sign Out href is correct');
}, casper.failOnTimeout(test, 'WaitForSelector #usermenu ul.overlay failed'));
});

View File

@ -108,7 +108,7 @@ describe('Admin Routing', function () {
.end(doEndNoAuth(done));
});
});
// we'll use X-Forwarded-Proto: https to simulate an 'https://' request behind a proxy
describe('Require HTTPS - no redirect', function() {
var forkedGhost, request;
@ -125,13 +125,13 @@ describe('Admin Routing', function () {
}).then(done)
.catch(done);
});
after(function (done) {
if (forkedGhost) {
forkedGhost.kill(done);
}
});
it('should block admin access over non-HTTPS', function(done) {
request.get('/ghost/')
.expect(403)
@ -139,12 +139,12 @@ describe('Admin Routing', function () {
});
it('should allow admin access over HTTPS', function(done) {
request.get('/ghost/signup/')
request.get('/ghost/setup/')
.set('X-Forwarded-Proto', 'https')
.expect(200)
.end(doEnd(done));
});
});
});
describe('Require HTTPS - redirect', function() {
var forkedGhost, request;
@ -161,13 +161,13 @@ describe('Admin Routing', function () {
}).then(done)
.catch(done);
});
after(function (done) {
if (forkedGhost) {
forkedGhost.kill(done);
}
});
it('should redirect admin access over non-HTTPS', function(done) {
request.get('/ghost/')
.expect('Location', /^https:\/\/localhost\/ghost\//)
@ -176,34 +176,33 @@ describe('Admin Routing', function () {
});
it('should allow admin access over HTTPS', function(done) {
request.get('/ghost/signup/')
request.get('/ghost/setup/')
.set('X-Forwarded-Proto', 'https')
.expect(200)
.end(done);
});
});
});
describe('Ghost Admin Signup', function () {
describe('Ghost Admin Setup', function () {
// TODO: needs new test for Ember
// it('should redirect from /ghost/ to /ghost/signin/ when no user', function (done) {
// request.get('/ghost/')
// .expect('Location', /ghost\/signin/)
// .expect('Cache-Control', cacheRules['private'])
// .expect(302)
// .end(doEnd(done));
// });
it('should redirect from /ghost/ to /ghost/setup/ when no user/not installed yet', function (done) {
request.get('/ghost/')
.expect('Location', /ghost\/setup/)
.expect('Cache-Control', cacheRules['private'])
.expect(302)
.end(doEnd(done));
});
it('should redirect from /ghost/signin/ to /ghost/signup/ when no user', function (done) {
it('should redirect from /ghost/signin/ to /ghost/setup/ when no user', function (done) {
request.get('/ghost/signin/')
.expect('Location', /ghost\/signup/)
.expect('Location', /ghost\/setup/)
.expect('Cache-Control', cacheRules['private'])
.expect(302)
.end(doEnd(done));
});
it('should respond with html for /ghost/signup/', function (done) {
request.get('/ghost/signup/')
it('should respond with html for /ghost/setup/', function (done) {
request.get('/ghost/setup/')
.expect('Content-Type', /html/)
.expect('Cache-Control', cacheRules['private'])
.expect(200)
@ -232,16 +231,16 @@ describe('Admin Routing', function () {
});
describe('Ghost Admin Forgot Password', function () {
// TODO: new test for Ember where user is added
// describe('Ghost Admin Forgot Password', function () {
// it('should respond with html for /ghost/forgotten/', function (done) {
// request.get('/ghost/forgotten/')
// .expect('Content-Type', /html/)
// .expect('Cache-Control', cacheRules['private'])
// .expect(200)
// .end(doEnd(done));
// });
it('should respond with html for /ghost/forgotten/', function (done) {
request.get('/ghost/forgotten/')
.expect('Content-Type', /html/)
.expect('Cache-Control', cacheRules['private'])
.expect(200)
.end(doEnd(done));
});
// TODO: new test for Ember
// it('should respond 404 for /ghost/reset/', function (done) {
// request.get('/ghost/reset/')
// .expect('Cache-Control', cacheRules['private'])
@ -250,15 +249,15 @@ describe('Admin Routing', function () {
// .end(doEnd(done));
// });
it('should redirect /ghost/reset/*/', function (done) {
request.get('/ghost/reset/athing/')
.expect('Location', /ghost\/forgotten/)
.expect('Cache-Control', cacheRules['private'])
.expect(302)
.end(doEnd(done));
});
});
});
// it('should redirect /ghost/reset/*/', function (done) {
// request.get('/ghost/reset/athing/')
// .expect('Location', /ghost\/forgotten/)
// .expect('Cache-Control', cacheRules['private'])
// .expect(302)
// .end(doEnd(done));
// });
// });
//});
// TODO: not working anymore, needs new test for Ember
// describe('Authenticated Admin Routing', function () {
@ -344,4 +343,4 @@ describe('Admin Routing', function () {
// });
// });
// });
// });
});

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