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?
4
.gitignore
vendored
@ -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
|
||||
|
197
Gruntfile.js
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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)
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 400 B |
Before Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 640 B |
@ -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;
|
||||
} ());
|
@ -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;
|
||||
}());
|
@ -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;
|
||||
} ());
|
@ -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;
|
||||
}());
|
@ -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: {} };
|
||||
|
||||
}
|
||||
}());
|
@ -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;
|
||||
} ());
|
@ -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;
|
||||
}());
|
175
core/clientold/assets/lib/jquery-utils.js
vendored
@ -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('');
|
||||
};
|
||||
|
||||
}());
|
@ -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));
|
224
core/clientold/assets/vendor/shortcuts.js
vendored
@ -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;
|
||||
}
|
||||
};
|
22
core/clientold/assets/vendor/to-title-case.js
vendored
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* To Title Case 2.0.1 – http://individed.com/code/to-title-case/
|
||||
* Copyright © 2008–2012 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);
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
}());
|
@ -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);
|
||||
}());
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
}());
|
@ -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
|
||||
});
|
||||
}());
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
@ -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;
|
||||
}
|
||||
});
|
||||
}());
|
@ -1,9 +0,0 @@
|
||||
/*global Ghost, Backbone */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
Ghost.Models.Themes = Backbone.Model.extend({
|
||||
url: Ghost.paths.apiRoot + '/themes/'
|
||||
});
|
||||
|
||||
}());
|
@ -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/*';
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
@ -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/'
|
||||
// });
|
||||
|
||||
}());
|
@ -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
|
||||
});
|
||||
|
||||
}());
|
@ -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" });
|
||||
}
|
||||
});
|
||||
}());
|
@ -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));
|
||||
});
|
||||
|
||||
}());
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1 +0,0 @@
|
||||
{{{content.text}}}
|
@ -1,4 +0,0 @@
|
||||
Press Ctrl / Cmd + C to copy the following HTML.
|
||||
<pre>
|
||||
<code class="modal-copyToHTML-content"></code>
|
||||
</pre>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
}());
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
@ -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'
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}());
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
}());
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
@ -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; }
|
||||
});
|
||||
}());
|
@ -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;
|
||||
}
|
||||
});
|
||||
}());
|
@ -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>'
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}());
|
@ -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'
|
||||
});
|
||||
|
||||
}());
|
@ -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: [{
|
||||
|
@ -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});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -1,4 +0,0 @@
|
||||
{{!< default}}
|
||||
<section class="forgotten-box js-forgotten-box">
|
||||
|
||||
</section>
|
@ -1,4 +0,0 @@
|
||||
{{!< default}}
|
||||
<section class="login-box js-login-box">
|
||||
|
||||
</section>
|
@ -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>
|
@ -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}}
|
@ -1,4 +0,0 @@
|
||||
{{!< default}}
|
||||
<section class="reset-box js-reset-box">
|
||||
|
||||
</section>
|
@ -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>
|
@ -1,4 +0,0 @@
|
||||
{{!< default}}
|
||||
<section class="signup-box js-signup-box">
|
||||
|
||||
</section>
|
@ -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');
|
||||
|
@ -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)
|
||||
};
|
||||
|
||||
}());
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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) {
|
||||
//
|
||||
//
|
||||
//
|
||||
//});
|
@ -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);
|
@ -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);
|
@ -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'));
|
||||
});
|
@ -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 () {
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|