2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Merge branch 'master' into migrations-003

Conflicts:
	core/server/data/migration/index.js
	core/server/models/post.js
This commit is contained in:
Hannah Wolfe 2013-09-05 12:40:43 +01:00
commit 5bae29a0db
131 changed files with 8371 additions and 13409 deletions

5
.gitignore vendored
View file

@ -40,4 +40,7 @@ projectFilesBackup
/core/test/functional/*_test.png
# Changelog, which is autogenerated, not committed
CHANGELOG.md
CHANGELOG.md
# Casper generated files
/core/test/functional/*.png

View file

@ -4,5 +4,16 @@ node_js:
- "0.10"
git:
submodules: false
before_install:
- gem update --system
- gem install sass bourbon
- npm install -g grunt-cli
- git clone git://github.com/n1k0/casperjs.git ~/casperjs
- cd ~/casperjs
- git checkout tags/1.1-beta1
- export PATH=$PATH:`pwd`/bin
- cd -
before_script:
- npm install -g grunt-cli
- phantomjs --version
- casperjs --version
- grunt init

148
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,148 @@
# Contributing to Ghost
So you're interested in giving us a hand? That's awesome! We've put together some brief guidelines that should help you get started quickly and easily.
## Reporting An Issue
If you think you've found a problem with Ghost, or you'd like to make a request for a new feature in the codebase… please follow these steps:
1. **Search for existing issues** - The most important step! Help us keep duplicate issues to a minimum by checking to see if someone has already reported your problem or requested your idea.
2. **Describe your issue in detail** - Help us help you. Before opening any issue, please read the [Issue Guidelines](https://github.com/necolas/issue-guidelines), written by [Nicolas Gallagher](https://github.com/necolas/). Include operating system and version, browser and version, version of Ghost, customized or vanilla build, etc. where appropriate. Also include steps to reproduce the bug.
3. **Include a screencast if relevant** - Is your issue about a design or front end feature or bug? The most helpful thing in the world is if we can *see* what you're talking about. Use [LICEcap](http://www.cockos.com/licecap/) to quickly and easily record a short screencast (24fps) and save it as an animated gif! Embed it directly into your Github issue. Kapow.
## Working on Ghost Core
**Note:** It is recommended that you use the [Ghost-Vagrant](https://github.com/TryGhost/Ghost-Vagrant) setup for developing Ghost.
**Pre-requisites:**
* node > 0.10 and < 0.11.4
* ruby and the gems 'sass' and 'bourbon'
* for running functional tests: phantomjs 1.9.* and casperjs 1.1.* ([instructions](https://github.com/TryGhost/Ghost/wiki/Functional-testing-with-PhantomJS-and-CasperJS))
* for building docs:, python and pygments
## Key Branches & Tags
- **[master](https://github.com/TryGhost/Ghost)** is the bleeding edge development branch. All work on the next release is here.
- **[gh-pages](http://tryghost.github.io/Ghost)** is The Ghost Guide documentation for Getting Started with Ghost.
- **[releases](https://github.com/TryGhost/Ghost/releases)** are used to contain stable tagged versions of Ghost.
### Installation / Setup Instructions
1. Clone the git repo
2. cd into the project folder
3. Run `git submodule update --init`
4. Run `npm install -g grunt-cli`
5. Run `npm install`.
* If the install fails with errors to do with "node-gyp rebuild", follow the SQLite3 install instructions below this list
* Usually if you're within vagrant, and have installed the guest plugins and updated that, this will not happen
6. run `grunt init` from the root - this installs Bourbon, compiles SASS and compiles Handlebars templates
Front-end can be located at [localhost:2368](http://localhost:2368), Admin is at [localhost:2368/ghost/](http://localhost:2368/ghost/)
Whist developing you may wish to use **grunt watch** to watch for changes to handlebars and sass and recompile automatically, see the [Grunt Toolkit docs](https://github.com/TryGhost/Ghost/wiki/Grunt-Toolkit).
### Updating with the latest changes
Pulling down the latest changes from master will often require more than just a pull, you may also need to do one or more of the following:
* `npm install` - fetch any new dependencies
* `git submodule update` - fetch the latest changes to Casper (the default theme)
* `grunt` - will recompile handlebars templates and sass for the admin (as long as you have previously run `grunt init` to install bourbon)
* delete core/server/data/*.db - delete the database and allow Ghost to recreate the fixtures
### SQLite3 Install Instructions
*Only needed if you experienced errors in Step 5 above - Skip this otherwise*
Ghost depends upon SQLite3, which has to be built for each OS. NPM is as smart as it can be about this, and as long as your machine has all the pre-requisites for compiling/building a C++ program, the npm install still works.
**For Mac users:** The easiest way to do this is to download/install xCode from the App Store (free). This will automatically install all the tools you need - you don't need to open the app.
**For Everyone else:** if you don't have the required pre-requisites, you will need to either get them, or as a shortcut, obtain a precompiled SQLite3 package for your OS. We have created some of these [here](https://github.com/developmentseed/node-sqlite3/issues/106).
The pre-compiled package should be downloaded, extracted and placed in the node\_modules folder, such that it lives in node\_modules/sqlite3, if you have a partial install of the SQLite3 package, replace it with the files you downloaded from Github. Be sure that all the SQLite3 files and folders live directly in node\_modules/sqlite3 - there should note be a node\_modules/sqlite3/sqlite3 folder.
### Compiling CSS & JavaScript
A SASS compiler is required to work with the CSS in this project. You can either do this by running `grunt` from the command line - or by using a 3rd party app. We recommend [CodeKit](http://incident57.com/codekit/) (Paid/Mac) & [Scout](http://mhs.github.io/scout-app/) (Free/Mac/PC).
## Coding standards
Good, clear and consistent code styles are pivotal in the success of any software project. Good use of style can reduce errors, consistency will enable us to work together efficiently.
### JavaScript
* JSLint is King (see JSLint section below).
* Use strict mode
* Protect the global scope
* Indent with 4 spaces
* Max line length 120
* Use unix line endings
* Document as you go - we are using groc and jsdoc formats
* Write tests, unit tests are written in Mocha using spec style, functional tests use Casper.js
For more in depth information please read the official [Ghost Coding Standards](https://github.com/TryGhost/Ghost/wiki/Code-standards).
### HTML & CSS
- 4 spaces for HTML & CSS indentation. Never tabs.
- Double quotes only, never single quotes.
- Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags)
- Adhere to the [Recess CSS property order](http://markdotto.com/2011/11/29/css-property-order/).
- Always a space after a property's colon (.e.g, `display: block;` and not `display:block;`).
- End all lines with a semi-colon.
- For multiple, comma-separated selectors, place each selector on its own line.
For more in depth information please read [Mark Otto](http://github.com/mdo)'s excellent [Code Guide](http://github.com/mdo/code-guide)
## Submitting Pull Requests
The easier it is for us to merge a PR, the faster we'll be able to do it. Please take steps to make merging easy and keep the history clean and useful.
Firstly, **always work on a branch**, it will make your life much easier - honest. Not touching the master branch will also simplify keeping your fork up-to-date.
*Note:* If you are not comfortable with git & using rebase, make a special 'merge' branch of your branch to do these things on, then if something goes awry you can always go back to your working branch and try again.
### Clean-up history
Whilst you're working on your branch on your own, you can do all the commits you like - lots of little commits are highly recommended. However, when you come to submit a PR, you should clean your history ready for public consumption.
- Run `git log master..your-branch-name` to see how many commits there are on your branch
- Run `git rebase -i HEAD~#` where # is the number of commits you have done on your branch
Use the interactive rebase to edit your history. Unless you have good reason to keep more than one commit, I recommend marking the first commit with 'r' and the others with 's'. This lets you keep the first commit only, but change the message. You commit message(s) should follow the pattern described in the [notes](https://github.com/TryGhost/Ghost/wiki/Git-workflow#notes-on-writing-good-commit-messages) above. The first line of your commit message will appear in the change log which goes out to our VIPs with each pre-release, so please keep that in mind.
### Check it passes the tests
Run `grunt validate` to check that your work passes JSLint, the server-side mocha unit tests, and functional tests written in casperjs. If this fails, your PR will throw an error when submitted.
### Need Help?
If you're not completely clear on how to submit / update / *do* Pull Requests, please check out our in depth [Git Workflow guide](https://github.com/TryGhost/Ghost/wiki/Git-Workflow) for Ghost.
## Grunt Toolkit
Ghost uses Grunt heavily to automate useful tasks such as building assets, testing, live reloading/watching etc etc
[Grunt Toolkit docs](https://github.com/TryGhost/Ghost/wiki/Grunt-Toolkit) are a worthwhile read for any would-be contributor.
## Contributor License Agreement
By contributing your code to Ghost you grant the Ghost Foundation a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: (a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution.
You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions.
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
The Ghost Foundation acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

View file

@ -6,8 +6,13 @@ var path = require('path'),
spawn = require("child_process").spawn,
buildDirectory = path.resolve(process.cwd(), '.build'),
distDirectory = path.resolve(process.cwd(), '.dist'),
config = require('./config'),
_ = require('underscore'),
configureGrunt = function (grunt) {
// load all grunt tasks
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
var cfg = {
// Common paths to be used by tasks
paths: {
@ -24,6 +29,72 @@ var path = require('path'),
buildType: 'Build',
pkg: grunt.file.readJSON('package.json'),
// Watch files and livereload in the browser during development
watch: {
handlebars: {
files: ['core/client/tpl/**/*.hbs'],
tasks: ['handlebars']
},
sass: {
files: ['<%= paths.adminAssets %>/sass/**/*'],
tasks: ['sass:admin']
},
livereload: {
files: [
// Theme CSS
'content/themes/casper/css/*.css',
// Theme JS
'content/themes/casper/js/*.js',
// Admin CSS
'<%= paths.adminAssets %>/css/*.css',
// Admin JS
'core/client/*.js',
'core/client/helpers/*.js',
'core/client/models/*.js',
'core/client/tpl/*.js',
'core/client/views/*.js'
],
options: {
livereload: true
}
},
express: {
// Restart any time client or server js files change
files: ['core/server/**/*.js'],
tasks: ['express:dev'],
options: {
//Without this option specified express won't be reloaded
nospawn: true
}
}
},
// Start our server in development
express: {
options: {
script: "index.js"
},
dev: {
options: {
//output: "Express server listening on address:.*$"
}
},
test: {
options: {
node_env: 'testing'
}
}
},
// Open the site in a browser
open: {
server: {
// TODO: Load this port from config?
path: 'http://127.0.0.1:2368'
}
},
// JSLint all the things!
jslint: {
server: {
@ -112,6 +183,18 @@ var path = require('path'),
src: ['core/test/unit/**/api*_spec.js']
},
client: {
src: ['core/test/unit/**/client*_spec.js']
},
server: {
src: ['core/test/unit/**/server*_spec.js']
},
shared: {
src: ['core/test/unit/**/shared*_spec.js']
},
perm: {
src: ['core/test/unit/**/permissions_spec.js']
},
@ -175,17 +258,6 @@ var path = require('path'),
}
},
watch: {
handlebars: {
files: 'core/client/tpl/**/*.hbs',
tasks: ['handlebars']
},
sass: {
files: '<%= paths.adminAssets %>/sass/**/*',
tasks: ['sass:admin']
}
},
copy: {
nightly: {
files: [{
@ -277,18 +349,6 @@ var path = require('path'),
grunt.initConfig(cfg);
grunt.loadNpmTasks("grunt-jslint");
grunt.loadNpmTasks("grunt-mocha-cli");
grunt.loadNpmTasks("grunt-shell");
grunt.loadNpmTasks("grunt-bump");
grunt.loadNpmTasks("grunt-contrib-compress");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-sass");
grunt.loadNpmTasks("grunt-contrib-handlebars");
grunt.loadNpmTasks('grunt-groc');
// Update the package information after changes
grunt.registerTask('updateCurrentPackageInfo', function () {
cfg.pkg = grunt.file.readJSON('package.json');
@ -298,6 +358,34 @@ var path = require('path'),
cfg.buildType = type;
});
grunt.registerTask('spawn-casperjs', function () {
var done = this.async(),
options = ['host', 'noPort', 'port', 'email', 'password'],
args = ['test', 'admin/', '--includes=base.js', '--direct', '--log-level=debug', '--port=2369'];
// Forward parameters from grunt to casperjs
_.each(options, function processOption(option) {
if (grunt.option(option)) {
args.push('--' + option + '=' + grunt.option(option));
}
});
grunt.util.spawn({
cmd: 'casperjs',
args: args,
opts: {
cwd: path.resolve('core/test/functional'),
stdio: 'inherit'
}
}, function (error, result, code) {
if (error) {
grunt.fail.fatal(result.stdout);
}
grunt.log.writeln(result.stdout);
done();
});
});
// Prepare the project for development
// TODO: Git submodule init/update (https://github.com/jaubourg/grunt-update-submodules)?
grunt.registerTask("init", ["shell:bourbon", "sass:admin", 'handlebars']);
@ -311,8 +399,11 @@ var path = require('path'),
// Run migrations tests only
grunt.registerTask("test-m", ["mochacli:migrate"]);
// Run casperjs tests only
grunt.registerTask('test-functional', ['express:test', 'spawn-casperjs']);
// Run tests and lint code
grunt.registerTask("validate", ["jslint", "mochacli:all"]);
grunt.registerTask("validate", ["jslint", "mochacli:all", "test-functional"]);
// Generate Docs
grunt.registerTask("docs", ["groc"]);
@ -601,6 +692,14 @@ var path = require('path'),
"compress:build"
]);
// Dev Mode; watch files and restart server on changes
grunt.registerTask("dev", [
"default",
"express:dev",
"open",
"watch"
]);
// When you just say "grunt"
grunt.registerTask("default", ['sass:admin', 'handlebars']);
};

View file

@ -1,4 +1,4 @@
Copyright (c) 2013 The Ghost Foundation
Copyright (c) 2013 Ghost Foundation - Released under The MIT License.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

118
README.md
View file

@ -7,16 +7,46 @@ Visit the project's website at [http://tryghost.org](http://tryghost.org)!
## Getting Started
There are two main ways to get started with Ghost:
1. Working from a VIP Release - these are pre-built zip packages found on vip.tryghost.org. Installation instructions are below.
2. Working from the GitHub repo - instructions can be found in [CONTRIBUTING.md](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md)
### Installing from a VIP Release
*Please Note:* VIP Releases are pre-built packages, GitHub releases (tags) are not. To install from a GitHub release you need to follow instructions 2-5 from the [Working on Ghost Core](#working-on-ghost-core) section.
*Please Note:* VIP Releases are pre-built packages, GitHub releases (tags) are not. To install from GitHub you need to follow the [contributing guide](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md).
1. Once you've downloaded one of the release packages, unzip it, and place the directory wherever you would like to run the code
1. Once you've downloaded one of the VIP packages, unzip it, and place the directory wherever you would like to run the code
2. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.js and index.js are)
3. run `npm install` to install the node dependencies (if you get errors to do with SQLite, please see the compatibility notes)
3. run `npm install` to install the node dependencies (if you get errors to do with SQLite, please see the SQLite3 instructions below this list)
4. To start ghost, run `npm start`
5. Visit `http://localhost:2368/` in your web browser
### Updating with the latest changes
**Warning:** The Ghost file system contains your database and config. Be sure to back these up first.
1. Make a backup of your data!
2. Update the files by pasting new files over the top of old ones. If prompted by your OS or FTP client to 'merge' or 'replace' always choose merge.
3. Run npm install
4. Run npm update
5. Restart the application
6. Log out and log back in again.
### SQLite3 Install Instructions
*Only needed if you experienced errors in Step 3 above - Skip this otherwise*
Ghost depends upon SQLite3, which has to be built for each OS. NPM is as smart as it can be about this, and as long as your machine has all the pre-requisites for compiling/building a C++ program, the npm install still works.
**For Mac users:** The easiest way to do this is to download/install XCode from the App Store (free). This will automatically install all the tools you need - you don't need to open the app.
**For Everyone else:** if you don't have the required pre-requisites, you will need to either get them, or as a shortcut, obtain a precompiled SQLite3 package for your OS. We have created some of these [here](https://github.com/developmentseed/node-sqlite3/issues/106).
The pre-compiled package should be downloaded, extracted and placed in the node\_modules folder, such that it lives in node\_modules/sqlite3, if you have a partial install of the SQLite3 package, replace it with the files you downloaded from github. Be sure that all the SQLite3 files and folders live directly in node\_modules/sqlite3 - there should note be a node\_modules/sqlite3/sqlite3 folder.
### Logging in For The First Time
Once you have the Ghost server up and running, you should be able to navigate to `http://localhost:2368/ghost` from a web browser, where you will be prompted for a login.
@ -25,76 +55,9 @@ Once you have the Ghost server up and running, you should be able to navigate to
2. Enter your user details (careful here: There is no password reset yet!)
3. Return to the login screen and use those details to log in.
Note - this is still very alpha. Not everything works yet.
### Currently Working Features
* Login / Logout / Register
* User can register an email address & password
* User can login
* User can logout
* All /ghost/ routes (the admin) are auth-protected
* Dashboard
* All widgets are static at the moment. Blogging related functionality is our first priority.
* Admin menu
* G, dashboard, content, new post & settings menu items go to correct pages
* Content screen
* Lists all posts
* Select post in list highlights that post and opens it in the preview pane
* Write screen
* Live preview works for all standard markdown
* Save draft button saves entered title & content. Everything is published by default.
* Editing/opening existing post puts correct info in title and content panels & save updates content.
* Database
* The database is created and populated with basic data on first run of the server
* New posts and edits save and last forever
* The data can be reset by finding core/shared/data/*.db and emptying or deleting the file. The next restart of the server will cause the database to be recreated and repopulated.
* Frontend
* Homepage lists a number of posts as configured in config.js
* Clicking on an individual post loads an individual post page
* Date formatting helper uses moment
## Working on Ghost Core
**Note:** It is recommended that you use the [Ghost-Vagrant](https://github.com/TryGhost/Ghost-Vagrant) setup for developing Ghost.
Pre-requisites:
- node 0.10 or 0.11
- ruby and the gems 'sass' and 'bourbon'
- if you want to build the docs, python and pygments
1. Clone the git repo
2. cd into the project folder
3. Run `git submodule update --init`
4. Run `npm install -g grunt-cli`
5. Run `npm install`.
* If the install fails with errors to do with "node-gyp rebuild", follow the SQLite3 install instructions
* Usually if you're within vagrant, and have installed the guest plugins and updated that, this will not happen
6. run `grunt init` from the root - this installs bourbon, compiles sass and compiles handlebars templates
Frontend can be located at [localhost:2368](http://localhost:2368), Admin is at [localhost:2368/ghost](http://localhost:2368/ghost)
Whist developing you may wish to use **grunt watch** to watch for changes to handlebars and sass and recompile automatically
### Updating with the latest changes
Pulling down the latest changes from master will often require more than just a pull, you may also need to do one or more of the following:
* `npm install` - fetch any new dependencies
* `git submodule update` - fetch the latest changes to Casper (the default theme)
* `grunt` - will recompile handlebars templates and sass for the admin (as long as you have previously run `grunt init` to install bourbon)
* delete core/server/data/*.db - delete the database and allow Ghost to recreate the fixtures
### SQLite3 Install Instructions
Ghost depends upon SQLite3, which has to be built for each OS. NPM is as smart as it can be about this, and as long as your machine has all the pre-requisites for compiling/building a C++ program, the npm install still works.
**For Mac users:** The easiest way to do this is to download/install XCode from the App Store (free). This will automatically install all the tools you need - you don't need to open the app.
**For Everyone else:** if you don't have the required pre-requisites, you will need to either get them, or as a shortcut, obtain a precompiled SQLite3 package for your OS. We have created some of these [here](https://github.com/developmentseed/node-sqlite3/issues/106).
The pre-compiled package should be downloaded, extracted and placed in the node\_modules folder, such that it lives in node\_modules/sqlite3, if you have a partial install of the SQLite3 package, replace it with the files you downloaded from github. Be sure that all the SQLite3 files and folders live directly in node\_modules/sqlite3 - there should note be a node\_modules/sqlite3/sqlite3 folder.
## Versioning
@ -111,13 +74,10 @@ Constructed with the following guidelines:
* A new *patch* release indicates a bugfix or small change which does not affect compatibility.
* A new *build* release indicates this is a pre-release of the version.
## Bugs
If you have a bug or feature request, please [open a new issue](https://github.com/TryGhost/Ghost/issues). Before opening any issue, please search for existing issues and read the [Issue Guidelines](https://github.com/necolas/issue-guidelines), written by [Nicolas Gallagher](https://github.com/necolas/).
## Reporting Bugs and Contributing Code
## Contributions
Please see our [Guide to Contributing](https://github.com/TryGhost/Ghost/wiki/Contributing).
Want to report a bug, request a feature, or help us build Ghost? Check out our in depth guide to [Contributing to Ghost](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md). We need all the help we can get!
## Community
@ -127,14 +87,6 @@ Keep track of Ghost development and Ghost community activity.
* Read and subscribe to the [The Official Ghost Blog](http://blog.tryghost.org).
* Chat with Ghost developers on IRC. We're on `irc.freenode.net`, in the `#Ghost` channel.
## Compiling CSS & JavaScript
A SASS compiler is required to work with the CSS in this project.
With bourbon, all you have to do is run `grunt init` from the root of Ghost, which will compile the admin section. For everything else, use `sass <sourcefile> <targetfile>`.
We also recommend [CodeKit](http://incident57.com/codekit/) (Paid/Mac) & [Scout](http://mhs.github.io/scout-app/) (Free/Mac/PC).
## Copyright & License

View file

@ -11,16 +11,14 @@ config.defaultLang = 'en_US';
// Force i18n to be on
config.forceI18n = true;
// ## Themes & Plugins
// Current active theme
config.activeTheme = 'casper';
// ## Plugins
// Current active plugins
config.activePlugins = [
'FancyFirstChar'
];
// ## Default Navigation Items
// Add new objects here to extend the menu output by {{nav}}
config.nav = [
@ -43,10 +41,12 @@ config.env = {
filename: path.join(__dirname, '/core/server/data/ghost-test.db')
}
},
url: {
server: {
host: '127.0.0.1',
port: '2368'
}
port: '2369'
},
// The url to use when providing links to the site; like RSS and email.
url: 'http://127.0.0.1:2369'
},
travis: {
@ -56,10 +56,12 @@ config.env = {
filename: path.join(__dirname, '/core/server/data/ghost-travis.db')
}
},
url: {
server: {
host: '127.0.0.1',
port: '2368'
}
},
// The url to use when providing links to the site; like RSS and email.
url: 'http://127.0.0.1:2368'
},
// Default configuration
@ -71,9 +73,23 @@ config.env = {
},
debug: false
},
url: {
server: {
host: '127.0.0.1',
port: '2368'
},
// The url to use when providing links to the site; like RSS and email.
url: 'http://127.0.0.1:2368',
// Example mail config
mail: {
transport: 'sendgrid',
host: 'smtp.sendgrid.net',
options: {
service: 'Sendgrid',
auth: {
user: '', // Super secret username
pass: '' // Super secret password
}
}
}
},
@ -85,10 +101,12 @@ config.env = {
},
debug: false
},
url: {
server: {
host: '127.0.0.1',
port: '2368'
}
},
// The url to use when providing links to the site; like RSS and email.
url: 'http://127.0.0.1:2368'
},
production: {
@ -99,12 +117,14 @@ config.env = {
},
debug: false
},
url: {
server: {
host: '127.0.0.1',
port: '2368'
}
},
// The url to use when providing links to the site; like RSS and email.
url: 'http://127.0.0.1:2368'
}
};
// Export config
module.exports = config;
module.exports = config;

@ -1 +1 @@
Subproject commit cebd42fc51638104186790e3e300888f88009f79
Subproject commit 618eba0e2f47c00c33a94e0f0a46c8097b1ffb7c

View file

@ -9,10 +9,7 @@ This is a custom SVG font generated by IcoMoon.
<font id="icons" horiz-adv-x="512" >
<font-face units-per-em="512" ascent="480" descent="-32" />
<missing-glyph horiz-adv-x="512" />
<glyph unicode="&#xe000;" d="M 53.746,220.369l0,1.157 c0,117.673, 91.673,214.052, 217.034,214.052c 74.498,0, 119.447-20.106, 162.611-56.736l-57.365-69.152
c-31.954,26.608-60.348,41.967-108.228,41.967c-66.271,0-118.859-58.522-118.859-128.872l0-1.157 c0-75.646, 52.029-131.275, 125.361-131.275
c 33.111,0, 62.659,8.279, 85.748,24.843l0,59.090 l-91.673,0 l0,78.637 l 179.744,0 l0-179.744 c-42.584-36.093-101.118-65.633-176.802-65.633
C 142.436,7.474, 53.746,97.95, 53.746,220.369z" data-tags="ghost" />
<glyph class="hidden" unicode="&#xf000;" d="M0,480L 512 -32L0 -32 z" horiz-adv-x="0" />
<glyph unicode="&#xe001;" d="M 387.115,348.579M 255.58,217.052L 124.053,348.579L-0.292,348.579L-0.597,348.259L 255.58,92.089L 511.751,348.259L 511.445,348.579L 387.115,348.579 z" data-tags="chevron-down" />
<glyph unicode="&#xe002;" d="M 120.412,97.756l0,0.149 c 0.092,8.135, 3.676,33.657, 41.628,56.882c 6.948,9.436, 12.658,22.308, 18.496,37.163
c 4.032,10.268, 3.349,19.036, 3.349,31.509c0,9.223, 1.735,24.014-0.548,32.149c-7.723,27.463-27.214,35.051-50.062,35.051
@ -57,7 +54,7 @@ This is a custom SVG font generated by IcoMoon.
L 512,46.798z M 63.879,288.085c0,70.528, 57.38,127.908, 127.908,127.908s 127.908-57.38, 127.908-127.908c0-70.528-57.372-127.9-127.908-127.9
S 63.879,217.557, 63.879,288.085z" data-tags="search-left" />
<glyph unicode="&#xe009;" d="M -0.292,303.886l0-98.596 c 64.007,0, 124.124-25.003, 169.337-70.428c 44.679-44.9, 69.49-104.455, 70.13-167.979L 337.778-33.131
C 336.412,152.924, 185.323,303.886 -0.292,303.886zM -0.142,478.649l0-98.56 c 227.15,0, 412.082-185.131, 413.49-413.198L 511.929-33.131 C 510.507,249.308, 281.387,478.649 -0.142,478.649zM -0.363,35.271A68.281,68.281 1260 1 1 136.213,35.266999999999996A68.281,68.281 1260 1 1 -0.363,35.266999999999996z" data-tags="rss" />
C 336.412,152.924, 185.323,303.886 -0.292,303.886zM -0.142,478.649l0-98.56 c 227.15,0, 412.082-185.131, 413.49-413.198L 511.929-33.131 C 510.507,249.308, 281.387,478.649 -0.142,478.649zM -0.363,35.271A68.281,68.281 1620 1 1 136.213,35.266999999999996A68.281,68.281 1620 1 1 -0.363,35.266999999999996z" data-tags="rss" />
<glyph unicode="&#xe00a;" d="M 424.533,288l-227.556,0 l0-197.924 l 227.556,0 L 424.533,288 z M 396.089,118.521l-170.667,0 L 225.422,259.548 l 170.667,0 L 396.089,118.521 zM 83.2,288L 156.978,288L 156.978,90.076L 83.2,90.076zM -1.401,438.165l0-440.96 l 511.417,0 L 510.016,438.165 L -1.401,438.165 z M 361.742,408.647l 104.789,0 l0-40.676 L 361.742,367.971 L 361.742,408.647 z M 40.54,408.647l 284.231,0 l0-40.676 L 40.54,367.971 L 40.54,408.647 z
M 467.349,39.872L 41.266,39.872 l0,298.738 l 426.084,0 L 467.349,39.872 z" data-tags="preview" />
<glyph unicode="&#xe00b;" d="M 392.476,433.145L 118.684,433.145 c0,0-119.524-54.784-119.524-215.417c0-160.64, 119.524-214.172, 119.524-214.172l 273.792,0
@ -79,11 +76,11 @@ This is a custom SVG font generated by IcoMoon.
c 20.935,0, 37.909,16.974, 37.909,37.909L 411.371,439.075 C 411.392,460.011, 394.404,476.985, 373.476,476.985z M 255.004-11.314c-13.419,0-24.299,10.873-24.299,24.299
c0,13.419, 10.88,24.299, 24.299,24.299s 24.299-10.88, 24.299-24.299C 279.31-0.441, 268.43-11.314, 255.004-11.314z M 382.933,63.004L 127.012,63.004 L 127.012,377.429
L 382.933,377.429 L 382.933,63.004 z" data-tags="mobile" />
<glyph unicode="&#xe011;" d="M 0.59,415.922l0-383.922 L 512,32 L 512,415.922 L 0.59,415.922 z M 469.333,74.667L 43.257,74.667 L 43.257,373.255 L 469.333,373.255 L 469.333,74.667 zM 94.684,110.13L 430.748,110.13L 385.266,257.813L 351.14,246.457L 321.579,302.201L 226.048,164.288L 174.869,197.262 zM 96.668,284.878A40.37,40.37 1260 1 1 177.408,284.878A40.37,40.37 1260 1 1 96.668,284.878z" data-tags="media" />
<glyph unicode="&#xe011;" d="M 0.59,415.922l0-383.922 L 512,32 L 512,415.922 L 0.59,415.922 z M 469.333,74.667L 43.257,74.667 L 43.257,373.255 L 469.333,373.255 L 469.333,74.667 zM 94.684,110.13L 430.748,110.13L 385.266,257.813L 351.14,246.457L 321.579,302.201L 226.048,164.288L 174.869,197.262 zM 96.668,284.878A40.37,40.37 1620 1 1 177.408,284.878A40.37,40.37 1620 1 1 96.668,284.878z" data-tags="media" />
<glyph unicode="&#xe012;" d="M 511.9,68.793c 1.052,3.1, 1.756,6.357, 1.756,9.792L 513.657,363.378 c0,3.264-0.654,6.357-1.607,9.316L 344.74,229.433L 511.9,68.793zM 257.188,194.887l 44.068,35.854l 22.308,18.148l 168.021,143.822c-2.759,0.818-5.618,1.394-8.633,1.394L 31.452,394.105
c-3.022,0-5.888-0.576-8.654-1.401l 168.121-143.893l 22.308-18.155L 257.188,194.887zM 482.944,47.865c 2.887,0, 5.632,0.533, 8.292,1.294L 322.432,211.299l-65.244-53.099l-65.145,53.013L 23.168,49.152
c 2.652-0.747, 5.397-1.287, 8.284-1.287L 482.944,47.865 zM 2.34,372.693c-0.953-2.958-1.607-6.052-1.607-9.316l0-284.793 c0-3.442, 0.704-6.699, 1.756-9.792l 167.246,160.569L 2.34,372.693z" data-tags="mail" />
<glyph unicode="&#xe013;" d="M 138.539,435.548L 512.697,435.548L 512.697,353.337L 138.539,353.337zM 138.539,260.707L 512.697,260.707L 512.697,178.482L 138.539,178.482zM 138.539,84.914L 512.697,84.914L 512.697,2.695L 138.539,2.695zM 0.533,394.51A41.038,41.038 1260 1 1 82.61,394.51A41.038,41.038 1260 1 1 0.533,394.51zM 0.533,219.883A41.038,41.038 1260 1 1 82.61,219.88299999999998A41.038,41.038 1260 1 1 0.533,219.88299999999998zM 0.533,44.48A41.038,41.038 1260 1 1 82.61,44.47300000000001A41.038,41.038 1260 1 1 0.533,44.47300000000001z" data-tags="list" />
<glyph unicode="&#xe013;" d="M 138.539,435.548L 512.697,435.548L 512.697,353.337L 138.539,353.337zM 138.539,260.707L 512.697,260.707L 512.697,178.482L 138.539,178.482zM 138.539,84.914L 512.697,84.914L 512.697,2.695L 138.539,2.695zM 0.533,394.51A41.038,41.038 1620 1 1 82.61,394.51A41.038,41.038 1620 1 1 0.533,394.51zM 0.533,219.883A41.038,41.038 1620 1 1 82.61,219.88299999999998A41.038,41.038 1620 1 1 0.533,219.88299999999998zM 0.533,44.48A41.038,41.038 1620 1 1 82.61,44.47300000000001A41.038,41.038 1620 1 1 0.533,44.47300000000001z" data-tags="list" />
<glyph unicode="&#xe014;" d="M 256,480c-141.383,0-256-114.61-256-256s 114.617-256, 256-256s 256,114.61, 256,256S 397.383,480, 256,480z M 288.853,400.704
c 10.247,0.050, 18.368-3.911, 23.58-10.098c 5.241-6.165, 7.644-14.3, 7.644-22.628c0-21.604-17.159-42.048-41.109-42.126c-0.199,0-0.412,0-0.612,0
c-18.78,0-30.514,13.938-30.635,34.219C 247.758,377.543, 261.803,400.604, 288.853,400.704z M 319.851,113.728
@ -161,7 +158,7 @@ This is a custom SVG font generated by IcoMoon.
c 5.703,0.697, 11.812,2.126, 18.325,4.48C 131.954-7.154, 204.715,57.806, 176.768,137.067zM 146.617,241.429c-10.567-3.804-19.378-12.274-23.132-23.765l-10.389-31.68c 3.136,0.014, 6.343-0.071, 9.614-0.32
c 20.174-1.372, 42.581-9.237, 60.622-28.58l 12.11,36.935c 3.797,11.52, 1.721,23.524-4.53,32.875l 68.402,208.469
c 4.011,12.245-2.631,25.408-14.876,29.419c-3.371,1.102-6.791,1.401-10.105,1.003c-8.683-1.060-16.398-7.004-19.314-15.879L 146.617,241.429z" data-tags="appearance" />
<glyph unicode="&#xe022;" d="M 192,336A80,80 11700 1 1 352,336A80,80 11700 1 1 192,336zM0,336A80,80 11700 1 1 160,336A80,80 11700 1 1 0,336zM 384,176l0,48 c0,17.6-14.4,32-32,32l-80,0 L 80,256 L 32,256 c-17.6,0-32-14.4-32-32l0-160 c0-17.6, 14.4-32, 32-32l 320,0 c 17.6,0, 32,14.4, 32,32
<glyph unicode="&#xe022;" d="M 192,336A80,80 12060 1 1 352,336A80,80 12060 1 1 192,336zM0,336A80,80 12060 1 1 160,336A80,80 12060 1 1 0,336zM 384,176l0,48 c0,17.6-14.4,32-32,32l-80,0 L 80,256 L 32,256 c-17.6,0-32-14.4-32-32l0-160 c0-17.6, 14.4-32, 32-32l 320,0 c 17.6,0, 32,14.4, 32,32
l0,48 l 128-80L 512,256 L 384,176z M 320,96L 64,96 l0,96 l 256,0 L 320,96 z" data-tags="camera, video, media, film, movie" />
<glyph unicode="&#xe023;" d="M 96-32L 416-32L 448,320L 64,320 zM 320,416L 320,480 L 192,480 l0-64 L 32,416 l0-96 l 32,32l 384,0 l 32-32L 480,416 L 320,416 z M 288,416l-64,0 L 224,448 l 64,0 L 288,416 z" data-tags="remove, delete, trashcan, recycle bin, bin, dispose" />
<glyph unicode="&#xe025;" d="M 123.86,124.211 C 136.923,113.598 156.131,115.58 166.758,128.644 L 166.758,128.695 L 197.837,166.956 L 247.303,99.964 C 253.879,91.077 264.806,86.418 275.829,87.888 C 286.815,89.315 296.141,96.636 300.193,106.986 L 347.209,226.948 L 415.393,66.348 C 420.184,55.025 431.228,47.784 443.458,47.784 C 444.021,47.784 444.577,47.799 445.162,47.813 C 458.028,48.567 468.992,57.256 472.656,69.573 L 537.571,287.963 L 566.184,229.179 C 573.52,214.038 591.762,207.748 606.91,215.106 C 622.043,222.457 628.363,240.713 620.99,255.824 L 558.109,385.090 C 552.587,396.42 540.738,403.141 528.216,402.132 C 515.65,401.093 505.088,392.536 501.482,380.438 L 438.367,168.185 L 374.17,319.525 C 369.313,330.957 358.122,338.286 345.659,338.096 C 333.261,337.92 322.253,330.306 317.725,318.727 L 263.57,180.553 L 223.481,234.84 C 217.841,242.432 209.115,246.952 199.658,247.201 C 190.179,247.413 181.263,243.295 175.301,235.93 L 119.442,167.139 C 108.815,154.039 110.819,134.832 123.86,124.211 L 123.86,124.211 Z M 123.86,124.211" horiz-adv-x="746" data-tags="Stats" />
@ -284,6 +281,7 @@ This is a custom SVG font generated by IcoMoon.
c-29.874,40.708-77.021,65.125-128.208,65.125c-34.125,0-66.312-11.042-92.938-30.334c 7.479,22.854, 28.729,39.479, 54.062,39.479
c 7.75,0, 15.062-1.562, 21.75-4.332c 15.188,29.562, 45.625,50.020, 81.125,50.020s 65.958-20.457, 81.084-50.020
c 6.729,2.77, 14.083,4.332, 21.749,4.332c 31.584,0, 57.167-25.583, 57.167-57.146C 480,225.52, 474.438,212.458, 465.562,202.375z" data-tags="cloudy, weather, clouds" />
<glyph unicode="&#xe03d;" d="M 206.933,147.022 L 265.529,311.644 L 265.593,311.68 L 265.536,311.68 L 265.593,311.865 L 110.834,311.737 C 111.573,314.261 148.516,415.324 167.424,480 L 80.604,480 L 0.064,252.935 L0,252.9 L 0.050,252.9 L0,252.715 L 154.759,252.836 C 154.233,251.051 135.367,199.765 117.803,147.015 L 45.312,147.015 L 112.732-32.484 L 283.812,147.015 L 206.933,147.015 L 206.933,147.022 Z M 206.933,147.022" horiz-adv-x="284.444" data-tags="lightning" />
<glyph unicode="&#xe000;" d="M0,480 L0,377.6 L 307.2,377.6 L 307.2,480 L0,480 Z M0,480M0,275.2 L0,172.8 L 512,172.8 L 512,275.2 L0,275.2 Z M0,275.2M0,70.4 L0-32 L 204.8-32 L 204.8,70.4 L0,70.4 Z M0,70.4M 307.2,70.4 L 307.2-32 L 512-32 L 512,70.4 L 307.2,70.4 Z M 307.2,70.4M 409.6,480 L 409.6,377.6 L 512,377.6 L 512,480 L 409.6,480 Z M 409.6,480" data-tags="ghst" />
<glyph unicode="&#x20;" horiz-adv-x="256" />
<glyph class="hidden" unicode="&#xf000;" d="M0,480L 512 -32L0 -32 z" horiz-adv-x="0" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

View file

@ -9,10 +9,7 @@ This is a custom SVG font generated by IcoMoon.
<font id="icons" horiz-adv-x="512" >
<font-face units-per-em="512" ascent="480" descent="-32" />
<missing-glyph horiz-adv-x="512" />
<glyph unicode="&#xe000;" d="M 53.746,220.369l0,1.157 c0,117.673, 91.673,214.052, 217.034,214.052c 74.498,0, 119.447-20.106, 162.611-56.736l-57.365-69.152
c-31.954,26.608-60.348,41.967-108.228,41.967c-66.271,0-118.859-58.522-118.859-128.872l0-1.157 c0-75.646, 52.029-131.275, 125.361-131.275
c 33.111,0, 62.659,8.279, 85.748,24.843l0,59.090 l-91.673,0 l0,78.637 l 179.744,0 l0-179.744 c-42.584-36.093-101.118-65.633-176.802-65.633
C 142.436,7.474, 53.746,97.95, 53.746,220.369z" />
<glyph class="hidden" unicode="&#xf000;" d="M0,480L 512 -32L0 -32 z" horiz-adv-x="0" />
<glyph unicode="&#xe001;" d="M 387.115,348.579M 255.58,217.052L 124.053,348.579L-0.292,348.579L-0.597,348.259L 255.58,92.089L 511.751,348.259L 511.445,348.579L 387.115,348.579 z" />
<glyph unicode="&#xe002;" d="M 120.412,97.756l0,0.149 c 0.092,8.135, 3.676,33.657, 41.628,56.882c 6.948,9.436, 12.658,22.308, 18.496,37.163
c 4.032,10.268, 3.349,19.036, 3.349,31.509c0,9.223, 1.735,24.014-0.548,32.149c-7.723,27.463-27.214,35.051-50.062,35.051
@ -57,7 +54,7 @@ This is a custom SVG font generated by IcoMoon.
L 512,46.798z M 63.879,288.085c0,70.528, 57.38,127.908, 127.908,127.908s 127.908-57.38, 127.908-127.908c0-70.528-57.372-127.9-127.908-127.9
S 63.879,217.557, 63.879,288.085z" />
<glyph unicode="&#xe009;" d="M -0.292,303.886l0-98.596 c 64.007,0, 124.124-25.003, 169.337-70.428c 44.679-44.9, 69.49-104.455, 70.13-167.979L 337.778-33.131
C 336.412,152.924, 185.323,303.886 -0.292,303.886zM -0.142,478.649l0-98.56 c 227.15,0, 412.082-185.131, 413.49-413.198L 511.929-33.131 C 510.507,249.308, 281.387,478.649 -0.142,478.649zM -0.363,35.271A68.281,68.281 1260 1 1 136.213,35.266999999999996A68.281,68.281 1260 1 1 -0.363,35.266999999999996z" />
C 336.412,152.924, 185.323,303.886 -0.292,303.886zM -0.142,478.649l0-98.56 c 227.15,0, 412.082-185.131, 413.49-413.198L 511.929-33.131 C 510.507,249.308, 281.387,478.649 -0.142,478.649zM -0.363,35.271A68.281,68.281 1620 1 1 136.213,35.266999999999996A68.281,68.281 1620 1 1 -0.363,35.266999999999996z" />
<glyph unicode="&#xe00a;" d="M 424.533,288l-227.556,0 l0-197.924 l 227.556,0 L 424.533,288 z M 396.089,118.521l-170.667,0 L 225.422,259.548 l 170.667,0 L 396.089,118.521 zM 83.2,288L 156.978,288L 156.978,90.076L 83.2,90.076zM -1.401,438.165l0-440.96 l 511.417,0 L 510.016,438.165 L -1.401,438.165 z M 361.742,408.647l 104.789,0 l0-40.676 L 361.742,367.971 L 361.742,408.647 z M 40.54,408.647l 284.231,0 l0-40.676 L 40.54,367.971 L 40.54,408.647 z
M 467.349,39.872L 41.266,39.872 l0,298.738 l 426.084,0 L 467.349,39.872 z" />
<glyph unicode="&#xe00b;" d="M 392.476,433.145L 118.684,433.145 c0,0-119.524-54.784-119.524-215.417c0-160.64, 119.524-214.172, 119.524-214.172l 273.792,0
@ -79,11 +76,11 @@ This is a custom SVG font generated by IcoMoon.
c 20.935,0, 37.909,16.974, 37.909,37.909L 411.371,439.075 C 411.392,460.011, 394.404,476.985, 373.476,476.985z M 255.004-11.314c-13.419,0-24.299,10.873-24.299,24.299
c0,13.419, 10.88,24.299, 24.299,24.299s 24.299-10.88, 24.299-24.299C 279.31-0.441, 268.43-11.314, 255.004-11.314z M 382.933,63.004L 127.012,63.004 L 127.012,377.429
L 382.933,377.429 L 382.933,63.004 z" />
<glyph unicode="&#xe011;" d="M 0.59,415.922l0-383.922 L 512,32 L 512,415.922 L 0.59,415.922 z M 469.333,74.667L 43.257,74.667 L 43.257,373.255 L 469.333,373.255 L 469.333,74.667 zM 94.684,110.13L 430.748,110.13L 385.266,257.813L 351.14,246.457L 321.579,302.201L 226.048,164.288L 174.869,197.262 zM 96.668,284.878A40.37,40.37 1260 1 1 177.408,284.878A40.37,40.37 1260 1 1 96.668,284.878z" />
<glyph unicode="&#xe011;" d="M 0.59,415.922l0-383.922 L 512,32 L 512,415.922 L 0.59,415.922 z M 469.333,74.667L 43.257,74.667 L 43.257,373.255 L 469.333,373.255 L 469.333,74.667 zM 94.684,110.13L 430.748,110.13L 385.266,257.813L 351.14,246.457L 321.579,302.201L 226.048,164.288L 174.869,197.262 zM 96.668,284.878A40.37,40.37 1620 1 1 177.408,284.878A40.37,40.37 1620 1 1 96.668,284.878z" />
<glyph unicode="&#xe012;" d="M 511.9,68.793c 1.052,3.1, 1.756,6.357, 1.756,9.792L 513.657,363.378 c0,3.264-0.654,6.357-1.607,9.316L 344.74,229.433L 511.9,68.793zM 257.188,194.887l 44.068,35.854l 22.308,18.148l 168.021,143.822c-2.759,0.818-5.618,1.394-8.633,1.394L 31.452,394.105
c-3.022,0-5.888-0.576-8.654-1.401l 168.121-143.893l 22.308-18.155L 257.188,194.887zM 482.944,47.865c 2.887,0, 5.632,0.533, 8.292,1.294L 322.432,211.299l-65.244-53.099l-65.145,53.013L 23.168,49.152
c 2.652-0.747, 5.397-1.287, 8.284-1.287L 482.944,47.865 zM 2.34,372.693c-0.953-2.958-1.607-6.052-1.607-9.316l0-284.793 c0-3.442, 0.704-6.699, 1.756-9.792l 167.246,160.569L 2.34,372.693z" />
<glyph unicode="&#xe013;" d="M 138.539,435.548L 512.697,435.548L 512.697,353.337L 138.539,353.337zM 138.539,260.707L 512.697,260.707L 512.697,178.482L 138.539,178.482zM 138.539,84.914L 512.697,84.914L 512.697,2.695L 138.539,2.695zM 0.533,394.51A41.038,41.038 1260 1 1 82.61,394.51A41.038,41.038 1260 1 1 0.533,394.51zM 0.533,219.883A41.038,41.038 1260 1 1 82.61,219.88299999999998A41.038,41.038 1260 1 1 0.533,219.88299999999998zM 0.533,44.48A41.038,41.038 1260 1 1 82.61,44.47300000000001A41.038,41.038 1260 1 1 0.533,44.47300000000001z" />
<glyph unicode="&#xe013;" d="M 138.539,435.548L 512.697,435.548L 512.697,353.337L 138.539,353.337zM 138.539,260.707L 512.697,260.707L 512.697,178.482L 138.539,178.482zM 138.539,84.914L 512.697,84.914L 512.697,2.695L 138.539,2.695zM 0.533,394.51A41.038,41.038 1620 1 1 82.61,394.51A41.038,41.038 1620 1 1 0.533,394.51zM 0.533,219.883A41.038,41.038 1620 1 1 82.61,219.88299999999998A41.038,41.038 1620 1 1 0.533,219.88299999999998zM 0.533,44.48A41.038,41.038 1620 1 1 82.61,44.47300000000001A41.038,41.038 1620 1 1 0.533,44.47300000000001z" />
<glyph unicode="&#xe014;" d="M 256,480c-141.383,0-256-114.61-256-256s 114.617-256, 256-256s 256,114.61, 256,256S 397.383,480, 256,480z M 288.853,400.704
c 10.247,0.050, 18.368-3.911, 23.58-10.098c 5.241-6.165, 7.644-14.3, 7.644-22.628c0-21.604-17.159-42.048-41.109-42.126c-0.199,0-0.412,0-0.612,0
c-18.78,0-30.514,13.938-30.635,34.219C 247.758,377.543, 261.803,400.604, 288.853,400.704z M 319.851,113.728
@ -161,7 +158,7 @@ This is a custom SVG font generated by IcoMoon.
c 5.703,0.697, 11.812,2.126, 18.325,4.48C 131.954-7.154, 204.715,57.806, 176.768,137.067zM 146.617,241.429c-10.567-3.804-19.378-12.274-23.132-23.765l-10.389-31.68c 3.136,0.014, 6.343-0.071, 9.614-0.32
c 20.174-1.372, 42.581-9.237, 60.622-28.58l 12.11,36.935c 3.797,11.52, 1.721,23.524-4.53,32.875l 68.402,208.469
c 4.011,12.245-2.631,25.408-14.876,29.419c-3.371,1.102-6.791,1.401-10.105,1.003c-8.683-1.060-16.398-7.004-19.314-15.879L 146.617,241.429z" />
<glyph unicode="&#xe022;" d="M 192,336A80,80 11700 1 1 352,336A80,80 11700 1 1 192,336zM0,336A80,80 11700 1 1 160,336A80,80 11700 1 1 0,336zM 384,176l0,48 c0,17.6-14.4,32-32,32l-80,0 L 80,256 L 32,256 c-17.6,0-32-14.4-32-32l0-160 c0-17.6, 14.4-32, 32-32l 320,0 c 17.6,0, 32,14.4, 32,32
<glyph unicode="&#xe022;" d="M 192,336A80,80 12060 1 1 352,336A80,80 12060 1 1 192,336zM0,336A80,80 12060 1 1 160,336A80,80 12060 1 1 0,336zM 384,176l0,48 c0,17.6-14.4,32-32,32l-80,0 L 80,256 L 32,256 c-17.6,0-32-14.4-32-32l0-160 c0-17.6, 14.4-32, 32-32l 320,0 c 17.6,0, 32,14.4, 32,32
l0,48 l 128-80L 512,256 L 384,176z M 320,96L 64,96 l0,96 l 256,0 L 320,96 z" />
<glyph unicode="&#xe023;" d="M 96-32L 416-32L 448,320L 64,320 zM 320,416L 320,480 L 192,480 l0-64 L 32,416 l0-96 l 32,32l 384,0 l 32-32L 480,416 L 320,416 z M 288,416l-64,0 L 224,448 l 64,0 L 288,416 z" />
<glyph unicode="&#xe025;" d="M 123.86,124.211 C 136.923,113.598 156.131,115.58 166.758,128.644 L 166.758,128.695 L 197.837,166.956 L 247.303,99.964 C 253.879,91.077 264.806,86.418 275.829,87.888 C 286.815,89.315 296.141,96.636 300.193,106.986 L 347.209,226.948 L 415.393,66.348 C 420.184,55.025 431.228,47.784 443.458,47.784 C 444.021,47.784 444.577,47.799 445.162,47.813 C 458.028,48.567 468.992,57.256 472.656,69.573 L 537.571,287.963 L 566.184,229.179 C 573.52,214.038 591.762,207.748 606.91,215.106 C 622.043,222.457 628.363,240.713 620.99,255.824 L 558.109,385.090 C 552.587,396.42 540.738,403.141 528.216,402.132 C 515.65,401.093 505.088,392.536 501.482,380.438 L 438.367,168.185 L 374.17,319.525 C 369.313,330.957 358.122,338.286 345.659,338.096 C 333.261,337.92 322.253,330.306 317.725,318.727 L 263.57,180.553 L 223.481,234.84 C 217.841,242.432 209.115,246.952 199.658,247.201 C 190.179,247.413 181.263,243.295 175.301,235.93 L 119.442,167.139 C 108.815,154.039 110.819,134.832 123.86,124.211 L 123.86,124.211 Z M 123.86,124.211" horiz-adv-x="746" />
@ -284,6 +281,7 @@ This is a custom SVG font generated by IcoMoon.
c-29.874,40.708-77.021,65.125-128.208,65.125c-34.125,0-66.312-11.042-92.938-30.334c 7.479,22.854, 28.729,39.479, 54.062,39.479
c 7.75,0, 15.062-1.562, 21.75-4.332c 15.188,29.562, 45.625,50.020, 81.125,50.020s 65.958-20.457, 81.084-50.020
c 6.729,2.77, 14.083,4.332, 21.749,4.332c 31.584,0, 57.167-25.583, 57.167-57.146C 480,225.52, 474.438,212.458, 465.562,202.375z" />
<glyph unicode="&#xe03d;" d="M 206.933,147.022 L 265.529,311.644 L 265.593,311.68 L 265.536,311.68 L 265.593,311.865 L 110.834,311.737 C 111.573,314.261 148.516,415.324 167.424,480 L 80.604,480 L 0.064,252.935 L0,252.9 L 0.050,252.9 L0,252.715 L 154.759,252.836 C 154.233,251.051 135.367,199.765 117.803,147.015 L 45.312,147.015 L 112.732-32.484 L 283.812,147.015 L 206.933,147.015 L 206.933,147.022 Z M 206.933,147.022" horiz-adv-x="284.444" />
<glyph unicode="&#xe000;" d="M0,480 L0,377.6 L 307.2,377.6 L 307.2,480 L0,480 Z M0,480M0,275.2 L0,172.8 L 512,172.8 L 512,275.2 L0,275.2 Z M0,275.2M0,70.4 L0-32 L 204.8-32 L 204.8,70.4 L0,70.4 Z M0,70.4M 307.2,70.4 L 307.2-32 L 512-32 L 512,70.4 L 307.2,70.4 Z M 307.2,70.4M 409.6,480 L 409.6,377.6 L 512,377.6 L 512,480 L 409.6,480 Z M 409.6,480" />
<glyph unicode="&#x20;" horiz-adv-x="256" />
<glyph class="hidden" unicode="&#xf000;" d="M0,480L 512 -32L0 -32 z" horiz-adv-x="0" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -5,7 +5,7 @@
(function () {
"use strict";
// UTILS
// ## UTILS
/**
* Allows to check contents of each element exactly
@ -23,18 +23,92 @@
* Center an element to the window vertically and centrally
* @returns {*}
*/
$.fn.center = function () {
this.css({
'position': 'fixed',
'left': '50%',
'top': '50%'
$.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);
});
this.css({
'margin-left': -this.outerWidth() / 2 + 'px',
'margin-top': -this.outerHeight() / 2 + 'px'
};
// ## 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-transtion-duration')) {
return Math.round(parseFloat(this.css('-webkit-transtion-duration')) * 1000);
}
if (this.css('-ms-transtion-duration')) {
return Math.round(parseFloat(this.css('-ms-transtion-duration')) * 1000);
}
if (this.css('-moz-transtion-duration')) {
return Math.round(parseFloat(this.css('-moz-transtion-duration')) * 1000);
}
if (this.css('-o-transtion-duration')) {
return Math.round(parseFloat(this.css('-o-transtion-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']);
}
});
});
$(window).trigger('centered');
return this;
};
$.fn.selectText = function () {
@ -80,7 +154,9 @@
return this;
};
$('.overlay').hideAway(); // TODO: Move to a more sensible global file.
// ## GLOBALS
$('.overlay').hideAway();
/**
* Adds appropriate inflection for pluralizing the singular form of a word when appropriate.

View file

@ -2,29 +2,21 @@
(function ($) {
"use strict";
var UploadUi,
$loader = '<span class="media"><span class="hidden">Image Upload</span></span>' +
'<div class="description">Add image</div>' +
'<a class="image-url" title="Add image from URL"><span class="hidden">URL</span></a>' +
'<a class="image-webcam" title="Add image from webcam">' +
'<span class="hidden">Webcam</span></a>',
$progress = $('<div />', {
"class" : "js-upload-progress progress progress-success active",
"style": "opacity:0",
"role": "progressbar",
"aria-valuemin": "0",
"aria-valuemax": "100"
}).append($("<div />", {
"class": "js-upload-progress-bar bar",
"style": "width:0%"
}));
var UploadUi;
UploadUi = function ($dropzone, settings) {
var source,
$link = $('<a class="js-edit-image image-edit" href="#" >' +
'<img src="/public/assets/img/add-image.png" width="16" height="16" alt="add, edit"></a>'),
$back = $('<a class="js-return-image image-edit" href="#" >' +
'<img src="/public/assets/img/return-image.png" width="16" height="16" alt="add, edit"></a>');
$cancel = '<a class="image-cancel js-cancel"><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, {
bindFileUpload: function () {
@ -33,11 +25,13 @@
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
url: '/ghost/upload',
add: function (e, data) {
$dropzone.find('a.js-return-image').remove();
$progress.find('.js-upload-progress-bar').removeClass('fail');
$dropzone.trigger('uploadstart');
$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('span.media').after($progress);
$dropzone.find('div.js-fail').after($progress);
$progress.animate({opacity: 100}, 250);
}
data.submit();
@ -48,25 +42,25 @@
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 + '%');
if (data.loaded / data.total === 1) {
$progress.animate({opacity: 0}, 250, function () {
$dropzone.find('span.media').after('<img class="fileupload-loading" src="/public/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
});
}
}
},
fail: function (e, data) {
$dropzone.find('.js-upload-progress-bar').addClass('fail');
$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) {
function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').removeClass('fileupload-loading');
$dropzone.css({"height": "auto"});
if (!$dropzone.find('a.js-edit-image')[0]) {
$link.css({"opacity": 100});
$dropzone.find('.js-upload-target').after($link);
}
$dropzone.delay(250).animate({opacity: 100}, 1000, function () {
self.init();
});
@ -83,20 +77,46 @@
});
}
function preloadImage() {
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="/public/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
});
$dropzone.trigger("uploadsuccess", [data.result]);
$img.one('load', function () { animateDropzone($img); })
.attr('src', data.result);
}
preloadImage();
preLoadImage();
}
});
},
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('div.description, span.media, div.js-upload-progress, a.image-url, a.image-webcam')
.remove();
$dropzone.find('span.media, div.js-upload-progress, a.image-url, a.image-webcam, div.js-fail, button.js-fail, a.js-cancel').remove();
},
initWithDropzone: function () {
@ -104,20 +124,8 @@
//This is the start point if no image exists
$dropzone.find('img.js-upload-target').css({"display": "none"});
$dropzone.removeClass('pre-image-uploader').addClass('image-uploader');
if (!$dropzone.find('span.media')[0]) {
$dropzone.append($loader);
}
if ($dropzone.find('a.js-edit-image')[0]) {
$dropzone.find('a.js-edit-image').remove();
}
$back.on('click', function () {
$dropzone.find('a.js-return-image').remove();
$dropzone.find('img.js-upload-target').attr({"src": source}).css({"display": "block"});
self.removeExtras();
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
self.init();
});
this.removeExtras();
this.buildExtras();
this.bindFileUpload();
},
@ -126,34 +134,26 @@
// This is the start point if an image already exists
source = $dropzone.find('img.js-upload-target').attr('src');
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
if (!$dropzone.find('a.js-edit-image')[0]) {
$link.css({"opacity": 100});
$dropzone.find('.js-upload-target').after($link);
}
$link.on('click', function () {
$dropzone.find('a.js-edit-image').remove();
$dropzone.find('img.js-upload-target').attr({"src": ""}).css({"display": "none"});
$back.css({"cursor": "pointer", "z-index": 9999, "opacity": 100});
$dropzone.find('.js-upload-target').after($back);
self.init();
$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();
self.initWithDropzone();
});
},
init: function () {
var img;
// First check if field image is defined by checking for js-upload-target class
if ($dropzone.find('img.js-upload-target')[0]) {
if ($dropzone.find('img.js-upload-target').attr('src') === '') {
this.initWithDropzone();
} else {
this.initWithImage();
}
} else {
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="" />');
this.init();
}
if ($dropzone.find('img.js-upload-target').attr('src') === '') {
this.initWithDropzone();
} else {
this.initWithImage();
}
}
});
@ -162,9 +162,9 @@
$.fn.upload = function (options) {
var settings = $.extend({
progressbar: true,
editor: false
}, options);
progressbar: true,
editor: false
}, options);
return this.each(function () {
var $dropzone = $(this),

View file

@ -1 +1 @@
/* IE specific override styles. */
/* IE specific override styles. */

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
* These styles control elements specific to the post editor screen
* used for publishing content with Ghost.
*
* Table of Contents:
* Table of Contents:
*
* Editor / Preview
* Post Preview Content
@ -18,265 +18,283 @@
.editor {
// The main post title
.entry-title {
@extend %box;
height: 35px;
padding: 10px 15px;
margin-bottom: 15px;
position:relative;
// The main post title
.entry-title {
@extend %box;
height: 53px;
padding: 2px 15px;
margin-bottom: 5px;
position: relative;
@include breakpoint($mobile) {
box-shadow: none;
}
@include breakpoint($mobile) {
box-shadow: none;
}
input {
border: 0;
margin: 0;
padding:0;
font-size: 2em;
font-weight: 300;
width: 100%;
&:focus {
outline: 0;
}
}
input {
border: 0;
margin: 0;
padding: 0;
font-size: 3em;
font-weight: bold;
letter-spacing: -1px;
width: 100%;
background: transparent;
&:focus {
outline: 0;
}
}
}
}
.entry-container {
position: relative;
height: 100%;
}
// The two content panel wrappers, positioned left/right
.entry-markdown { left:0; border-right:$lightbrown 2px solid; }
.entry-preview { right:0; border-left:$lightbrown 2px solid; }
// The two content panel wrappers, positioned left/right
.entry-markdown { left: 0; border-right: $lightbrown 2px solid; }
.entry-preview { right: 0; border-left: $lightbrown 2px solid; }
// The visual styles for both panels
.entry-markdown, .entry-preview {
@include box-sizing(border-box);
width: 50%;
padding: 15px;
position: absolute;
bottom:40px; // height of the publish bar
top:69px; // height of the post title + margin
background: #fff;
box-shadow: $shadow;
// The visual styles for both panels
.entry-markdown, .entry-preview {
@include box-sizing(border-box);
width: 50%;
padding: 15px;
position: absolute;
bottom: 40px; // height of the publish bar
top: 61px; // height of the post title + margin
background: #fff;
box-shadow: $shadow;
@include breakpoint($mobile) {
box-shadow: none;
}
@include breakpoint($mobile) {
box-shadow: none;
}
// Convert all content areas to small boxes
@include breakpoint($netbook) {
top:109px;
left:0;
right:0;
width:100%;
border:none;
z-index:100;
min-height:380px;
.markdown, .entry-preview-content {
height:50px;
overflow: hidden;
}
}
// Convert all content areas to small boxes
@include breakpoint($netbook) {
top: 109px;
left: 0;
right: 0;
width: 100%;
border: none;
z-index: 100;
min-height: 380px;
.markdown, .entry-preview-content {
height: 50px;
overflow: hidden;
}
}
.floatingheader {
.floatingheader {
// Turn headers into tabs which act as links
// both headers set to grey/inactive colour
@include breakpoint($netbook) {
cursor:pointer;
width:50%;
border-right:$lightbrown 2px solid;
color:#fff;
font-weight: normal;
background:$brown;
position:absolute;
top:-40px;
left:0;
box-shadow: rgba(0,0,0,0.1) 0 -2px 3px inset;
// Turn headers into tabs which act as links
// both headers set to grey/inactive colour
@include breakpoint($netbook) {
cursor: pointer;
width: 50%;
border-right: $lightbrown 2px solid;
color: #fff;
font-weight: normal;
background: $brown;
position: absolute;
top: -40px;
left: 0;
box-shadow: rgba(0,0,0,0.1) 0 -2px 3px inset;
a {
color:#fff;
}
}
a {
color: #fff;
}
}
a {
color: $brown;
}
a {
color: $brown;
}
.markdown-help {
.markdown-help {
position: relative;
top: -5px;
right: -5px;
@include icon($i-question, '', lighten($brown, 15%));
float:right;
@include icon($i-question, '', lighten($brown, 15%));
float: right;
padding: 5px;
&:hover {
@include icon($i-question, '', $brown);
}
}
&:hover {
@include icon($i-question, '', $brown);
}
}
.entry-word-count {
float:right;
}
.entry-word-count {
float: right;
}
}
}
// Give the tab with the .active class the highest z-index
&.active {
z-index: 200;
}
// Give the tab with the .active class the highest z-index
&.active {
z-index: 200;
}
// Restore the normal height of the .active tab (inactive tab stays small, hidden behind)
&.active .markdown,
&.active .entry-preview-content {
height:auto;
overflow: auto;
}
// Restore the normal height of the .active tab (inactive tab stays small, hidden behind)
&.active .markdown,
&.active .entry-preview-content {
height: auto;
overflow: auto;
}
// Restore the white bg of the currently .active tab, remove hand cursor from currently active tab
&.active header {
@include breakpoint($netbook) {
cursor:auto;
color: $brown;
background:#fff;
box-shadow: none;
a {
color: $brown;
}
}
}
// Restore the white bg of the currently .active tab, remove hand cursor from currently active tab
&.active header {
@include breakpoint($netbook) {
cursor: auto;
color: $brown;
background: #fff;
box-shadow: none;
a {
color: $brown;
}
}
}
// Hide markdown icon + wordcount when we hit mobile
@include breakpoint($mobile) {
.markdown-help,
.entry-word-count {
display: none;
}
}
// Hide markdown icon + wordcount when we hit mobile
@include breakpoint($mobile) {
.markdown-help,
.entry-word-count {
display: none;
}
}
}
}
.entry-markdown-content {
.entry-markdown-content {
textarea {
border: 0;
width: 100%;
height: 100%;
max-width: 100%;
margin: 0;
padding: 0;
position:absolute;
top: 0;
right:0;
bottom:0;
left:0;
textarea {
border: 0;
width: 100%;
height: 100%;
max-width: 100%;
margin: 0;
padding: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
&:focus {
outline: 0;
}
}
&:focus {
outline: 0;
}
}
.CodeMirror {
height: auto;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
font-family: $font-family-mono;
font-size:1.1em;
line-height:1.2em;
color: lighten($darkgrey, 30%);
.CodeMirror {
height: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-family: $font-family-mono;
font-size: 1.4em;
line-height: 1.3em;
color: lighten($darkgrey, 10%);
.CodeMirror-focused,
.CodeMirror-selected {
color: $darkgrey;
background: lighten($blue, 20%);
text-shadow: none;
}
.CodeMirror-focused,
.CodeMirror-selected {
color: $darkgrey;
background: lighten($blue, 20%);
text-shadow: none;
}
::selection {
color: $darkgrey;
background: lighten($blue, 20%);
text-shadow: none;
}
}
::selection {
color: $darkgrey;
background: lighten($blue, 20%);
text-shadow: none;
}
}
.CodeMirror-lines {
padding: 65px 0 40px 0; /* Vertical padding around content */
@include breakpoint($netbook) {padding-top: 25px;}
@include breakpoint($mobile) {padding: 15px 0;}
}
.CodeMirror pre {
padding: 0 40px; /* Horizontal padding of content */
@include breakpoint($mobile) {padding: 0 15px;}
}
.CodeMirror-lines {
padding: 65px 0 40px 0; /* Vertical padding around content */
@include breakpoint($netbook) {padding-top: 25px;}
@include breakpoint($mobile) {padding: 15px 0;}
}
.CodeMirror pre {
padding: 0 40px; /* Horizontal padding of content */
@include breakpoint($mobile) {padding: 0 15px;}
}
.cm-header {
color:#000;
font-size: 1.4em;
line-height: 1.4em;
}
.cm-header {
color: #000;
font-size: 1.4em;
line-height: 1.4em;
font-weight: bold;
}
.cm-string,
.cm-link,
.cm-comment,
.cm-quote {color:#000;}
.cm-variable-2,
.cm-variable-3,
.cm-keyword {
color: lighten($darkgrey, 10%);
}
}
.cm-string,
.cm-strong,
.cm-link,
.cm-comment,
.cm-quote,
.cm-number,
.cm-atom,
.cm-tag {
color: #000;
font-weight: bold;
}
.entry-preview {
// Align the tab of entry-preview on the right
.floatingheader {
@include breakpoint($netbook) {
right:0;
left:auto;
border-right:none;
border-left:$lightbrown 2px solid;
}
}
}
.entry-preview-content {
position:absolute;
top:0;
right:0;
bottom:0;
left:0;
padding: 60px 40px 40px 40px;
overflow: auto;
.entry-preview {
// Align the tab of entry-preview on the right
.floatingheader {
@include breakpoint($netbook) {
right: 0;
left: auto;
border-right: none;
border-left: $lightbrown 2px solid;
}
}
// Tweak padding for smaller screens
@include breakpoint($netbook) {padding-top: 20px;}
@include breakpoint($mobile) {padding: 15px;}
}
}
.entry-preview-content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 60px 40px 40px 40px;
overflow: auto;
word-break: break-word;
hyphens: auto;
// Special case, when scrolling, add shadows to content headers.
.scrolling {
// Tweak padding for smaller screens
@include breakpoint($netbook) {padding-top: 20px;}
@include breakpoint($mobile) {padding: 15px;}
}
}
.floatingheader {
@include breakpoint($netbook) {
box-shadow: none;
}
// Special case, when scrolling, add shadows to content headers.
.scrolling {
&::before,
&::after {
@include breakpoint($netbook) {display:none;}
}
}
.CodeMirror-scroll,
.entry-preview-content {
@include breakpoint($netbook) {
box-shadow: 0 5px 5px rgba(0,0,0,0.05) inset;
}
}
}
.floatingheader {
@include breakpoint($netbook) {
box-shadow: none;
}
&::before,
&::after {
@include breakpoint($netbook) {display: none;}
}
}
.CodeMirror-scroll,
.entry-preview-content {
@include breakpoint($netbook) {
box-shadow: 0 5px 5px rgba(0,0,0,0.05) inset;
}
}
}
}//.editor
@ -289,60 +307,56 @@
// TODO: These should just be defaults, overridden by editor.hbs in theme dir
.entry-preview-content,
.content-preview-content {
font-size:1.15em;
line-height: 1.5em;
font-size: 1.4em;
line-height: 1.5em;
a {
color: $blue;
text-decoration: underline;
}
p {
text-indent: 0;
margin: 1.2em 0 1.6em;
&:first-child {
margin-top:0;
}
}
h1 {
font-size: 3em;
}
h2 {
font-size: 2.2em;
}
h3 {
font-size: 1.8em;
}
.btn {
text-decoration: none;
color: $grey;
}
.img-placeholder {
border: 5px dashed $grey;
height: 100px;
position: relative;
span {
display: block;
height: 30px;
position: absolute;
margin-top: -15px;
top: 50%;
width: 100%;
text-align: center;
}
}
a {
color: $blue;
text-decoration: underline;
}
p {
margin: 1.2em 0 1.6em;
&:first-child {
margin-top: 0;
}
}
h1 {
font-size: 3em;
}
h2 {
font-size: 2.2em;
}
h3 {
font-size: 1.8em;
}
.btn {
text-decoration: none;
color: $grey;
}
.img-placeholder {
border: 5px dashed $grey;
height: 100px;
position: relative;
span {
display: block;
height: 30px;
position: absolute;
margin-top: -15px;
top: 50%;
width: 100%;
text-align: center;
}
}
a {
&.image-edit {
width: 16px;
height: 16px;
}
}
img {
width: 100%;
height: auto;
}
// prevent uploaded image from being streched in editor
.pre-image-uploader img {
width: auto;
img {
max-width: 100%;
height: auto;
margin: 0 auto;
}
}
@ -353,17 +367,17 @@
============================================================================= */
body.zen {
background: lighten($lightbrown, 3%);
#usermenu {display:none;}
#global-header, #publish-bar {
opacity: 0;
height:0;
overflow: hidden;
@include transition(all 0.5s ease-out);
}
background: lighten($lightbrown, 3%);
#usermenu {display: none;}
#global-header, #publish-bar {
opacity: 0;
height: 0;
overflow: hidden;
@include transition(all 0.5s ease-out);
}
main {top: 15px;@include transition(all 0.5s ease-out);}
.entry-markdown, .entry-preview {bottom:0;@include transition(all 0.5s ease-out);}
main {top: 15px;@include transition(all 0.5s ease-out);}
.entry-markdown, .entry-preview {bottom: 0;@include transition(all 0.5s ease-out);}
}
@ -373,121 +387,115 @@ body.zen {
============================================================================= */
#publish-bar {
@include box-sizing(border-box);
height:40px;
padding: 0;
color: $midgrey;
background: darken($darkgrey, 4%);
position: fixed;
bottom: 0;
left:0;
right:0;
z-index:900;
box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
@include box-sizing(border-box);
height: 40px;
padding: 0;
color: $midgrey;
background: darken($darkgrey, 4%);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 900;
box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
@include breakpoint($netbook) {font-weight:normal;}
@include breakpoint($netbook) {font-weight: normal;}
button {
min-height: 30px;
height: 30px;
line-height: 12px;
padding: 0 10px;
margin-top: 5px;
border-top:rgba(255,255,255,0.4) 1px solid;
}
button {
min-height: 30px;
height: 30px;
line-height: 12px;
padding: 0 10px;
margin-top: 5px;
border-top: rgba(255,255,255,0.4) 1px solid;
}
.button-link { border-top: none; }
.button-link { border-top: none; }
.options {
width:30px;
min-height: 30px;
height: 30px;
margin-top:5px;
box-shadow: rgba(255,255,255,0.4) 0 1px 0 inset;
.options {
width: 30px;
min-height: 30px;
height: 30px;
margin-top: 5px;
box-shadow: rgba(255,255,255,0.4) 0 1px 0 inset;
}
&.up:hover {
@include icon($i-chevron-down) {
margin-top:-5px;
@include transform(rotate(540deg));
@include transition(transform 0.6s ease);
};
.splitbutton-save{
.button-save{
@include transition(width 0.25s ease);
}
}
.splitbutton-save{
.button-save{
@include transition(width 0.25s ease);
}
.editor-options{
@extend %menu;
@extend %menu-right;
bottom: 140%;
right: -3%;
.editor-options{
@extend %menu;
@extend %menu-right;
bottom: 140%;
right: -3%;
a{
font-size: 14px;
}
}
}
a{
font-size: 14px;
}
}
}
}
#entry-categories {
position: absolute;
top:0;
left:0;
right: 165px;
bottom:0;
text-transform: none;
padding: 10px 0 0 0;
#entry-tags {
position: absolute;
top: 0;
left: 0;
right: 165px;
bottom: 0;
text-transform: none;
padding: 10px 0 0 0;
}
.category-label {
display: block;
float:left;
@include icon($i-tag);
padding:1px 8px 0 8px;
@include transition;
.tag-label {
display: block;
float: left;
@include icon($i-tag);
padding: 1px 8px 0 8px;
@include transition;
&:hover {
cursor:pointer;
color: $lightgrey;
}
&:hover {
cursor: pointer;
color: $lightgrey;
}
}
.category-input {
display: inline-block;
color: $lightgrey;
font-weight: 300;
background: transparent;
border: none;
.tag-input {
display: inline-block;
color: $lightgrey;
font-weight: 300;
background: transparent;
border: none;
&:focus {outline:none;}
&:focus {outline: none;}
}
.category {
@include icon-after($i-x, 8px, $darkgrey) {
margin-left:4px;
vertical-align:5%;
text-shadow: rgba(255,255,255,0.15) 0 1px 0;
@include transition;
}
display: block;
float: left;
margin-right: 5px;
padding: 0 5px;
color: $lightgrey;
background: lighten($grey, 15%);
border-radius: $rounded;
box-shadow:
rgba(255,255,255,0.2) 0 1px 0 inset,
#000 0 1px 3px;
.tag {
@include icon-after($i-x, 8px, $darkgrey) {
margin-left: 4px;
vertical-align: 5%;
text-shadow: rgba(255,255,255,0.15) 0 1px 0;
@include transition;
}
display: block;
float: left;
margin-right: 5px;
padding: 0 5px;
color: $lightgrey;
background: lighten($grey, 15%);
border-radius: $rounded;
box-shadow:
rgba(255,255,255,0.2) 0 1px 0 inset,
#000 0 1px 3px;
&:hover {
cursor: pointer;
@include icon-after($i-x, 8px, $lightgrey) {text-shadow: none;}
@include user-select(none);
}
&:hover {
cursor: pointer;
@include icon-after($i-x, 8px, $lightgrey) {text-shadow: none;}
}
}
.suggestions {
@ -496,47 +504,52 @@ body.zen {
li.selected{
background: $blue;
box-shadow:
box-shadow:
rgba(255,255,255,0.2) 0 1px 0 inset,
rgba(0,0,0,0.5) 0 1px 5px;
}
li a {
padding-left: 25px;
}
mark{
background: none;
color: white;
font-weight: bold;
}
}
#entry-settings {
@include icon($i-settings, 1.1em){vertical-align:0;};
@include box-sizing(border-box);
display:inline-block;
padding: 0 10px;
line-height: 1.8em;
color: $midgrey;
@include transition;
@include icon($i-settings, 1.1em){vertical-align: 0;};
@include box-sizing(border-box);
display: inline-block;
padding: 0 10px;
line-height: 1.8em;
color: $midgrey;
@include transition;
&:hover {
color: $lightgrey;
}
&:hover {
color: $lightgrey;
}
}
#entry-settings-menu {
position: absolute;
bottom:50px;
right:-5px;
position: absolute;
bottom: 50px;
right: -5px;
}
#entry-actions {
margin-right:6px;
position: relative;
margin-right: 6px;
position: relative;
}
#entry-actions-menu {
position: absolute;
bottom:50px;
right:-5px;
bottom: 50px;
right: -5px;
}
/* =============================================================================

View file

@ -0,0 +1,80 @@
/*
* These styles control elements specific to the error screens
*
* Table of Contents:
*
* General
* 404
*/
/* =============================================================================
General
============================================================================= */
.error-content {
max-width: 530px;
margin: 0 auto;
padding: 0;
@include breakpoint(630px) {
max-width: 264px;
text-align: center;
}
}
.error-image {
display: inline-block;
vertical-align: middle;
width: 96px;
height: 150px;
@include breakpoint(630px) {
width: 72px;
height: 112px;
}
img {
width: 100%;
height: 100%;
}
}
.error-message {
position: relative;
top: -5px;
display: inline-block;
vertical-align: middle;
margin-left: 10px;
}
.error-code {
margin: 0;
font-size: 7.8em;
line-height: 0.9em;
color: #979797;
@include breakpoint(630px) {
font-size: 5.8em;
}
}
.error-description {
margin: 0;
padding: 0;
font-weight: 300;
font-size: 1.9em;
color: #979797;
border: none;
@include breakpoint(630px) {
font-size: 1.4em;
}
}
/* =============================================================================
404
============================================================================= */
.error-404 {
width: 300px;
}

View file

@ -19,8 +19,7 @@
}
main {
top: 15px;
@include breakpoint($mobile) { top: 0; }
top: 0;
}
}//.ghost-login

View file

@ -19,6 +19,10 @@
position: relative;
height: 100%;
width: 100%;
@include breakpoint($tablet) {
overflow-x: hidden;
}
}
.content-list {
@include box-sizing(border-box);
@ -32,9 +36,6 @@
background: #fff;
box-shadow: $shadow;
@include breakpoint(900px) {
width:300px;
}
@include breakpoint($tablet) {
width:auto;
right:0;
@ -182,9 +183,11 @@
border-left:$lightbrown 2px solid;
background: #fff;
box-shadow: $shadow;
@include breakpoint(900px) {
@include breakpoint($tablet) {
width: auto;
left: 300px;
left: 100%;
right: -100%;
margin-left: 15px;
}
.unfeatured {
@ -213,6 +216,8 @@
left:0;
overflow: auto;
padding: 80px 40px;
word-break: break-word;
hyphens: auto;
.wrapper {
max-width: 700px;

View file

@ -0,0 +1,181 @@
/* =============================================================================
Plugins
============================================================================= */
.settings {
.plugin-section {
padding-bottom: 20px;
}
.plugin-section-header {
h3 {
margin: 15px 0;
font-size: 1.1em;
font-weight: normal;
color: $brown;
}
}
.plugin-section-footer {
text-align: right;
}
.button-update-all {
@include icon($i-lightning, 1em, #FFC125) {
margin-right: 5px;
};
}
.button-cancel {
@include icon($i-x, 1em, #fff) {
margin-right: 5px;
};
}
.plugin-section-table {
margin-top: 5px;
tbody > tr:nth-child(odd) > td {
background: none;
}
.plugin-section-item {
&.inactive {
.plugin-meta {
opacity: 0.4;
}
td:last-child {
.plugin-meta {
opacity: 1;
}
}
}
td {
padding: 20px 0;
border-bottom:$lightbrown 1px solid;
&:first-child {
padding-left: 0px;
border-top:$lightbrown 1px solid;
.plugin-meta {
padding: 0px;
width: 75%;
border-left: none;
text-align: left;
}
}
&:last-child {
.plugin-meta {
padding: 0px;
text-align: right;
}
}
}
}
.plugin-icon {
display: inline-block;
width: 40px;
height: 40px;
margin-right: 15px;
background: #FFC125;
border-radius: 5px;
vertical-align: middle;
img {
width: 100%;
}
}
.plugin-meta {
@include box-sizing(border-box);
display: inline-block;
width: 100%;
height: 100%;
padding: 0 20px;
vertical-align: middle;
border-left: $lightbrown 1px solid;
text-align: center;
}
.plugin-info {
display: block;
color: lighten($grey, 5%);
font-size: 1.2em;
font-weight: normal;
vertical-align: top;
}
.plugin-title {
color: $grey;
}
.plugin-sub-info {
display: block;
color: $midgrey;
}
.plugin-download-progress {
position: relative;
display: block;
height: 6px;
margin-top: 10px;
background: $lightbrown;
border-radius: 3px;
> span {
position: absolute;
left: 0;
top: 0;
content: "";
height: 100%;
background-color: $blue;
border-radius: 3px;
}
}
.rating {
unicode-bidi: bidi-override;
text-align: center;
> span {
display: inline-block;
position: relative;
width: 1.1em;
height: 1.1em;
font-size: 0.8em;
&:before {
content: "\2605";
position: absolute;
left: 0;
opacity: 0.5;
}
&.active {
&:before {
content: "\2605";
opacity: 1;
}
}
}
}
.plugin-settings-icon {
display: block;
margin-top: 9px;
font-size: 1.4em;
@include icon($i-settings, 1em, $grey);
}
} //.plugin-section-table
} //.settings

View file

@ -23,6 +23,10 @@
height: 100%;
margin:0;
padding:0;
@include breakpoint($tablet) {
overflow-x: hidden;
}
}
.title {
@ -48,6 +52,7 @@
left:0;
bottom:0;
z-index: 700;
background: #FFFFFF;
box-shadow: $lightbrown 1px 0 0;
@include breakpoint($tablet) {
width:100%;
@ -157,7 +162,19 @@
right:0;
left:20%;
bottom:0;
@include breakpoint($tablet) { display: none; }
background: #FFFFFF;
@include breakpoint($tablet) {
display: none;
width: 100%;
left: 100%;
right: -100%;
margin-left: 15px;
}
img {
max-width: 100%;
}
display: none;
&.active {display:block;}
@ -225,417 +242,6 @@
@include breakpoint($letterbox) { top: 0; }
}
/* =============================================================================
Users List
============================================================================= */
.user-group-header {
margin-bottom: 0px;
padding-bottom: 20px;
border: 0 none;
border-bottom: 1px solid darken($lightbrown, 10%);
h3 {
display: inline-block;
margin: 0;
color: $midbrown;
font-weight: normal;
font-size: 1.1em;
line-height: 1em;
}
}
.user-search {
display: inline-block;
float: right;
label { margin: 0}
&:hover .user-search-input, .user-search-input:focus {
width: 260px;
padding: 0 10px;
}
.user-search-input {
@include box-sizing(border-box);
width: 0px;
padding: 0;
border: none;
border-bottom: lighten($lightbrown, 2%) 1px solid;
@include transition(width 0.2s ease-in-out);
box-shadow: none;
}
.search-icon {
@include icon($i-search, 1em, $midbrown);
}
}
.users {
padding: 0px;
margin-top: 0px;
list-style: none;
}
.user {
@include box-sizing(border-box);
display: block;
width: 100%;
padding: 20px;
border: 0 none;
border-top: 1px solid $lightgrey;
&:first-child {
border: none;
}
.user-image {
display: inline-block;
width: 40px;
height: 40px;
margin-right: 17px;
vertical-align: middle;
background-color: $lightbrown;
border-radius: 20px;
&.invite {
@include box-sizing(border-box);
padding-top: 8px;
text-align: center;
@include icon($i-mail, 1em, $brown);
}
img {
width: 40px;
height: 40px;
border-radius: 20px;
}
}
.user-meta {
display: inline-block;
vertical-align: middle;
.user-name {
margin: 0;
margin-top: 0.4em;
font-weight: 400;
font-size: 1.2em;
line-height: 1em;
}
.user-last-seen {
line-height: 1em;
}
}
}
.user-role {
padding: 2px 8px;
float: right;
font-size: 0.8em;
color: #fff;
text-transform: uppercase;
&.admin {
background-color: #DE523A;
}
&.editor {
background-color: #4A8CBD;
}
}
/* =============================================================================
User Profile
============================================================================= */
.user-profile-header {
position: relative;
width: 100%;
height: 300px;
}
.cover-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
img {
position: absolute;
max-width: 100%;
}
}
.button-change-cover {
position: absolute;
right: 40px;
bottom: 40px;
background: rgba(0,0,0,0.4);
color: #FFFFFF;
z-index: 2;
&:hover {
background: rgba(0,0,0,0.8);
}
}
.user-details-container {
position: relative;
top: -90px;
z-index: 1;
}
.user-details-top {
margin-bottom: 0;
overflow: auto;
p {
color: #FFFFFF;
}
}
.user-avatar-image {
@include box-sizing(border-box);
position: relative;
width: 120px;
height: 120px;
float: left;
margin-left: 40px;
margin-right: 20px;
border-radius: 60px;
border: 3px solid #FFFFFF;
cursor: pointer;
z-index: 2;
img {
width: 100%;
height: 100%;
border-radius: 60px;
}
.button-change-avatar {
position: absolute;
top: 0;
width: 100%;
height: 100%;
display: block;
border-radius: 60px;
background: rgba(0,0,0,0.5);
opacity: 0;
color: #FFFFFF;
@include transition(opacity 0.3s ease);
}
&:hover {
.button-change-avatar {
opacity: 1;
}
}
}
.user-details-bottom {
padding: 0 40px;
margin: 0;
}
.bio-container {
max-width: 370px;
}
.bio-desc {
display: inline-block;
}
.word-count {
margin-right: 20px;
float: right;
font-weight: bold;
color: darken($brown, 5%);
}
/* =============================================================================
User Profile
============================================================================= */
.plugin-section {
padding-bottom: 20px;
}
.plugin-section-header {
h3 {
margin: 15px 0;
font-size: 1.1em;
font-weight: normal;
color: $brown;
}
}
.plugin-section-footer {
text-align: right;
}
.button-update-all {
@include icon($i-power, 1em, #FFC125){ // TODO: Need lightening icon
margin-right: 5px;
};
}
.button-cancel {
@include icon($i-x, 1em, #FFFFFF){
margin-right: 5px;
};
}
.plugin-section-table {
margin-top: 5px;
tbody > tr:nth-child(odd) > td {
background: none;
}
.plugin-section-item {
&.inactive {
.plugin-meta {
opacity: 0.4;
}
td:last-child {
.plugin-meta {
opacity: 1;
}
}
}
td {
padding: 20px 0;
border-bottom:$lightbrown 1px solid;
&:first-child {
padding-left: 0px;
border-top:$lightbrown 1px solid;
.plugin-meta {
padding: 0px;
width: 75%;
border-left: none;
text-align: left;
}
}
&:last-child {
.plugin-meta {
padding: 0px;
text-align: right;
}
}
}
}
.plugin-icon {
display: inline-block;
width: 40px;
height: 40px;
margin-right: 15px;
background: #FFC125;
border-radius: 5px;
vertical-align: middle;
img {
width: 100%;
}
}
.plugin-meta {
@include box-sizing(border-box);
display: inline-block;
width: 100%;
height: 100%;
padding: 0 20px;
vertical-align: middle;
border-left: $lightbrown 1px solid;
text-align: center;
}
.plugin-info {
display: block;
color: lighten($grey, 5%);
font-size: 1.2em;
font-weight: normal;
vertical-align: top;
}
.plugin-title {
color: $grey;
}
.plugin-sub-info {
display: block;
color: $midgrey;
}
.plugin-download-progress {
position: relative;
display: block;
height: 6px;
margin-top: 10px;
background: $lightbrown;
border-radius: 3px;
> span {
position: absolute;
left: 0;
top: 0;
content: "";
height: 100%;
background-color: $blue;
border-radius: 3px;
}
}
.rating {
unicode-bidi: bidi-override;
text-align: center;
> span {
display: inline-block;
position: relative;
width: 1.1em;
height: 1.1em;
font-size: 0.8em;
&:before {
content: "\2605";
position: absolute;
left: 0;
opacity: 0.5;
}
&.active {
&:before {
content: "\2605";
opacity: 1;
}
}
}
}
.plugin-settings-icon {
display: block;
margin-top: 9px;
font-size: 1.4em;
@include icon($i-settings, 1em, $grey);
}
} //.plugin-section-table
}//.settings-content
}//.settings

View file

@ -0,0 +1,248 @@
/* =============================================================================
Users List
============================================================================= */
.settings {
.user-group-header {
margin-bottom: 0px;
padding-bottom: 20px;
border: 0 none;
border-bottom: 1px solid darken($lightbrown, 10%);
h3 {
display: inline-block;
margin: 0;
color: $midbrown;
font-weight: normal;
font-size: 1.1em;
line-height: 1em;
}
}
.user-search {
display: inline-block;
float: right;
label { margin: 0}
&:hover .user-search-input, .user-search-input:focus {
width: 260px;
padding: 0 10px;
}
.user-search-input {
@include box-sizing(border-box);
width: 0px;
padding: 0;
border: none;
border-bottom: lighten($lightbrown, 2%) 1px solid;
@include transition(width 0.2s ease-in-out);
box-shadow: none;
}
.search-icon {
@include icon($i-search, 1em, $midbrown);
}
}
.users {
padding: 0px;
margin-top: 0px;
list-style: none;
}
.user {
@include box-sizing(border-box);
display: block;
width: 100%;
padding: 20px;
border: 0 none;
border-top: 1px solid $lightgrey;
&:first-child {
border: none;
}
.user-image {
display: inline-block;
width: 40px;
height: 40px;
margin-right: 17px;
vertical-align: middle;
background-color: $lightbrown;
border-radius: 20px;
&.invite {
@include box-sizing(border-box);
padding-top: 8px;
text-align: center;
@include icon($i-mail, 1em, $brown);
}
img {
width: 40px;
height: 40px;
border-radius: 20px;
}
}
.user-meta {
display: inline-block;
vertical-align: middle;
.user-name {
margin: 0;
margin-top: 0.4em;
font-weight: 400;
font-size: 1.2em;
line-height: 1em;
}
.user-last-seen {
line-height: 1em;
}
}
}
.user-role {
padding: 2px 8px;
float: right;
font-size: 0.8em;
color: #fff;
text-transform: uppercase;
&.admin {
background-color: #DE523A;
}
&.editor {
background-color: #4A8CBD;
}
}
/* =============================================================================
User Profile
============================================================================= */
.user-profile-header {
position: relative;
width: 100%;
height: 300px;
&:after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 110px;
@include linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,0.6) 100%);
}
}
.cover-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
img {
position: absolute;
min-width: 100%;
min-height: 100%;
}
}
.button-change-cover {
position: absolute;
right: 40px;
bottom: 40px;
background: rgba(0,0,0,0.4);
border-radius: 0;
color: #FFFFFF;
z-index: 2;
&:hover {
background: rgba(0,0,0,0.8);
}
}
.user-details-container {
position: relative;
top: -90px;
z-index: 1;
}
.user-details-top {
margin-bottom: 0;
overflow: auto;
p {
color: #FFFFFF;
}
}
.user-avatar-image {
@include box-sizing(border-box);
position: relative;
width: 120px;
height: 120px;
float: left;
margin-left: 40px;
margin-right: 20px;
border-radius: 60px;
border: 3px solid #FFFFFF;
cursor: pointer;
z-index: 2;
img {
width: 100%;
height: 100%;
border-radius: 60px;
}
.button-change-avatar {
position: absolute;
top: 0;
width: 100%;
height: 100%;
display: block;
border-radius: 60px;
background: rgba(0,0,0,0.5);
opacity: 0;
color: #FFFFFF;
@include transition(opacity 0.3s ease);
}
&:hover {
.button-change-avatar {
opacity: 1;
}
}
}
.user-details-bottom {
padding: 0 40px;
margin: 0;
}
.bio-container {
max-width: 370px;
}
.bio-desc {
display: inline-block;
}
.word-count {
margin-right: 20px;
float: right;
font-weight: bold;
color: darken($brown, 5%);
}
} //.settings

View file

@ -12,20 +12,20 @@
============================================================================= */
@-webkit-keyframes off-canvas {
0% { left:0; }
100% { left:300px; }
0% { left:0; }
100% { left:300px; }
}
@-moz-keyframes off-canvas {
0% { opacity: 0; }
100% { opacity: 1; }
0% { opacity: 0; }
100% { opacity: 1; }
}
@-o-keyframes off-canvas {
0% { opacity: 0; }
100% { opacity: 1; }
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes off-canvas {
0% { opacity: 0; }
100% { opacity: 1; }
0% { opacity: 0; }
100% { opacity: 1; }
}
@include keyframes(fadeIn) {

View file

@ -280,10 +280,26 @@ input[type="reset"] {
right: 50%;
margin-top: -3px;
margin-right: -5px;
@include transition(transform 0.3s ease, margin-top 0.3s ease);
@include transition(margin-top 0.3s ease);
/* Transition of transform properties are split out due to a
defect in the vendor prefixing of transform transitions.
See: http://github.com/thoughtbot/bourbon/pull/86 */
@include transition-property(transform);
@include transition-duration(0.3);
@include transition-timing-function(ease);
};
// Spin the arrow on hover
// Keep the arrow spun when the associated menu is open
&.active:before {
@include transform(rotate(360deg));
}
&.up.active:before {
margin-top:-4px;
@include transform(rotate(540deg));
}
// Spin the arrow on hover and while menu is open
&:hover {
box-shadow: none;
background: #f8f8f8;
@ -297,7 +313,9 @@ input[type="reset"] {
@include icon($i-chevron-down) {
margin-top:-4px;
@include transform(rotate(540deg));
@include transition(transform 0.6s ease);
@include transition-property(transform);
@include transition-duration(0.6);
@include transition-timing-function(ease);
};
}
}
@ -322,7 +340,7 @@ input[type="reset"] {
@extend %splitbutton;
.options {
background: darken($blue, 5%);
&:hover {background: darken($blue, 10%);}
&:hover, &.active {background: darken($blue, 10%);}
}
}

View file

@ -10,6 +10,8 @@
* Global Navigation
* Mobile Navigation
* Drop-down / Pop-up Menu
* Notifications
* Modals
* Main Elements
* Floating Headers
* Image Uploader
@ -62,6 +64,10 @@
Global Styles
========================================================================== */
html {
font: normal 81.2%/1.65 "Open Sans", sans-serif;
}
body {
width:100%;
color: $darkgrey;
@ -96,6 +102,9 @@ article aside {
h1, h2, h3,
h4, h5, h6 {
color: $darkgrey;
text-rendering: optimizeLegibility;
line-height: 1;
margin-top: 0;
}
h2 {
@ -169,11 +178,6 @@ ul ol, ol ul {
margin: 0.4em 0;
}
p + p,
aside + p {
text-indent: 1.5em;
}
a {
color:$blue;
text-decoration:none;
@ -253,6 +257,7 @@ mark {
code, tt {
font-family: $font-family-mono;
font-size: 0.85em;
white-space: pre;
background: lighten($lightbrown, 2%);
border: 1px solid darken($lightbrown, 8%);
border-radius: 2px;
@ -268,11 +273,14 @@ pre {
padding: 10px;
font-family: $font-family-mono;
font-size: 0.9em;
white-space: pre;
overflow: auto;
border-radius: 3px;
code, tt {
font-size: inherit;
white-space: -moz-pre-wrap;
white-space: pre-wrap;
background: transparent;
border: none;
padding: 0;
@ -290,8 +298,8 @@ kbd {
font-weight: bold;
background: #f4f4f4;
border-radius: 4px;
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.2),
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.2),
0 1px 0 0 #fff inset;
}
@ -375,24 +383,25 @@ nav {
/* ==========================================================================
Main Navigation
========================================================================== */
#global-header { /* Added because the code editor was breaking for the title "Ghost" - this also happens on TryGhost */
#ghost {
@include icon($i-ghost);
display: block;
float:left;
height:40px;
padding:12px 15px;
color: lighten($grey, 10%);
@include box-sizing(border-box);
&:hover {text-decoration:none;}
}
#ghost:hover {
color: $lightgrey;
background:darken($darkgrey, 2%);
}
.ghost-logo {
@include icon($i-ghost);
display: block;
float:left;
height:40px;
padding:12px 15px;
color: lighten($grey, 10%);
@include box-sizing(border-box);
&:hover {text-decoration:none;}
}
.ghost-logo:hover {
color: $lightgrey;
background:darken($darkgrey, 2%);
}
.navbar {
height: 40px;
@ -573,7 +582,7 @@ nav {
#global-header {
@include breakpoint(650px) {
#ghost {
.ghost-logo {
@include icon($i-menu, 14px);
height:40px;
width: 40px;
@ -741,7 +750,7 @@ nav {
&:hover {
background: $blue;
box-shadow:
box-shadow:
rgba(255,255,255,0.2) 0 1px 0 inset,
rgba(0,0,0,0.5) 0 1px 5px;
}
@ -826,6 +835,9 @@ nav {
/* ==========================================================================
Notifications
========================================================================== */
.js-bb-notification {
@include transform(translateZ(0));
}
%notification, .notification {
@include box-sizing(border-box);
@ -834,9 +846,10 @@ nav {
padding: 10px 43px 10px 57px;
margin: 0 0 15px 0;
color: rgba(255,255,255,0.9);
background: $orange;
background: $blue;
position:relative;
box-shadow: $shadow;
@include transform(translateZ(0));
@include icon($i-notification) {
position: absolute;
@ -885,7 +898,14 @@ nav {
background: $red;
}
.notification-alert {
.notification-warn {
@extend %notification;
@include icon($i-info);
background: $orange;
}
.notification-info {
@extend %notification;
@include icon($i-info);
background: $blue;
@ -902,6 +922,8 @@ nav {
left: 0;
right: 0;
z-index: 999;
@include transition(all 0.15s linear 0s);
@include transform(translateZ(0));
&.dark {
background: rgba(0,0,0,0.4);
@ -920,8 +942,14 @@ nav {
}
body.blur > *:not(#modal-container) {
@include transition(all 0.15s linear 0s);
-webkit-filter: blur(2px);
filter: blur(2px); // Not used yet
-moz-filter: blur(2px);
-ms-filter: blur(2px);
-o-filter: blur(2px);
filter: blur(2px);
@include transform(translateZ(0));
}
%modal, .modal {
@ -935,8 +963,13 @@ body.blur > *:not(#modal-container) {
overflow:auto;
z-index: 1001;
&.fadeIn {
@include animation(fadeIn 0.3s linear 1);
&.fade {
opacity: 0;
@include transition(opacity 0.2s linear 0s);
&.in {
opacity: 1;
}
}
}
@ -982,13 +1015,12 @@ body.blur > *:not(#modal-container) {
}
.modal-content {
padding: 20px;
padding: 0 20px;
}
.modal-footer {
padding: 20px;
padding-top: 0;
text-align: center;
}
.modal-style-wide {
@ -1062,7 +1094,7 @@ main {
.scrolling {
.floatingheader {
box-shadow:
box-shadow:
rgba(0,0,0,0.02) 0 1px 2px,
rgba(255, 255, 255, 0.5) 0 -1px 0 inset;
@ -1135,15 +1167,12 @@ main {
color: $brown;
text-decoration: none;
}
.image-edit {
line-height: 12px;
padding: 10px;
display: block;
position: absolute;
top: 0;
left: 0;
opacity: 0;
text-decoration: none;
.button-add {
display: inline-block;
position:relative;
z-index: 700;
color: #fff;
padding-left:5px;
}
.image-webcam {
@ -1156,7 +1185,6 @@ main {
right: 0;
color: $brown;
text-decoration: none;
}
input {
@ -1192,16 +1220,26 @@ main {
background-size: contain;
}
.failed {
position: relative;
top: -40px;
font-size: 16px;
}
.bar {
height: 12px;
background: $blue;
&.fail {
background: $red;
}
}
}
.pre-image-uploader {
@include box-sizing(border-box);
@include baseline;
position: relative;
overflow:hidden;
overflow: hidden;
height: auto;
color: $brown;
@ -1210,49 +1248,35 @@ main {
left: 9999px;
opacity: 0;
}
.image-edit {
line-height: 12px;
padding: 10px;
display: block;
position: absolute;
top: 0;
left: 0;
opacity: 0;
a {
z-index: 10000;
color: $brown;
text-decoration: none;
&:hover {
color: $darkgrey;
}
}
img {
max-width: 100%;
}
.image-cancel {
@include icon($i-x, 16px);
position: absolute;
color: white;
text-shadow: rgba(0,0,0,0.5) 0 0 1px;
top: 10px;
right: 10px;
text-decoration: none;
&:hover {
cursor: pointer;
color: white;
}
}
}
//.progress {
// position: relative;
// top: -39px;
// margin: auto;
// margin-bottom: -12px;
// display: block;
// overflow: hidden;
// @include linear-gradient(to bottom, #f5f5f5, #f9f9f9);
// border-radius: 12px;
// box-shadow: (rgba(0,0,0,0.1) 0 1px 2px inset);
//}
//
//.fileupload-loading {
// display: block;
// top: 50%;
// width: 35px;
// height: 28px;
// margin: -28px auto 0;
// background-size: contain;
//}
//
//.bar {
// height: 12px;
// background: $blue;
//}
/* ==========================================================================
Misc
========================================================================== */
@ -1279,7 +1303,7 @@ main {
@include transition(all 0.15s ease-in-out);
&:hover {
box-shadow:
box-shadow:
rgba(0,0,0,0.05) 5px 0 0 inset,
rgba(0,0,0,0.05) -5px 0 0 inset,
rgba(0,0,0,0.05) 0 5px 0 inset,

View file

@ -18,14 +18,14 @@
/* Generated by icomoon.co */
@font-face {
font-family: 'Icons';
src:url('../fonts/icons.eot');
src:url('../fonts/icons.eot?#iefix') format('embedded-opentype'),
url('../fonts/icons.woff') format('woff'),
url('../fonts/icons.ttf') format('truetype'),
url('../fonts/icons.svg#icons') format('svg');
font-weight: normal;
font-style: normal;
font-family: 'Icons';
src:url('../fonts/icons.eot');
src:url('../fonts/icons.eot?#iefix') format('embedded-opentype'),
url('../fonts/icons.woff') format('woff'),
url('../fonts/icons.ttf') format('truetype'),
url('../fonts/icons.svg#icons') format('svg');
font-weight: normal;
font-style: normal;
}
@ -35,19 +35,19 @@
/*
* Epic dynamic icon element by Eric Eggert, this thing is so fucking cool it's
* actually unreal. http://cl.ly/O40t
* actually unreal. - bit.ly/TJwPPo
*/
%icon:before,
%icon:after {
font-family: "Icons";
font-weight: normal;
font-style: normal;
vertical-align: -7%;
text-transform:none;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
font-family: "Icons";
font-weight: normal;
font-style: normal;
vertical-align: -7%;
text-transform:none;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
@ -110,54 +110,54 @@
$i: \e018;
// Icons
$i-ghost: \e000;
$i-chevron-down: \e001;
$i-users: \e002;
$i-tag: \e003;
$i-tablet: \e004;
$i-menu: \e005;
$i-settings: \e006;
$i-search: \e007;
$i-search-left: \e008;
$i-rss: \e009;
$i-preview: \e00a;
$i-plugins: \e00b;
$i-pin: \e00c;
$i-pc: \e00d;
$i-pacman: \e00e;
$i-edit: \e00f;
$i-mobile: \e010;
$i-image: \e011;
$i-mail: \e012;
$i-list: \e013;
$i-info: \e014;
$i-home: \e015;
$i-grid: \e016;
$i-fullscreen: \e017;
$i-question: \e018;
$i-external: \e019;
$i-error: \e01a;
$i-comments: \e01b;
$i-close: \e01c;
$i-chevron: \e01d;
$i-calendar: \e01e;
$i-archive: \e01f;
$i-services: \e020;
$i-appearance: \e021;
$i-video: \e022;
$i-remove: \e023;
$i-reply: \e024;
$i-stats: \e025;
$i-featured: \e026;
$i-unfeatured: \e027;
$i-clock: \e028;
$i-settings2: \e029;
$i-camera: \e02a;
$i-power: \e02b;
$i-lock: \e02c;
$i-content: \e02d;
$i-user: \e02e;
$i-support: \e02f;
$i-ghost: \e000;
$i-chevron-down: \e001;
$i-users: \e002;
$i-tag: \e003;
$i-tablet: \e004;
$i-menu: \e005;
$i-settings: \e006;
$i-search: \e007;
$i-search-left: \e008;
$i-rss: \e009;
$i-preview: \e00a;
$i-plugins: \e00b;
$i-pin: \e00c;
$i-pc: \e00d;
$i-pacman: \e00e;
$i-edit: \e00f;
$i-mobile: \e010;
$i-image: \e011;
$i-mail: \e012;
$i-list: \e013;
$i-info: \e014;
$i-home: \e015;
$i-grid: \e016;
$i-fullscreen: \e017;
$i-question: \e018;
$i-external: \e019;
$i-error: \e01a;
$i-comments: \e01b;
$i-close: \e01c;
$i-chevron: \e01d;
$i-calendar: \e01e;
$i-archive: \e01f;
$i-services: \e020;
$i-appearance: \e021;
$i-video: \e022;
$i-remove: \e023;
$i-reply: \e024;
$i-stats: \e025;
$i-featured: \e026;
$i-unfeatured: \e027;
$i-clock: \e028;
$i-settings2: \e029;
$i-camera: \e02a;
$i-power: \e02b;
$i-lock: \e02c;
$i-content: \e02d;
$i-user: \e02e;
$i-support: \e02f;
$i-success: \e030;
$i-notification: \e031;
$i-add: \e032;
@ -171,6 +171,7 @@ $i-weather-sun: \e039;
$i-weather-partial: \e03a;
$i-weather-snow: \e03b;
$i-weather-cloudy: \e03c;
$i-lightning: \e03d;
/* =============================================================================
@ -181,32 +182,32 @@ To create a button with a label that is prefixed with a camera icon, we might
write our Sass something like this:
#button {
display: block;
width: 200px;
height: 40px;
@include icon($i-camera, 16px, #fff) {vertical-align:-10%;};
display: block;
width: 200px;
height: 40px;
@include icon($i-camera, 16px, #fff) {vertical-align:-10%;};
}
Thi would then output full CSS something like this:
#button {
display: block;
width: 200px;
height: 40px;
display: block;
width: 200px;
height: 40px;
}
#button:before {
content: "\e02a";
size: 16px;
color: #fff;
font-family: "Icons";
font-weight: normal;
font-style: normal;
vertical-align: -10%;
text-transform:none;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
content: "\e02a";
size: 16px;
color: #fff;
font-family: "Icons";
font-weight: normal;
font-style: normal;
vertical-align: -10%;
text-transform:none;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
*/

View file

@ -4,18 +4,18 @@
*
* Table of Contents:
*
* Compass Shit
* Compass Plugins
* Bourbon
* Breakpoint
* Typography
* Colors
* Gradients
* Typography
* Global Styles
*
*/
/* =============================================================================
Bourbon Imports
Bourbon
============================================================================= */
// Bourbon - http://bourbon.io/
@ -26,7 +26,7 @@ $default-transition-duration: 0.3s;
/* =============================================================================
Plugins
Breakpoint
============================================================================= */
// Breakpoint - http://breakpoint-sass.com/
@ -51,6 +51,26 @@ $letterbox: max-height 600px, max-height 600px;
$retina: 2 device-pixel-ratio;
/* =============================================================================
Typography
============================================================================= */
$font-family: 'Open Sans', sans-serif;
$font-family-serif: serif;
$font-family-mono: Inconsolata, monospace;
@mixin baseline {
margin: 1.6em 0;
}
//Does this really need to be a mixin?
@mixin hidden {
text-indent: -9999px;
visibility: hidden;
display: none;
}
/* =============================================================================
Colors
============================================================================= */
@ -85,19 +105,19 @@ $green: #9FBB58;
@mixin gradient($color1: #aaa, $color2: none) {
@if $color2 == 'none' {
background-color: lighten($color1, 10%);
background-image: -webkit-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: -moz-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: -ms-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: linear-gradient(bottom, $color1, lighten($color1, 10%));
} @else {
background-color: $color2;
background-image: -webkit-linear-gradient(bottom, $color1, $color2);
background-image: -moz-linear-gradient(bottom, $color1, $color2);
background-image: -ms-linear-gradient(bottom, $color1, $color2);
background-image: linear-gradient(to top, $color1, $color2);
}
@if $color2 == 'none' {
background-color: lighten($color1, 10%);
background-image: -webkit-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: -moz-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: -ms-linear-gradient(bottom, $color1, lighten($color1, 10%));
background-image: linear-gradient(bottom, $color1, lighten($color1, 10%));
} @else {
background-color: $color2;
background-image: -webkit-linear-gradient(bottom, $color1, $color2);
background-image: -moz-linear-gradient(bottom, $color1, $color2);
background-image: -ms-linear-gradient(bottom, $color1, $color2);
background-image: linear-gradient(to top, $color1, $color2);
}
}
@ -105,79 +125,63 @@ $green: #9FBB58;
@mixin inversegradient($color1: #aaa, $color2: none) {
@if $color2 == 'none' {
background-color: $color1;
background-image: -webkit-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: -moz-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: -ms-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: linear-gradient(bottom, lighten($color1, 10%), $color1);
} @else {
background-color: $color1;
background-image: -webkit-linear-gradient(bottom, $color2, $color1);
background-image: -moz-linear-gradient(bottom, $color2, $color1);
background-image: -ms-linear-gradient(bottom, $color2, $color1);
background-image: linear-gradient(to top, $color2, $color1);
}
@if $color2 == 'none' {
background-color: $color1;
background-image: -webkit-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: -moz-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: -ms-linear-gradient(bottom, lighten($color1, 10%), $color1);
background-image: linear-gradient(bottom, lighten($color1, 10%), $color1);
} @else {
background-color: $color1;
background-image: -webkit-linear-gradient(bottom, $color2, $color1);
background-image: -moz-linear-gradient(bottom, $color2, $color1);
background-image: -ms-linear-gradient(bottom, $color2, $color1);
background-image: linear-gradient(to top, $color2, $color1);
}
}
/* =============================================================================
Typography
============================================================================= */
@mixin baseline {
margin: 1.6em 0;
}
//Does this really need to be a mixin?
@mixin hidden {
text-indent: -9999px;
visibility: hidden;
display: none;
}
/* =============================================================================
Global Elements
============================================================================= */
%box, .box {
padding: 15px;
margin-bottom: 15px;
background: #fff;
position: relative;
box-shadow: $shadow;
header {
height:14px;
border-bottom: 1px solid $lightbrown;
padding-bottom: 15px;
padding: 15px;
margin-bottom: 15px;
text-transform: uppercase;
font-size:0.85em;
color: $brown;
}
background: #fff;
position: relative;
box-shadow: $shadow;
footer {
height:14px;
border-top: 1px solid $lightbrown;
padding-top: 10px;
margin-top:15px;
text-transform: uppercase;
font-size:0.85em;
color: $brown;
}
header a,
footer a {
color:$brown;
&:hover {
color:$darkgrey;
text-decoration: none;
header {
height:14px;
border-bottom: 1px solid $lightbrown;
padding-bottom: 15px;
margin-bottom: 15px;
text-transform: uppercase;
font-size:0.85em;
color: $brown;
}
footer {
height:14px;
border-top: 1px solid $lightbrown;
padding-top: 10px;
margin-top:15px;
text-transform: uppercase;
font-size:0.85em;
color: $brown;
}
header a,
footer a {
color:$brown;
&:hover {
color:$darkgrey;
text-decoration: none;
}
}
}
}
/* =============================================================================

View file

@ -1,678 +0,0 @@
/*!
+---------------------------------------------------------------------+
| _ _ _ |
| | |_ _ _ _ __ ___ _ __ | | __ _ | |_ ___ |
| | __|| | | || '_ \ / _ \| '_ \ | | / _` || __|/ _ \ |
| | |_ | |_| || |_) || __/| |_) || || (_| || |_| __/ |
| \__| \__, || .__/ \___|| .__/ |_| \__,_| \__|\___| |
| |___/ |_| |_| |
| |
| |
| URL: http://typeplate.com |
| VERSION: 1.0.0 |
| Github: https://github.com/typePlate/typeplate.github.com |
| AUTHORS: Zachary Kain (@zakkain) & Dennis Gaebel (@gryghostvisuals) |
| LICENSE: Creative Commmons |
| http://creativecommons.org/licenses/by/3.0 |
| |
+---------------------------------------------------------------------+
*/
// ==========================================================================
//
// $V a r i a b l e s
//
// ==========================================================================
// $B a s e T y p e
// --------------------------------------------------------------------------
$weight: normal;
$line-height: 1.65;
$font-size: 81.2; // percentage value (16 * 112.5% = 18px)
$font-base: 16 * ($font-size/100); // converts our percentage to a pixel value
$measure: $font-base * $line-height;
$font-family: 'Open Sans', sans-serif;
$font-family-serif: serif;
$font-family-mono: Inconsolata, monospace;
$font-properties: $weight, $line-height, $font-size, $font-family;
//the serif boolean var can be redeclared from another stylesheet. However
//the var must be placed after your @import "typeplate.scss";
$sans-serif-boolean: true !default;
// $C o l o r
// --------------------------------------------------------------------------
$body-copy-color: #444;
$heading-color: #222;
// $A M P E R S A N D @font-face
// --------------------------------------------------------------------------
$amp-fontface-name: Ampersand;
$amp-fontface-source: local('Georgia'), local('Garamond'), local('Palatino'), local('Book Antiqua');
$amp-fontface-fallback: local('Georgia');
// $A M P E R S A N D e l e m e n t
// --------------------------------------------------------------------------
// Allows for our ampersand element to have differing
// font-family from the ampersand unicode font-family.
$amp-font-family: Verdana, sans-serif;
// $T y p e S c a l e
// --------------------------------------------------------------------------
$tera: 117; // 117 = 18 × 6.5
$giga: 90; // 90 = 18 × 5
$mega: 72; // 72 = 18 × 4
$alpha: 60; // 60 = 18 × 3.3333
$beta: 48; // 48 = 18 × 2.6667
$gamma: 36; // 36 = 18 × 2
$delta: 24; // 24 = 18 × 1.3333
$epsilon: 21; // 21 = 18 × 1.1667
$zeta: 18; // 18 = 18 × 1
// $T y p e S c a l e U n i t
// --------------------------------------------------------------------------
$type-scale-unit-value: rem;
// $T e x t I n d e n t a t i o n
// --------------------------------------------------------------------------
$indent-val: 1.5em;
// $S t a t s T a b
// --------------------------------------------------------------------------
$stats-font-size: 1.5rem;
$stats-list-margin: 0 0.625rem 0 0;
$stats-list-padding: 0 0.625rem 0 0;
$stats-item-font-size: 0.875rem;
$stats-item-margin: 0.125rem 0 0 0;
$stats-border-style: 0.125rem solid #ccc;
// ==========================================================================
//
// $F o n t f a c e s
//
// ==========================================================================
// $U N I C O D E - R A N G E A m p e r s a n d
// --------------------------------------------------------------------------
@font-face {
font-family: '#{$amp-fontface-name}';
src: $amp-fontface-source;
unicode-range: U+0026;
}
// Ampersand fallback font for unicode range
@font-face {
font-family: '#{$amp-fontface-name}';
src: $amp-fontface-fallback;
unicode-range: U+270C;
}
// ==========================================================================
//
// $F u n c t i o n s
//
// ==========================================================================
// $C o n t e x t C a l c u l a t o r
// --------------------------------------------------------------------------
@function ems($target, $context) {
@return ($target/$context)#{em};
}
// $M o d u l a r S c a l e
// --------------------------------------------------------------------------
// http://thesassway.com/projects/modular-scale
@function modular-scale($scale, $base, $value) {
// divide a given font-size by base font-size & return a relative em value
@return ($scale/$base)#{$value};
}
@function measure-margin($scale, $measure, $value) {
// divide 1 unit of measure by given font-size & return a relative em value
@return ($measure/$scale)#{$value};
}
// ==========================================================================
//
// $M i x i n s
//
// ==========================================================================
// $M o d u l a r S c a l e
// --------------------------------------------------------------------------
// $Typographic scale
@mixin modular-scale($scale, $base, $value, $measure:"") {
font-size: $scale#{px};
font-size: modular-scale($scale, $base, $value);
@if $measure != "" {
margin-bottom: measure-margin($scale, $measure, $value);
}
}
// $B o d y C o p y
// --------------------------------------------------------------------------
@mixin base-type($weight, $line-height, $font-size, $font-family...) {
@if $sans-serif-boolean {
font: $weight #{$font-size}%/#{$line-height} $font-family;
}@else {
font: $weight #{$font-size}%/#{$line-height} $font-family-serif;
}
}
// $H y p h e n
// --------------------------------------------------------------------------
//http://trentwalton.com/2011/09/07/css-hyphenation
@mixin css-hyphens($val) {
// Accepted values: [ none | manual | auto ]
-webkit-hyphens: $val; // Safari 5.1 thru 6, iOS 4.2 thru 6
-moz-hyphens: $val; // Firefox 16 thru 20
-ms-hyphens: $val; // IE10
hyphens: $val; // W3C standard
};
// $S m a l l c a p s
// --------------------------------------------------------------------------
// http://blog.hypsometry.com/articles/true-small-capitals-with-font-face
// ISSUE#1 : https://github.com/zakkain/web-thang/issues/1
@mixin smallcaps($color, $font-weight) {
// depends on the font family.
// some font-families don't support small caps
// or don't provide them with their web font.
font-variant: small-caps;
font-weight: $font-weight;
text-transform: lowercase;
color: $color;
}
// $F o n t - S i z e - A d j u s t
// --------------------------------------------------------------------------
// correct x-height for fallback fonts: requires secret formula
// yet to be discovered. This is still wacky for support. Use
// wisely grasshopper.
@mixin font-size-adjust($adjust-value) {
// firefox 17+ only (as of Feb. 2013)
font-size-adjust: $adjust-value;
}
// $A m p e r s a n d
// --------------------------------------------------------------------------
@mixin ampersand($amp-font-family...) {
font-family: $amp-font-family;
}
%ampersand-placeholder {
@include ampersand($amp-fontface-name, $amp-font-family);
}
// Call your ampersand on any element you wish from another stylesheet
// using this Sass extend we've provided...
// @extend %ampersand-placeholder;
// $W o r d W r a p
// --------------------------------------------------------------------------
// Silent Sass Classes - A.K.A Placeholders
//
// normal: Indicates that lines may only break at normal word break points.
// break-word : Indicates that normally unbreakable words may be broken at
// arbitrary points if there are no otherwise acceptable break points in the line.
%breakword {
word-wrap: breakword;
}
%normal-wrap {
word-wrap: normal;
}
%inherit-wrap {
word-wrap: auto;
}
// $D r o p c a p s
// --------------------------------------------------------------------------
/**
* Dropcap Sass @include
* Use the following Sass @include with any selector you feel necessary.
*
@include dropcap($float: left, $font-size: 4em, $font-family: inherit, $text-indent: 0, $margin: inherit, $padding: inherit, $color: inherit, $lineHeight: 1, $bg: transparent);
*
* Extend this object into your custom stylesheet.
*
*/
// Include your '@include dropcap()' mixin and pass the following
// arguments below. Feel free to pass in arguments we've provided.
// At this time you cannot pass in font-family arguments but you're gonna
// change that anyway so why not just make that separately in your declaration.
@mixin dropcap($float: left, $font-size: 4em, $font-family: inherit, $text-indent: 0, $margin: inherit, $padding: inherit, $color: inherit, $lineHeight: 1, $bg: transparent) {
&:first-letter {
float: $float;
margin: $margin;
padding: $padding;
font-size: $font-size;
font-family: $font-family;
line-height: $lineHeight;
text-indent: $text-indent;
background: $bg;
color: $color;
}
}
// $D e f i n i t i o n L i s t
// --------------------------------------------------------------------------
// lining
// http://lea.verou.me/2012/02/flexible-multiline-definition-lists-with-2-lines-of-css
//
// dictionary-style
// http://lea.verou.me/2012/02/flexible-multiline-definition-lists-with-2-lines-of-css
@mixin definition-list-style($style) {
// lining style
@if $style == lining {
dt,
dd {
display: inline;
margin: 0;
}
dt,
dd {
& + dt {
&:before {
content: "\A";
white-space: pre;
}
}
}
dd {
& + dd {
&:before {
content: ", ";
}
}
&:before {
content: ": ";
margin-left: -0.2rem; //removes extra space between the dt and the colon
}
}
}
// dictionary-style
@if $style == dictionary-style {
dt {
display: inline;
counter-reset: definitions;
& + dt {
&:before {
content: ", ";
margin-left: -0.2rem; // removes extra space between the dt and the comma
}
}
}
dd {
display: block;
counter-increment: definitions;
&:before {
content: counter(definitions, decimal) ". ";
}
}
}
}
// ==========================================================================
//
// $T y p e l a t e S t y l i n g
//
// ==========================================================================
// $G l o b a l s
// --------------------------------------------------------------------------
html {
@include base-type($font-properties...);
}
body {
// Ala Trent Walton
@include css-hyphens(auto);
// normal: Indicates that lines may only break at normal word break points.
// break-word : Indicates that normally unbreakable words may be broken at ...
// arbitrary points if there are no otherwise acceptable break points in the line.
@extend %breakword;
color: $body-copy-color;
}
// $H e a d i n g s
// --------------------------------------------------------------------------
// styles for all headings, in the style of @csswizardry
%hN {
// voodoo to enable ligatures and kerning
text-rendering: optimizeLegibility;
// this fixes huge spaces when a heading wraps onto two lines
line-height: 1;
margin-top: 0;
}
// make a multi-dimensional array, where:
// the first value is the name of the class
// and the second value is the variable for the size
$sizes: tera $tera, giga $giga, mega $mega, alpha $alpha, beta $beta, gamma $gamma, delta $delta, epsilon $epsilon, zeta $zeta;
// for each size in the scale, create a class
@each $size in $sizes {
.#{nth($size, 1)} {
@include modular-scale(nth($size, 2), $font-base, '#{$type-scale-unit-value}', $measure);
}
}
// associate h1-h6 tags with their appropriate greek heading
h1 { @extend .alpha; @extend %hN; }
h2 { @extend .beta; @extend %hN; }
h3 { @extend .gamma; @extend %hN; }
h4 { @extend .delta; @extend %hN; }
h5 { @extend .epsilon; @extend %hN; }
h6 { @extend .zeta; @extend %hN; }
// $ P a r a g r a p h s
// --------------------------------------------------------------------------
p {
& + p {
//siblings indentation
text-indent: $indent-val;
}
}
// $C o d e b l o c k s
// --------------------------------------------------------------------------
@mixin white-space($wrap-space) {
@if $wrap-space == 'pre-wrap' {
white-space: #{-moz-}$wrap-space; // Firefox 1.0-2.0
white-space: $wrap-space; // current browsers
} @else {
white-space: $wrap-space;
}
}
pre code {
@extend %normal-wrap;
@include white-space(pre-wrap);
}
pre {
@include white-space(pre);
}
code {
@include white-space(pre);
font-family: monospace;
}
// $ S m a l l c a p s
// --------------------------------------------------------------------------
/**
* Abbreviations Markup
*
<abbr title="hyper text markup language">HMTL</abbr>
*
* Extend this object into your markup.
*
*/
abbr {
@include smallcaps(gray, 600);
&:hover {
cursor: help;
}
}
// $ H e a d i n g s C o l o r
// --------------------------------------------------------------------------
h1,
h2,
h3,
h4,
h5,
h6 {
color: $heading-color;
}
// $ D e f i n i t i o n L i s t s
// --------------------------------------------------------------------------
/**
* Lining Definition Style Markup
*
<dl class="lining">
<dt><b></b></dt>
<dd></dd>
</dl>
*
* Extend this object into your markup.
*
*/
.lining {
@include definition-list-style(lining);
}
/**
* Dictionary Definition Style Markup
*
<dl class="dictionary-style">
<dt><b></b></dt>
<dd></dd>
</dl>
*
* Extend this object into your markup.
*
*/
.dictionary-style {
@include definition-list-style(dictionary-style);
}
// $S t a t s T a b
// --------------------------------------------------------------------------
/**
* Stats Tab Markup
*
<ul class="stats-tabs">
<li><a href="#">[value]<b>[name]</b></a></li>
</ul>
*
* Extend this object into your markup.
*
*/
.stats-tabs {
padding: 0;
li {
display: inline-block;
margin: $stats-list-margin;
padding: $stats-list-padding;
border-right: $stats-border-style;
&:last-child {
margin: 0;
padding: 0;
border: none;
}
a {
display: inline-block;
font-size: $stats-font-size;
font-weight: bold;
b {
display: block;
margin: $stats-item-margin;
font-size: $stats-item-font-size;
font-weight: normal;
}
}
}
}
// $Blockquote Cites
// --------------------------------------------------------------------------
/**
* Blockquote Markup
*
<blockquote cite="">
<p>&Prime;&Prime;</p>
<cite>
<small><a href=""></a></small>
</cite>
</blockquote>
*
* Extend this object into your markup.
*
*/
@mixin cite-style($display:block, $text-align:right, $font-size: .875em) {
display: $display;
font-size: $font-size;
text-align: $text-align;
}
%cite {
@include cite-style;
}
// $Pull Quotes
// --------------------------------------------------------------------------
// http://24ways.org/2005/swooshy-curly-quotes-without-images
//
// http://todomvc.com - Thanks sindresorhus!
// https://github.com/typeplate/typeplate.github.com/issues/49
/**
* Pull Quotes Markup
*
<aside class="pull-quote">
<blockquote>
<p></p>
</blockquote>
</aside>
*
* Extend this object into your custom stylesheet.
*
*/
@mixin pull-quotes($font-size, $opacity) {
position: relative;
padding: ems($font-size, $font-size);
&:before,
&:after {
height: ems($font-size, $font-size);
opacity: $opacity;
position: absolute;
font-size: $font-size;
}
&:before {
content: '';
top: 0em;
left: 0em;
}
&:after {
content: '';
bottom: 0em;
right: 0em;
}
}
.pull-quote {
@include pull-quotes(4em, .15);
}
// $Figures
// --------------------------------------------------------------------------
/**
* Figures Markup
*
<figure>
<figcaption>
<strong>Fig. 4.2 | </strong>Type Anatomy, an excerpt from Mark Boulton's book<cite title="http://designingfortheweb.co.uk/book/part3/part3_chapter11.php">"Designing for the Web"</cite>
</figcaption>
</figure>
*
* Extend this object into your markup.
*
*/
// $Footnotes
// --------------------------------------------------------------------------
/**
* Footnote Markup : Replace 'X' with your unique number for each footnote
*
<article>
<p><sup><a href="#fn-itemX" id="fn-returnX"></a></sup></p>
<footer>
<ol class="foot-notes">
<li id="fn-itemX"><a href="#fn-returnX"></a></li>
</ol>
</footer>
</article>
*
* Extend this object into your markup.
*
*/

View file

@ -7,26 +7,23 @@
Modules - These styles are re-used in many areas, and are grouped by type.
========================================================================== */
@import "modules/mixins";
/* Sass variables like colours, font sizes, basic styles. */
@import "modules/mixins";
/* Sass variables like colours, font sizes, basic styles. */
@import "modules/normalize";
/* Browser cross compatibility normalisation*/
@import "modules/normalize";
/* Browser cross compatibility normalisation*/
@import "modules/typeplate";
/* All the styles controlling the typographic styles. */
@import "modules/icons";
/* All the styles controlling icons. */
@import "modules/icons";
/* All the styles controlling icons. */
@import "modules/animations";
/* Keyframe animations. */
@import "modules/animations";
/* Keyframe animations. */
@import "modules/global";
/* Global elements for the UI, like the header and footer. */
@import "modules/global";
/* Global elements for the UI, like the header and footer. */
@import "modules/forms";
/* All the styles controlling forms and form fields. */
@import "modules/forms";
/* All the styles controlling forms and form fields. */
@ -34,17 +31,29 @@
Layouts - Styles for specific admin screen layouts, grouped by screen.
========================================================================== */
@import "layouts/dashboard";
/* The default admin page, the dashboard. */
@import "layouts/dashboard";
/* The default admin page, the dashboard. */
@import "layouts/manage";
/* The manage posts screen. */
@import "layouts/manage";
/* The manage posts screen. */
@import "layouts/editor";
/* The write/edit post screen. */
@import "layouts/editor";
/* The write/edit post screen. */
@import "layouts/settings";
/* The settings screen. */
@import "layouts/login";
/* The login screen. */
@import "layouts/login";
/* The settings screen. */
@import "layouts/errors";
/* The error screens. */
/* ==========================================================================
Settings Layouts - Styles for the individual settings panes, grouped by pane.
========================================================================== */
@import "layouts/settings";
/* The settings screen. */
@import "layouts/users";
/* The users pane. */
@import "layouts/plugins";
/* The plugins pane. */

View file

@ -0,0 +1,59 @@
// Utility function that allows modes to be combined. The mode given
// as the base argument takes care of most of the normal mode
// functionality, but a second (typically simple) mode is used, which
// can override the style of text. Both modes get to parse all of the
// text, but when both assign a non-null style to a piece of code, the
// overlay wins, unless the combine argument was true, in which case
// the styles are combined.
// overlayParser is the old, deprecated name
CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, combine) {
return {
startState: function() {
return {
base: CodeMirror.startState(base),
overlay: CodeMirror.startState(overlay),
basePos: 0, baseCur: null,
overlayPos: 0, overlayCur: null
};
},
copyState: function(state) {
return {
base: CodeMirror.copyState(base, state.base),
overlay: CodeMirror.copyState(overlay, state.overlay),
basePos: state.basePos, baseCur: null,
overlayPos: state.overlayPos, overlayCur: null
};
},
token: function(stream, state) {
if (stream.start == state.basePos) {
state.baseCur = base.token(stream, state.base);
state.basePos = stream.pos;
}
if (stream.start == state.overlayPos) {
stream.pos = stream.start;
state.overlayCur = overlay.token(stream, state.overlay);
state.overlayPos = stream.pos;
}
stream.pos = Math.min(state.basePos, state.overlayPos);
if (stream.eol()) state.basePos = state.overlayPos = 0;
if (state.overlayCur == null) return state.baseCur;
if (state.baseCur != null && combine) return state.baseCur + " " + state.overlayCur;
else return state.overlayCur;
},
indent: base.indent && function(state, textAfter) {
return base.indent(state.base, textAfter);
},
electricChars: base.electricChars,
innerMode: function(state) { return {state: state.base, mode: base}; },
blankLine: function(state) {
if (base.blankLine) base.blankLine(state.base);
if (overlay.blankLine) overlay.blankLine(state.overlay);
}
};
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
CodeMirror.defineMode("gfm", function(config) {
var codeDepth = 0;
function blankLine(state) {
state.code = false;
return null;
}
var gfmOverlay = {
startState: function() {
return {
code: false,
codeBlock: false,
ateSpace: false
};
},
copyState: function(s) {
return {
code: s.code,
codeBlock: s.codeBlock,
ateSpace: s.ateSpace
};
},
token: function(stream, state) {
// Hack to prevent formatting override inside code blocks (block and inline)
if (state.codeBlock) {
if (stream.match(/^```/)) {
state.codeBlock = false;
return null;
}
stream.skipToEnd();
return null;
}
if (stream.sol()) {
state.code = false;
}
if (stream.sol() && stream.match(/^```/)) {
stream.skipToEnd();
state.codeBlock = true;
return null;
}
// If this block is changed, it may need to be updated in Markdown mode
if (stream.peek() === '`') {
stream.next();
var before = stream.pos;
stream.eatWhile('`');
var difference = 1 + stream.pos - before;
if (!state.code) {
codeDepth = difference;
state.code = true;
} else {
if (difference === codeDepth) { // Must be exact
state.code = false;
}
}
return null;
} else if (state.code) {
stream.next();
return null;
}
// Check if space. If so, links can be formatted later on
if (stream.eatSpace()) {
state.ateSpace = true;
return null;
}
if (stream.sol() || state.ateSpace) {
state.ateSpace = false;
if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) {
// User/Project@SHA
// User@SHA
// SHA
return "link";
} else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) {
// User/Project#Num
// User#Num
// #Num
return "link";
}
}
if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i)) {
// URLs
// Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
// And then (issue #1160) simplified to make it not crash the Chrome Regexp engine
return "link";
}
stream.next();
return null;
},
blankLine: blankLine
};
CodeMirror.defineMIME("gfmBase", {
name: "markdown",
underscoresBreakWords: false,
taskLists: true,
fencedCodeBlocks: true
});
return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay);
}, "markdown");

View file

@ -0,0 +1,74 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: GFM mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="../../addon/mode/overlay.js"></script>
<script src="../xml/xml.js"></script>
<script src="../markdown/markdown.js"></script>
<script src="gfm.js"></script>
<!-- Code block highlighting modes -->
<script src="../javascript/javascript.js"></script>
<script src="../css/css.js"></script>
<script src="../htmlmixed/htmlmixed.js"></script>
<script src="../clike/clike.js"></script>
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<link rel="stylesheet" href="../../doc/docs.css">
</head>
<body>
<h1>CodeMirror: GFM mode</h1>
<form><textarea id="code" name="code">
GitHub Flavored Markdown
========================
Everything from markdown plus GFM features:
## URL autolinking
Underscores_are_allowed_between_words.
## Fenced code blocks (and syntax highlighting)
```javascript
for (var i = 0; i &lt; items.length; i++) {
console.log(items[i], i); // log them
}
```
## Task Lists
- [ ] Incomplete task list item
- [x] **Completed** task list item
## A bit of GitHub spice
* SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* \#Num: #1
* User/#Num: mojombo#1
* User/Project#Num: mojombo/god#1
See http://github.github.com/github-flavored-markdown/.
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
mode: 'gfm',
lineNumbers: true,
theme: "default"
});
</script>
<p>Optionally depends on other modes for properly highlighted code blocks.</p>
<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#gfm_*">normal</a>, <a href="../../test/index.html#verbose,gfm_*">verbose</a>.</p>
</body>
</html>

View file

@ -0,0 +1,112 @@
(function() {
var mode = CodeMirror.getMode({tabSize: 4}, "gfm");
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
MT("emInWordAsterisk",
"foo[em *bar*]hello");
MT("emInWordUnderscore",
"foo_bar_hello");
MT("emStrongUnderscore",
"[strong __][em&strong _foo__][em _] bar");
MT("fencedCodeBlocks",
"[comment ```]",
"[comment foo]",
"",
"[comment ```]",
"bar");
MT("fencedCodeBlockModeSwitching",
"[comment ```javascript]",
"[variable foo]",
"",
"[comment ```]",
"bar");
MT("taskListAsterisk",
"[variable-2 * []] foo]", // Invalid; must have space or x between []
"[variable-2 * [ ]]bar]", // Invalid; must have space after ]
"[variable-2 * [x]]hello]", // Invalid; must have space after ]
"[variable-2 * ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links
" [variable-3 * ][property [x]]][variable-3 foo]"); // Valid; can be nested
MT("taskListPlus",
"[variable-2 + []] foo]", // Invalid; must have space or x between []
"[variable-2 + [ ]]bar]", // Invalid; must have space after ]
"[variable-2 + [x]]hello]", // Invalid; must have space after ]
"[variable-2 + ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links
" [variable-3 + ][property [x]]][variable-3 foo]"); // Valid; can be nested
MT("taskListDash",
"[variable-2 - []] foo]", // Invalid; must have space or x between []
"[variable-2 - [ ]]bar]", // Invalid; must have space after ]
"[variable-2 - [x]]hello]", // Invalid; must have space after ]
"[variable-2 - ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links
" [variable-3 - ][property [x]]][variable-3 foo]"); // Valid; can be nested
MT("taskListNumber",
"[variable-2 1. []] foo]", // Invalid; must have space or x between []
"[variable-2 2. [ ]]bar]", // Invalid; must have space after ]
"[variable-2 3. [x]]hello]", // Invalid; must have space after ]
"[variable-2 4. ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links
" [variable-3 1. ][property [x]]][variable-3 foo]"); // Valid; can be nested
MT("SHA",
"foo [link be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] bar");
MT("shortSHA",
"foo [link be6a8cc] bar");
MT("tooShortSHA",
"foo be6a8c bar");
MT("longSHA",
"foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar");
MT("badSHA",
"foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar");
MT("userSHA",
"foo [link bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] hello");
MT("userProjectSHA",
"foo [link bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] world");
MT("num",
"foo [link #1] bar");
MT("badNum",
"foo #1bar hello");
MT("userNum",
"foo [link bar#1] hello");
MT("userProjectNum",
"foo [link bar/hello#1] world");
MT("vanillaLink",
"foo [link http://www.example.com/] bar");
MT("vanillaLinkPunctuation",
"foo [link http://www.example.com/]. bar");
MT("vanillaLinkExtension",
"foo [link http://www.example.com/index.html] bar");
MT("notALink",
"[comment ```css]",
"[tag foo] {[property color][operator :][keyword black];}",
"[comment ```][link http://www.example.com/]");
MT("notALink",
"[comment ``foo `bar` http://www.example.com/``] hello");
MT("notALink",
"[comment `foo]",
"[link http://www.example.com/]",
"[comment `foo]",
"",
"[link http://www.example.com/]");
})();

View file

@ -8,7 +8,12 @@
<script src="../../addon/edit/continuelist.js"></script>
<script src="../xml/xml.js"></script>
<script src="markdown.js"></script>
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<style type="text/css">
.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}
.cm-s-default .cm-trailing-space-a:before,
.cm-s-default .cm-trailing-space-b:before {position: absolute; content: "\00B7"; color: #777;}
.cm-s-default .cm-trailing-space-new-line:before {position: absolute; content: "\21B5"; color: #777;}
</style>
<link rel="stylesheet" href="../../doc/docs.css">
</head>
<body>

View file

@ -1,7 +1,7 @@
CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
var htmlFound = CodeMirror.mimeModes.hasOwnProperty("text/html");
var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? "text/html" : "text/plain");
var htmlFound = CodeMirror.modes.hasOwnProperty("xml");
var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? {name: "xml", htmlMode: true} : "text/plain");
var aliases = {
html: "htmlmixed",
js: "javascript",
@ -103,6 +103,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
state.f = inlineNormal;
state.block = blockNormal;
}
// Reset state.trailingSpace
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
// Mark this line as blank
state.thisLineHasContent = false;
return null;
@ -217,6 +220,12 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
}
if (state.trailingSpaceNewLine) {
styles.push("trailing-space-new-line");
} else if (state.trailingSpace) {
styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b"));
}
return styles.length ? styles.join(' ') : null;
}
@ -308,11 +317,11 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return type;
}
if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, true)) {
if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) {
return switchInline(stream, state, inlineElement(linkinline, '>'));
}
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, true)) {
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
return switchInline(stream, state, inlineElement(linkemail, '>'));
}
@ -369,6 +378,14 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
}
if (ch === ' ') {
if (stream.match(/ +$/, false)) {
state.trailingSpace++;
} else if (state.trailingSpace) {
state.trailingSpaceNewLine = true;
}
}
return getType(state);
}
@ -453,7 +470,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
taskList: false,
list: false,
listDepth: 0,
quote: 0
quote: 0,
trailingSpace: 0,
trailingSpaceNewLine: false
};
},
@ -481,6 +500,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
list: s.list,
listDepth: s.listDepth,
quote: s.quote,
trailingSpace: s.trailingSpace,
trailingSpaceNewLine: s.trailingSpaceNewLine,
md_inside: s.md_inside
};
},
@ -504,6 +525,10 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
// Reset state.code
state.code = false;
// Reset state.trailingSpace
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
state.f = state.block;
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length;
var difference = Math.floor((indentation - state.indentation) / 4) * 4;

View file

@ -5,6 +5,20 @@
MT("plainText",
"foo");
// Don't style single trailing space
MT("trailingSpace1",
"foo ");
// Two or more trailing spaces should be styled with line break character
MT("trailingSpace2",
"foo[trailing-space-a ][trailing-space-new-line ]");
MT("trailingSpace3",
"foo[trailing-space-a ][trailing-space-b ][trailing-space-new-line ]");
MT("trailingSpace4",
"foo[trailing-space-a ][trailing-space-b ][trailing-space-a ][trailing-space-new-line ]");
// Code blocks using 4 spaces (regardless of CodeMirror.tabSize value)
MT("codeBlocksUsing4Spaces",
" [comment foo]");
@ -533,9 +547,15 @@
MT("linkWeb",
"[link <http://example.com/>] foo");
MT("linkWebDouble",
"[link <http://example.com/>] foo [link <http://example.com/>]");
MT("linkEmail",
"[link <user@example.com>] foo");
MT("linkEmailDouble",
"[link <user@example.com>] foo [link <user@example.com>]");
MT("emAsterisk",
"[em *foo*] bar");

File diff suppressed because one or more lines are too long

View file

@ -1,20 +1,31 @@
(function () {
var ghostdown = function (converter) {
var ghostdown = function () {
return [
// [image] syntax
// ![] image syntax
{
type: 'lang',
filter: function (source) {
return source.replace(/\n?!(?:image)?\[([^\n\]]*)\](?:\(([^\n\)]*)\))?/gi, function (match, alt, a) {
return '<section class="js-drop-zone image-uploader">' +
'<span class="media"><span class="hidden">Image Upload</span></span>' +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
'<img class="js-upload-target" style="display: none" alt="alt" src="" />' +
'<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' +
'<a class="image-url" title="Add image from URL"><span class="hidden">URL</span></a>' +
'<a class="image-webcam" title="Add image from webcam">' +
'<span class="hidden">Webcam</span></a>' +
'</section>';
filter: function (text) {
var defRegex = /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gim,
match,
defUrls = {};
while ((match = defRegex.exec(text)) !== null) {
defUrls[match[1]] = match;
}
return text.replace(/^!(?:\[([^\n\]]*)\])(?:\[([^\n\]]*)\]|\(([^\n\]]*)\))?$/gim, function (match, alt, id, src) {
var result = "";
/* regex from isURL in node-validator. Yum! */
if (src && src.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i)) {
result = '<img class="js-upload-target" src="' + src + '"/>';
} else if (id && defUrls.hasOwnProperty(id)) {
result = '<img class="js-upload-target" src="' + defUrls[id][2] + '"/>';
}
return '<section class="js-drop-zone image-uploader">' + result +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
'<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' +
'</section>';
});
}
}

View file

@ -2,7 +2,7 @@
*/
(function () {
"use strict";
Handlebars.registerHelper('dateFormat', function (context, block) {
Handlebars.registerHelper('date', function (context, block) {
var f = block.hash.format || "MMM Do, YYYY",
timeago = block.hash.timeago,
date;

View file

@ -20,6 +20,8 @@
router: null
};
_.extend(Ghost, Backbone.Events);
Ghost.init = function () {
Ghost.router = new Ghost.Router();

View file

@ -1,6 +1,6 @@
// # Surrounds given text with Markdown syntax
/*global $, window, CodeMirror, Showdown */
/*global $, window, CodeMirror, Showdown, moment */
(function () {
"use strict";
var Markdown = {
@ -15,8 +15,50 @@
self.replace();
},
replace: function () {
var text = this.elem.getSelection(), pass = true, md, cursor, word, converter;
var text = this.elem.getSelection(), pass = true, md, cursor, line, word, letterCount, converter;
switch (this.style) {
case "h1":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "# " + line);
this.elem.setCursor(cursor.line, cursor.ch + 2);
pass = false;
break;
case "h2":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "## " + line);
this.elem.setCursor(cursor.line, cursor.ch + 3);
pass = false;
break;
case "h3":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "### " + line);
this.elem.setCursor(cursor.line, cursor.ch + 4);
pass = false;
break;
case "h4":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "#### " + line);
this.elem.setCursor(cursor.line, cursor.ch + 5);
pass = false;
break;
case "h5":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "##### " + line);
this.elem.setCursor(cursor.line, cursor.ch + 6);
pass = false;
break;
case "h6":
cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line);
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");
@ -51,16 +93,24 @@
break;
case "copyHTML":
converter = new Showdown.converter();
md = converter.makeHtml(text);
if (text) {
md = converter.makeHtml(text);
} else {
md = converter.makeHtml(this.elem.getValue());
}
$(".modal-copyToHTML-content").text(md).selectText();
$(".js-modal").center();
pass = false;
break;
case "list":
md = text.replace(/^/gm, "* ");
this.elem.replaceSelection("\n" + md + "\n", "end");
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");
break;
default:
if (this.options.syntax[this.style]) {
md = this.options.syntax[this.style].replace('$1', text);
@ -68,6 +118,11 @@
}
if (pass && md) {
this.elem.replaceSelection(md, "end");
if (!text) {
letterCount = md.length;
cursor = this.elem.getCursor();
this.elem.setCursor({line: cursor.line, ch: cursor.ch - (letterCount / 2)});
}
}
}
};
@ -84,16 +139,9 @@
italic: "_$1_",
strike: "~~$1~~",
code: "`$1`",
h1: "\n# $1\n",
h2: "\n## $1\n",
h3: "\n### $1\n",
h4: "\n#### $1\n",
h5: "\n##### $1\n",
h6: "\n###### $1\n",
link: "[$1](http://)",
image: "!image[$1](http://)",
blockquote: "> $1",
currentDate: new Date().toLocaleString()
image: "![$1](http://)",
blockquote: "> $1"
}
};

View file

@ -0,0 +1,43 @@
// # Ghost Mobile Interactions
/*global window, document, $ */
(function () {
"use strict";
// ## Touch Gestures
// Initiate Hammer.js
// `touchAction: true` Potentially fix problems in IE10 [Ref](https://github.com/EightMedia/hammer.js/wiki/Tips-&-Tricks)
// `drag: false` Removes the Hammer drag event listeners, as they were clashing with jQueryUI Draggable
var Hammer = $(document).hammer({stop_browser_behavior: { touchAction: true }, drag: false});
// ### Show content preview when swiping left on content list
Hammer.on("tap", ".manage .content-list ol li", function (event) {
if (window.matchMedia('(max-width: 800px)').matches) {
event.gesture.preventDefault();
event.stopPropagation();
$(".content-list").animate({right: "100%", left: "-100%", 'margin-right': "15px"}, 300);
$(".content-preview").animate({right: "0", left: "0", 'margin-left': "0"}, 300);
}
});
// ### Show settings options page when swiping left on settings menu link
Hammer.on("tap", ".settings .settings-menu li", function (event) {
if (window.matchMedia('(max-width: 800px)').matches) {
event.gesture.preventDefault();
event.stopPropagation();
$(".settings-sidebar").animate({right: "100%", left: "-102%", 'margin-right': "15px"}, 300);
$(".settings-content").animate({right: "0", left: "0", 'margin-left': "0"}, 300);
}
});
// ### Toggle the sidebar menu
$('[data-off-canvas]').on('click', function (e) {
if (window.matchMedia('(max-width: 650px)').matches) {
e.preventDefault();
$('body').toggleClass('off-canvas');
}
});
}());

View file

@ -26,6 +26,20 @@
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);
}
});
@ -46,4 +60,4 @@
}
});
}());
}());

View file

@ -4,7 +4,7 @@
// Set the url manually and id to '0' to force PUT requests
Ghost.Models.Settings = Backbone.Model.extend({
url: '/api/v0.1/settings/',
url: Ghost.settings.apiRoot + '/settings',
id: "0",
defaults: {
title: 'My Blog',

View file

@ -0,0 +1,8 @@
/*global window, document, Ghost, $, _, Backbone */
(function () {
"use strict";
Ghost.Collections.Tags = Backbone.Collection.extend({
url: Ghost.settings.apiRoot + '/tags'
});
}());

View file

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

View file

@ -3,11 +3,11 @@
"use strict";
Ghost.Models.User = Backbone.Model.extend({
url: Ghost.settings.apiRoot + '/users/1'
url: Ghost.settings.apiRoot + '/users/me'
});
// Ghost.Collections.Users = Backbone.Collection.extend({
// url: Ghost.settings.apiRoot + '/users'
// });
}());
}());

View file

@ -14,7 +14,8 @@
'debug/' : 'debug',
'register/' : 'register',
'signup/' : 'signup',
'login/' : 'login'
'signin/' : 'login',
'forgotten/' : 'forgotten'
},
signup: function () {
@ -25,6 +26,10 @@
Ghost.currentView = new Ghost.Views.Login({ el: '.js-login-container' });
},
forgotten: function () {
Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-login-container' });
},
blog: function () {
var posts = new Ghost.Collections.Posts();
posts.fetch({ data: { status: 'all', orderBy: ['updated_at', 'DESC'] } }).then(function () {
@ -33,7 +38,19 @@
},
settings: function (pane) {
Ghost.currentView = new Ghost.Views.Settings({ el: '#main', pane: 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) {

View file

@ -1,188 +0,0 @@
// ## Tag Selector UI
/*global window, document, $ */
(function () {
"use strict";
var suggestions,
categoryOffset,
existingTags = [], // This will be replaced by an API return.
keys = {
UP: 38,
DOWN: 40,
ESC: 27,
ENTER: 13,
COMMA: 188,
BACKSPACE: 8
};
function findTerms(searchTerm, array) {
searchTerm = searchTerm.toUpperCase();
return $.map(array, function (item) {
var match = item.toUpperCase().indexOf(searchTerm) !== -1;
return match ? item : null;
});
}
function showSuggestions($target, searchTerm) {
suggestions.show();
var results = findTerms(searchTerm, existingTags),
pos = $target.position(),
styles = {
left: pos.left
},
maxSuggestions = 5, // Limit the suggestions number
results_length = results.length,
i,
suggest;
suggestions.css(styles);
suggestions.html("");
if (results_length < maxSuggestions) {
maxSuggestions = results_length;
}
for (i = 0; i < maxSuggestions; i += 1) {
suggestions.append("<li><a href='#'>" + results[i] + "</a></li>");
}
suggest = $('ul.suggestions li a:contains("' + searchTerm + '")');
suggest.each(function () {
var src_str = $(this).html(),
term = searchTerm,
pattern;
term = term.replace(/(\s+)/, "(<[^>]+>)*$1(<[^>]+>)*");
pattern = new RegExp("(" + term + ")", "i");
src_str = src_str.replace(pattern, "<mark>$1</mark>");
/*jslint regexp: true */ // - would like to remove this
src_str = src_str.replace(/(<mark>[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1</mark>$2<mark>$4");
$(this).html(src_str);
});
}
function handleTagKeyup(e) {
var $target = $(e.currentTarget),
searchTerm = $.trim($target.val()).toLowerCase(),
category,
populator;
if (e.keyCode === keys.UP) {
e.preventDefault();
if (suggestions.is(":visible")) {
if (suggestions.children(".selected").length === 0) {
suggestions.find("li:last-child").addClass('selected');
} else {
suggestions.children(".selected").removeClass('selected').prev().addClass('selected');
}
}
} else if (e.keyCode === keys.DOWN) {
e.preventDefault();
if (suggestions.is(":visible")) {
if (suggestions.children(".selected").length === 0) {
suggestions.find("li:first-child").addClass('selected');
} else {
suggestions.children(".selected").removeClass('selected').next().addClass('selected');
}
}
} else if (e.keyCode === keys.ESC) {
suggestions.hide();
} else if ((e.keyCode === keys.ENTER || e.keyCode === keys.COMMA) && searchTerm) {
// Submit tag using enter or comma key
e.preventDefault();
if (suggestions.is(":visible") && suggestions.children(".selected").length !== 0) {
if ($('.category:containsExact("' + suggestions.children(".selected").text() + '")').length === 0) {
category = $('<span class="category">' + suggestions.children(".selected").text() + '</span>');
if ($target.data('populate')) {
populator = $($target.data('populate'));
populator.append(category);
}
}
suggestions.hide();
} else {
if (e.keyCode === keys.COMMA) {
searchTerm = searchTerm.replace(",", "");
} // Remove comma from string if comma is uses to submit.
if ($('.category:containsExact("' + searchTerm + '")').length === 0) {
category = $('<span class="category">' + searchTerm + '</span>');
if ($target.data('populate')) {
populator = $($target.data('populate'));
populator.append(category);
}
}
}
$target.val('').focus();
searchTerm = ""; // Used to reset search term
suggestions.hide();
}
if (e.keyCode === keys.UP || e.keyCode === keys.DOWN) {
return false;
}
if (searchTerm) {
showSuggestions($target, searchTerm);
} else {
suggestions.hide();
}
}
function handleTagKeyDown(e) {
var $target = $(e.currentTarget),
populator,
lastBlock;
// Delete character tiggers on Keydown, so needed to check on that event rather than Keyup.
if (e.keyCode === keys.BACKSPACE && !$target.val()) {
populator = $($target.data('populate'));
lastBlock = populator.find('.category').last();
lastBlock.remove();
}
}
function handleSuggestionClick(e) {
var $target = $(e.currentTarget),
category = $('<span class="category">' + $(e.currentTarget).text() + '</span>'),
populator;
if ($target.parent().data('populate')) {
populator = $($target.parent().data('populate'));
populator.append(category);
suggestions.hide();
$('[data-input-behaviour="tag"]').val('').focus();
}
}
function handleCategoryClick(e) {
$(e.currentTarget).remove();
}
function handleClickOff(e) {
if (window.matchMedia('(max-width: 650px)').matches) {
e.preventDefault();
$('body').toggleClass('off-canvas');
}
}
$(document).ready(function () {
suggestions = $("ul.suggestions").hide(); // Initnialise suggestions overlay
if ($('.category-input').length) {
categoryOffset = $('.category-input').offset().left;
$('.category-blocks').css({'left': categoryOffset + 'px'});
}
$('[data-input-behaviour="tag"]')
.on('keyup', handleTagKeyup)
.on('keydown', handleTagKeyDown);
$('ul.suggestions').on('click', "li", handleSuggestionClick);
$('.categories').on('click', ".category", handleCategoryClick);
$('[data-off-canvas]').on('click', handleClickOff);
});
}());

View file

@ -0,0 +1,9 @@
<form id="forgotten" 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>
<section class="meta">
<a href="/ghost/login/">Log in</a>
</section>
</form>

View file

@ -3,7 +3,7 @@
<section class="entry-meta">
<time datetime="2013-01-04" class="date">
{{#if published}}
Published {{dateFormat published_at timeago="True"}}
Published {{date published_at timeago="True"}}
{{else}}
<span class="status-draft">Draft</span>
{{/if}}

View file

@ -1,12 +1,12 @@
<form id="login" method="post">
<form id="login" method="post" novalidate="novalidate">
<div class="email-wrap">
<input class="email" type="text" placeholder="Email Address" name="email">
<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="#">Forgotten password?</a> &bull; <a href="/ghost/signup/">Register new user</a>
<a class="forgotten-password" href="/ghost/forgotten/">Forgotten password?</a> &bull; <a href="/ghost/signup/">Register new user</a>
</section>
</form>

View file

@ -1,12 +1,12 @@
<aside class="modal-background"></aside>
<article class="modal{{#if options.type}}-{{options.type}}{{/if}} {{#if options.style}}modal-style-{{options.style}}{{/if}} {{options.animation}} js-modal">
<header class="modal-header"><h1>{{content.title}}</h1>{{#if options.close}}<a class="close" href="#"><span class="hidden">Close</span></a>{{/if}}</header>
{{#if content.title}}<header class="modal-header"><h1>{{content.title}}</h1>{{#if options.close}}<a class="close" href="#"><span class="hidden">Close</span></a>{{/if}}</header>{{/if}}
<section class="modal-content">
</section>
{{#if options.confirm}}
<footer class="modal-footer">
<button class="button-add js-button-accept">{{options.confirm.accept.text}}</button>
<button class="button-delete js-button-reject">{{options.confirm.reject.text}}</button>
<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}}
</article>

View file

@ -34,7 +34,7 @@
</tr>
<tr>
<td>Image</td>
<td>!image[image](http://)</td>
<td>![alt](http://)</td>
<td>Ctrl + Shift + I</td>
</tr>
<tr>
@ -50,37 +50,37 @@
<tr>
<td>H1</td>
<td># Heading</td>
<td>Alt + 1</td>
<td>Ctrl + Alt + 1</td>
</tr>
<tr>
<td>H2</td>
<td>## Heading</td>
<td>Alt + 2</td>
<td>Ctrl + Alt + 2</td>
</tr>
<tr>
<td>H3</td>
<td>### Heading</td>
<td>Alt + 3</td>
<td>Ctrl + Alt + 3</td>
</tr>
<tr>
<td>H4</td>
<td>#### Heading</td>
<td>Alt + 4</td>
<td>Ctrl + Alt + 4</td>
</tr>
<tr>
<td>H5</td>
<td>##### Heading</td>
<td>Alt + 5</td>
<td>Ctrl + Alt + 5</td>
</tr>
<tr>
<td>H6</td>
<td>###### Heading</td>
<td>Alt + 6</td>
<td>Ctrl + Alt + 6</td>
</tr>
<tr>
<td>Select Word</td>
<td></td>
<td>Ctrl + Option + W</td>
<td>Ctrl + Alt + W</td>
</tr>
<tr>
<td>Uppercase</td>
@ -95,7 +95,7 @@
<tr>
<td>Titlecase</td>
<td></td>
<td>Ctrl + Option + Shift + U</td>
<td>Ctrl + Alt + Shift + U</td>
</tr>
<tr>
<td>Insert Current Date</td>

View file

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

View file

@ -5,7 +5,7 @@
{{! TODO: JavaScript toggle featured/unfeatured}}
<span class="status">{{#if published}}Published{{else}}Written{{/if}}</span>
<span class="normal">by</span>
<span class="author">Joe Bloggs</span>
<span class="author">{{#if author.name}}{{author.name}}{{else}}{{author.email_address}}{{/if}}</span>
<section class="post-controls">
<a class="post-edit" href="#"><span class="hidden">Edit Post</span></a>
<a class="post-settings" href="#" data-toggle=".menu-drop-right"><span class="hidden">Post Settings</span></a>
@ -15,5 +15,5 @@
</section>
</header>
<section class="content-preview-content">
<div class="wrapper">{{{content}}}</div>
<div class="wrapper"><h1>{{{title}}}</h1>{{{content}}}</div>
</section>

View file

@ -5,38 +5,41 @@
</section>
</header>
<section class="content">
<form id="settings-general">
<form id="settings-general" novalidate="novalidate">
<fieldset>
<div class="form-group">
<label><strong>Blog Title</strong></label>
<input id="blog-title" name="general[title]" type="text" value="{{title}}" />
<input id="blog-title" name="general[title]" type="text" value="{{title}}">
<p>How your blog name appears on the site</p>
</div>
<div class="form-group">
<label><strong>Blog Logo</strong></label>
<section class="js-drop-zone">
<img id="logo" class="js-upload-target" src="{{logo}}"{{#unless logo}} style="display: none"{{/unless}} alt="logo"/>
<input data-url="upload" class="js-fileupload" type="file" name="uploadimage">
</section>
{{#if logo}}
<a class="js-modal-logo"><img src="{{logo}}" alt="logo"></a>
{{else}}
<a class="button-add js-modal-logo" href="#">Upload Image</a>
{{/if}}
<p>Display a logo on your site in place of blog title</p>
</div>
<div class="form-group">
<label><strong>Blog Icon</strong></label>
<section class="js-drop-zone">
<img id="icon" class="js-upload-target" src="{{icon}}"{{#unless icon}} style="display: none"{{/unless}} style="display: none" alt="icon"/>
<input data-url="upload" class="js-fileupload" type="file" name="uploadimage">
</section>
{{#if icon}}
<a class="js-modal-icon"><img src="{{icon}}" alt="icon"></a>
{{else}}
<a class="button-add js-modal-icon" href="#">Upload Image</a>
{{/if}}
<p>The icon for your blog, used in your browser tab and elsewhere</p>
</div>
<div class="form-group">
<label for="email-address"><strong>Email Address</strong></label>
<input id="email-address" name="general[email-address]" type="text" value="{{email}}" />
<input id="email-address" name="general[email-address]" type="email" value="{{email}}">
<p>Address to use for <a href="#">admin notifications</a></p>
<label class="checkbox">
<input type="checkbox" value="1" name="general[public-email]" /> Show my email address on my public profile
<input type="checkbox" value="1" name="general[public-email]"> Show my email address on my public profile
</label>
</div>
@ -50,6 +53,15 @@
</select>
</div>
<div class="form-group">
<label for="activeTheme"><strong>Theme</strong></label>
<select id="activeTheme" name="general[activeTheme]">
{{#each availableThemes}}
<option value="{{ name }}" {{#if active}}selected{{/if}}>{{ name }}</option>
{{/each}}
</select>
</div>
</fieldset>
<hr />

View file

@ -11,21 +11,21 @@
<button class="button-change-cover">Change Cover</button>
</figure>
</header>
<form class="user-details-container">
<form class="user-details-container" novalidate="novalidate">
<fieldset class="user-details-top">
<figure class="user-avatar-image">
<img id="user-profile-picture" src="{{#if profile_picture}}{{profile_picture}}{{else}}/shared/img/default-user-profile-picture.jpg{{/if}}" title="{{full_name}}"/>
<button class="button-change-avatar">Update Avatar</button>
<button class="button-change-avatar">Edit Picture</button>
</figure>
<label>
<input type="text" value="{{full_name}}" id="user-name" placeholder="Joe Bloggs">
<input type="url" value="{{full_name}}" id="user-name" placeholder="Joe Bloggs" autocapitalize="off" autocorrect="off">
<p>Use your real name so people can recognise you.</p>
</label>
</fieldset>
<fieldset class="user-details-bottom">
<div class="form-group">
<label><strong>Email</strong></label>
<input type="text" value="{{email_address}}" id="user-email">
<input type="email" value="{{email_address}}" id="user-email" placeholder="Email Address" autocapitalize="off" autocorrect="off">
<p>Email will not be publicly displayed. <a class="highlight" href="#" >Learn more</a>.</p>
</div>
@ -57,7 +57,6 @@
<div class="form-group">
<label><strong>Old Password</strong></label>
<input type="password" id="user-password-old">
<p><a href="#" >Forgot your password?</a></p>
</div>
<div class="form-group">
@ -69,7 +68,9 @@
<label><strong>Verify Password</strong></label>
<input type="password" id="user-new-password-verification">
</div>
<button class="button-change-password">Change Password</button>
<div class="form-group">
<button class="button-delete">Change Password</button>
</div>
</fieldset>

View file

@ -1,6 +1,6 @@
<form id="register" method="post">
<form id="register" method="post" novalidate="novalidate">
<div class="email-wrap">
<input class="email" type="text" placeholder="Email Address" name="email">
<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">

View file

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, JST, shortcut */
/*global window, document, setTimeout, Ghost, $, _, Backbone, JST, shortcut */
(function () {
"use strict";
@ -54,13 +54,14 @@
// by `addSubview`, which will in-turn remove any
// children of those views, and so on.
removeSubviews: function () {
var i, l, children = this.subviews;
var children = this.subviews;
if (!children) {
return this;
}
for (i = 0, l = children.length; i < l; i += 1) {
children[i].remove();
}
_(children).invoke("remove");
this.subviews = [];
return this;
},
@ -73,9 +74,52 @@
}
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;
// 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"
message = request.responseJSON.error || "Unknown Error";
} catch (e) {
message = "The server returned an error (" + (request.status || "?") + ").";
}
}
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 #flashbar.
@ -110,7 +154,11 @@
Ghost.Views.NotificationCollection = Ghost.View.extend({
el: '#flashbar',
initialize: function () {
var self = this;
this.render();
Ghost.on('urlchange', function () {
self.clearEverything();
});
},
events: {
'animationend .js-notification': 'removeItem',
@ -126,139 +174,213 @@
}, this);
},
renderItem: function (item) {
var itemView = new Ghost.Views.Notification({ model: item });
this.$el.html(itemView.render().el);
var itemView = new Ghost.Views.Notification({ model: item }),
height,
self = this;
this.$el.html(itemView.render().el).css({height: 0});
height = this.$('.js-notification').hide().outerHeight(true);
this.$el.animate({height: height}, 250, function () {
$(this).css({height: "auto"});
self.$('.js-notification').fadeIn(250);
});
},
addItem: function (item) {
this.model.push(item);
this.renderItem(item);
},
clearEverything: function () {
var height = this.$('.js-notification').outerHeight(true),
self = this;
this.$el.css({height: height});
this.$el.find('.js-notification.notification-passive').fadeOut(250, function () {
$(this).remove();
self.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
});
});
},
removeItem: function (e) {
e.preventDefault();
var self = e.currentTarget;
var self = e.currentTarget,
bbSelf = this;
if (self.className.indexOf('notification-persistent') !== -1) {
$.ajax({
type: "DELETE",
url: '/api/v0.1/notifications/' + $(self).find('.close').data('id')
}).done(function (result) {
$(e.currentTarget).remove();
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
$(self).remove();
});
});
} else {
$(e.currentTarget).remove();
this.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
$(self).remove();
});
}
},
closePassive: function (e) {
$(e.currentTarget).parent().fadeOut(200, function () { $(this).remove(); });
var height = this.$('.js-notification').outerHeight(true),
self = this;
this.$el.css({height: height});
$(e.currentTarget).parent().fadeOut(250, function () {
$(this).remove();
self.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
});
});
},
closePersistent: function (e) {
var self = e.currentTarget;
var self = e.currentTarget,
bbSelf = this;
$.ajax({
type: "DELETE",
url: '/api/v0.1/notifications/' + $(self).data('id')
}).done(function (result) {
if ($(self).parent().parent().hasClass('js-bb-notification')) {
$(self).parent().parent().fadeOut(200, function () { $(self).remove(); });
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 {
$(self).parent().fadeOut(200, function () { $(self).remove(); });
$parent.fadeOut(200, function () {
$(this).remove();
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
});
});
}
});
}
});
/**
* This is the view to generate the markup for the individual
* modal. Will be included into #modals.
*
*
*
* Types can be
* - (empty)
*
*/
// ## 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.confirm) {
shortcut.add("ESC", function () {
self.removeItem();
self.removeElement();
});
$(document).on('click', '.modal-background', function (e) {
self.removeItem(e);
self.removeElement(e);
});
} else {
// Initiate functions for buttons here so models don't get tied up.
this.acceptModal = function () {
this.model.options.confirm.accept.func();
self.removeItem();
this.model.options.confirm.accept.func.call(this);
self.removeElement();
};
this.rejectModal = function () {
this.model.options.confirm.reject.func();
self.removeItem();
this.model.options.confirm.reject.func.call(this);
self.removeElement();
};
shortcut.remove("ESC");
$(document).off('click', '.modal-background');
}
},
template: function (data) {
return JST[this.templateName](data);
templateData: function () {
return this.model;
},
events: {
'click .close': 'removeItem',
'click .close': 'removeElement',
'click .js-button-accept': 'acceptModal',
'click .js-button-reject': 'rejectModal'
},
render: function () {
this.$el.html(this.template(this.model));
afterRender: function () {
this.$(".modal-content").html(this.addSubview(new Ghost.Views.Modal.ContentView({model: this.model})).render().el);
this.$el.children(".js-modal").center();
this.$el.addClass("active");
this.$el.children(".js-modal").center({animate: false}).css("max-height", $(window).height() - 120); // same as resize(), but the debounce causes init lag
this.$el.addClass("active dark");
if (document.body.style.webkitFilter !== undefined) { // Detect webkit filters
$("body").addClass("blur");
} else {
this.$el.addClass("dark");
}
return this;
if (_.isFunction(this.model.options.afterRender)) {
this.model.options.afterRender.call(this);
}
if (this.model.options.animation) {
this.animate(this.$el.children(".js-modal"));
}
var self = this;
$(window).on('resize', self.resize);
},
// #### resize
// Center and resize modal based on window height
resize: _.debounce(function () {
$(".js-modal").center().css("max-height", $(window).height() - 120);
}, 50),
// #### remove
// Removes Backbone attachments from modals
remove: function () {
this.undelegateEvents();
this.$el.empty();
this.stopListening();
return this;
},
removeItem: function (e) {
// #### removeElement
// Visually removes the modal from the user interface
removeElement: function (e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
$('.js-modal').fadeOut(300, function () {
$(this).remove();
$("#modal-container").removeClass('active dark');
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");
}
});
this.remove();
self.$el.removeClass('dark');
setTimeout(function () {
self.remove();
self.$el.removeClass('active');
}, removeBackgroundDelay);
}, removeModalDelay);
},
// #### animate
// Animates the animation
animate: function (target) {
setTimeout(function () {
target.addClass('in');
}, target.transitionDuration());
}
});
/**
* Modal Content
* @type {*}
*/
// ## Modal Content
Ghost.Views.Modal.ContentView = Ghost.View.extend({
template: function (data) {
return JST['modals/' + this.model.content.template](data);
},
render: function () {
this.$el.html(this.template(this.model));
return this;
templateData: function () {
return this.model;
}
});

View file

@ -4,16 +4,7 @@
var ContentList,
ContentItem,
PreviewContainer,
// Add shadow during scrolling
scrollShadow = function (target, e) {
if ($(e.currentTarget).scrollTop() > 10) {
$(target).addClass('scrolling');
} else {
$(target).removeClass('scrolling');
}
};
PreviewContainer;
// Base view
// ----------
@ -34,7 +25,7 @@
},
initialize: function (options) {
this.$('.content-list-content').on('scroll', _.bind(scrollShadow, null, '.content-list'));
this.$('.content-list-content').scrollClass({target: '.content-list', offset: 10});
this.listenTo(this.collection, 'remove', this.showNext);
},
@ -72,9 +63,9 @@
},
removeItem: function () {
var view = this;
var self = this;
$.when(this.$el.slideUp()).then(function () {
view.remove();
self.remove();
});
},
@ -129,7 +120,6 @@
initialize: function (options) {
this.listenTo(Backbone, 'blog:activeItem', this.setActivePreview);
this.$('.content-preview-content').on('scroll', _.bind(scrollShadow, null, '.content-preview'));
},
setActivePreview: function (id) {
@ -177,7 +167,7 @@
},
type: "action",
style: "wide",
animation: 'fadeIn'
animation: 'fade'
},
content: {
template: 'blank',
@ -201,6 +191,7 @@
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');
});

View file

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, JST */
/*global window, document, localStorage, Ghost, $, _, Backbone, JST */
(function () {
"use strict";

View file

@ -0,0 +1,238 @@
// The Tag UI area associated with a post
/*global window, document, $, _, Backbone, Ghost */
(function () {
"use strict";
Ghost.View.EditorTagWidget = Ghost.View.extend({
events: {
'keyup [data-input-behaviour="tag"]': 'handleKeyup',
'keydown [data-input-behaviour="tag"]': 'handleKeydown',
'click ul.suggestions li': 'handleSuggestionClick',
'click .tags .tag': 'handleTagClick'
},
keys: {
UP: 38,
DOWN: 40,
ESC: 27,
ENTER: 13,
COMMA: 188,
BACKSPACE: 8
},
initialize: function () {
var self = this,
tagCollection = new Ghost.Collections.Tags();
tagCollection.fetch().then(function () {
self.allGhostTags = tagCollection.toJSON();
});
this.model.on('willSave', this.completeCurrentTag, this);
},
render: function () {
var tags = this.model.get('tags'),
$tags = $('.tags'),
tagOffset;
$tags.empty();
if (tags) {
_.forEach(tags, function (tag) {
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + tag.name + '</span>');
$tags.append($tag);
});
}
this.$suggestions = $("ul.suggestions").hide(); // Initialise suggestions overlay
if ($tags.length) {
tagOffset = $('.tag-input').offset().left;
$('.tag-blocks').css({'left': tagOffset + 'px'});
}
return this;
},
showSuggestions: function ($target, searchTerm) {
this.$suggestions.show();
var results = this.findMatchingTags(searchTerm),
styles = {
left: $target.position().left
},
maxSuggestions = 5, // Limit the suggestions number
results_length = results.length,
i,
suggest;
this.$suggestions.css(styles);
this.$suggestions.html("");
if (results_length < maxSuggestions) {
maxSuggestions = results_length;
}
for (i = 0; i < maxSuggestions; i += 1) {
this.$suggestions.append("<li data-tag-id='" + results[i].id + "' data-tag-name='" + results[i].name + "'><a href='#'>" + results[i].name + "</a></li>");
}
suggest = $('ul.suggestions li a:contains("' + searchTerm + '")');
suggest.each(function () {
var src_str = $(this).html(),
term = searchTerm,
pattern;
term = term.replace(/(\s+)/, "(<[^>]+>)*$1(<[^>]+>)*");
pattern = new RegExp("(" + term + ")", "i");
src_str = src_str.replace(pattern, "<mark>$1</mark>");
/*jslint regexp: true */ // - would like to remove this
src_str = src_str.replace(/(<mark>[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1</mark>$2<mark>$4");
$(this).html(src_str);
});
},
handleKeyup: function (e) {
var $target = $(e.currentTarget),
searchTerm = $.trim($target.val()).toLowerCase(),
tag,
$selectedSuggestion;
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 ((e.keyCode === this.keys.ENTER || e.keyCode === this.keys.COMMA) && searchTerm) {
// Submit tag using enter or comma key
e.preventDefault();
$selectedSuggestion = this.$suggestions.children(".selected");
if (this.$suggestions.is(":visible") && $selectedSuggestion.length !== 0) {
if ($('.tag:containsExact("' + $selectedSuggestion.data('tag-name') + '")').length === 0) {
tag = {id: $selectedSuggestion.data('tag-id'), name: $selectedSuggestion.data('tag-name')};
this.addTag(tag);
}
} else {
if (e.keyCode === this.keys.COMMA) {
searchTerm = searchTerm.replace(/,/g, "");
} // Remove comma from string if comma is used to submit.
if ($('.tag:containsExact("' + searchTerm + '")').length === 0) {
this.addTag({id: null, name: searchTerm});
}
}
$target.val('').focus();
searchTerm = ""; // Used to reset search term
this.$suggestions.hide();
}
if (e.keyCode === this.keys.UP || e.keyCode === this.keys.DOWN) {
return false;
}
if (searchTerm) {
this.showSuggestions($target, searchTerm);
} else {
this.$suggestions.hide();
}
},
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);
}
},
completeCurrentTag: function () {
var $target = this.$('.tag-input'),
tagName = $target.val(),
usedTagNames,
hasAlreadyBeenAdded;
usedTagNames = _.map(this.model.get('tags'), function (tag) {
return tag.name.toUpperCase();
});
hasAlreadyBeenAdded = usedTagNames.indexOf(tagName.toUpperCase()) !== -1;
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: $target.data('tag-name')});
},
handleTagClick: function (e) {
var $tag = $(e.currentTarget),
tag = {id: $tag.data('tag-id'), name: $tag.text()};
$tag.remove();
this.model.removeTag(tag);
},
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 = _.some(self.model.get('tags'), function (usedTag) {
return tag.name.toUpperCase() === usedTag.name.toUpperCase();
});
return tagNameMatches && !hasAlreadyBeenAdded;
});
return matchingTagModels;
},
addTag: function (tag) {
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + tag.name + '</span>');
this.$('.tags').append($tag);
this.model.addTag(tag);
this.$('.tag-input').val('').focus();
this.$suggestions.hide();
}
});
}());

View file

@ -5,7 +5,6 @@
"use strict";
var PublishBar,
TagWidget,
ActionsWidget,
MarkdownShortcuts = [
{'key': 'Ctrl+B', 'style': 'bold'},
@ -14,6 +13,7 @@
{'key': 'Meta+I', 'style': 'italic'},
{'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'},
@ -23,14 +23,14 @@
{'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+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'},
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
{'key': 'CMD+Alt+C', 'style': 'copyHTML'}
{'key': 'Meta+Alt+C', 'style': 'copyHTML'}
];
// The publish bar associated with a post, which has the TagWidget and
@ -39,7 +39,7 @@
PublishBar = Ghost.View.extend({
initialize: function () {
this.addSubview(new TagWidget({el: this.$('#entry-categories'), model: this.model})).render();
this.addSubview(new Ghost.View.EditorTagWidget({el: this.$('#entry-tags'), model: this.model})).render();
this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
},
@ -47,25 +47,21 @@
});
// The Tag UI area associated with a post
// ----------------------------------------
TagWidget = Ghost.View.extend({
render: function () { return this; }
});
// The Publish, Queue, Publish Now buttons
// ----------------------------------------
ActionsWidget = Ghost.View.extend({
events: {
'click [data-set-status]': 'handleStatus',
'click .js-post-button': 'updatePost'
'click .js-post-button': 'handlePostButton'
},
statusMap: {
'draft' : 'Save Draft',
'published': 'Update Post',
'scheduled' : 'Save Schedued Post'
'draft': 'Save Draft',
'published': 'Publish Now',
'scheduled': 'Save Schedued Post',
'queue': 'Add to Queue',
'publish-on': 'Publish on...'
},
initialize: function () {
@ -87,9 +83,11 @@
},
toggleStatus: function () {
var keys = Object.keys(this.statusMap),
var self = this,
keys = Object.keys(this.statusMap),
model = this.model,
currentIndex = keys.indexOf(model.get('status')),
prevStatus = this.model.get('status'),
currentIndex = keys.indexOf(prevStatus),
newIndex;
@ -107,69 +105,84 @@
message: 'Your post: ' + model.get('title') + ' has been ' + keys[newIndex],
status: 'passive'
});
}, function () {
Ghost.notifications.addItem({
type: 'error',
message: 'Your post: ' + model.get('title') + ' has not been ' + keys[newIndex],
status: 'passive'
});
}, function (xhr) {
var status = keys[newIndex];
// Show a notification about the error
self.reportSaveError(xhr, model, status);
// Set the button text back to previous
model.set({ status: prevStatus });
});
},
setActiveStatus: function setActiveStatus(status, displayText) {
// Set the publish button's action
$('.js-post-button')
.attr('data-status', status)
.text(displayText);
// Set the active action in the popup
$('.splitbutton-save .editor-options li')
.removeClass('active')
.filter(['li[data-set-status="', status, '"]'].join(''))
.addClass('active');
},
handleStatus: function (e) {
if (e) { e.preventDefault(); }
var status = $(e.currentTarget).attr('data-set-status');
this.setActiveStatus(status, this.statusMap[status]);
// Dismiss the popup menu
$('body').find('.overlay:visible').fadeOut();
},
handlePostButton: function (e) {
e.preventDefault();
var status = $(e.currentTarget).attr('data-set-status'),
model = this.model;
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;
if (status === 'publish-on') {
Ghost.notifications.addItem({
return Ghost.notifications.addItem({
type: 'alert',
message: 'Scheduled publishing not supported yet.',
status: 'passive'
});
}
if (status === 'queue') {
Ghost.notifications.addItem({
return Ghost.notifications.addItem({
type: 'alert',
message: 'Scheduled publishing not supported yet.',
status: 'passive'
});
}
this.model.trigger('willSave');
this.savePost({
status: status
}).then(function () {
Ghost.notifications.addItem({
type: 'success',
message: 'Your post: ' + model.get('title') + ' has been ' + status,
status: 'passive'
});
}, function () {
Ghost.notifications.addItem({
type: 'error',
message: 'Your post: ' + model.get('title') + ' has not been ' + status,
status: 'passive'
});
});
},
updatePost: function (e) {
if (e) {
e.preventDefault();
}
var model = this.model;
this.savePost().then(function () {
Ghost.notifications.addItem({
type: 'success',
message: 'Your post was saved as ' + model.get('status'),
status: 'passive'
});
}, function () {
Ghost.notifications.addItem({
type: 'error',
message: model.validationError,
message: ['Your post "', model.get('title'), '" has been ', status, '.'].join(''),
status: 'passive'
});
}, function (xhr) {
// Show a notification about the error
self.reportSaveError(xhr, model, status);
// Set the button text back to previous
model.set({ status: prevStatus });
});
},
@ -192,6 +205,25 @@
return $.Deferred().reject();
},
reportSaveError: function (response, model, status) {
var title = model.get('title') || '[Untitled]',
message = 'Your post: ' + title + ' has not been ' + 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.addItem({
type: 'error',
message: message,
status: 'passive'
});
},
render: function () {
this.$('.js-post-button').text(this.statusMap[this.model.get('status')]);
}
@ -207,6 +239,7 @@
// Add the container view for the Publish Bar
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
this.$('#entry-title').val(this.model.get('title'));
this.$('#entry-markdown').html(this.model.get('content_raw'));
this.initMarkdown();
@ -222,30 +255,11 @@
$('body').toggleClass('fullscreen');
});
$('.options.up').on('click', function (e) {
e.stopPropagation();
$(this).next("ul").fadeToggle(200);
});
this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
// Shadow on Markdown if scrolled
this.$('.CodeMirror-scroll').on('scroll', function (e) {
if ($('.CodeMirror-scroll').scrollTop() > 10) {
$('.entry-markdown').addClass('scrolling');
} else {
$('.entry-markdown').removeClass('scrolling');
}
});
this.$('.CodeMirror-scroll').scrollClass({target: '.entry-markdown', offset: 10});
this.$('.entry-preview-content').scrollClass({target: '.entry-preview', offset: 10});
// Shadow on Preview if scrolled
this.$('.entry-preview-content').on('scroll', function (e) {
if ($('.entry-preview-content').scrollTop() > 10) {
$('.entry-preview').addClass('scrolling');
} else {
$('.entry-preview').removeClass('scrolling');
}
});
// Zen writing mode shortcut
shortcut.add("Alt+Shift+Z", function () {
@ -260,7 +274,8 @@
},
events: {
'click .markdown-help': 'showHelp'
'click .markdown-help': 'showHelp',
'blur #entry-title': 'trimTitle'
},
syncScroll: _.debounce(function (e) {
@ -286,7 +301,7 @@
close: true,
type: "info",
style: "wide",
animation: 'fadeIn'
animation: 'fade'
},
content: {
template: 'markdown',
@ -296,49 +311,60 @@
}));
},
trimTitle: function () {
var $title = $('#entry-title'),
rawTitle = $title.val(),
trimmedTitle = $.trim(rawTitle);
if (rawTitle !== trimmedTitle) {
$title.val(trimmedTitle);
}
},
// This updates the editor preview panel.
// Currently gets called on every key press.
// Also trigger word count update
renderPreview: function () {
var view = this,
var self = this,
preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
view.$('.js-drop-zone').upload({editor: true});
this.$('.js-drop-zone').upload({editor: true});
Countable.once(preview, function (counter) {
view.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
view.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
view.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
});
},
// Markdown converter & markdown shortcut initialization.
initMarkdown: function () {
this.converter = new Showdown.converter({extensions: ['ghostdown']});
var self = this;
this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
mode: 'gfm',
tabMode: 'indent',
tabindex: "2",
lineWrapping: true
lineWrapping: true,
dragDrop: false
});
var view = this;
// Inject modal for HTML to be viewed in
shortcut.add("Ctrl+Alt+C", function () {
view.showHTML();
self.showHTML();
});
shortcut.add("Ctrl+Alt+C", function () {
view.showHTML();
self.showHTML();
});
_.each(MarkdownShortcuts, function (combo) {
shortcut.add(combo.key, function () {
return view.editor.addMarkdown({style: combo.style});
return self.editor.addMarkdown({style: combo.style});
});
});
this.editor.on('change', function () {
view.renderPreview();
self.renderPreview();
});
},
@ -349,7 +375,7 @@
close: true,
type: "info",
style: "wide",
animation: 'fadeIn'
animation: 'fade'
},
content: {
template: 'copyToHTML',

View file

@ -24,7 +24,13 @@
},
centerOnResize: _.debounce(function (e) {
$(".js-login-container").center();
var container = $(".js-login-container");
container.css({
'position': 'relative'
}).animate({
'top': Math.round($(window).height() / 2) - container.outerHeight() / 2 + 'px'
});
$(window).trigger("centered");
}, 100),
remove: function () {
@ -45,22 +51,24 @@
submitHandler: function (event) {
event.preventDefault();
var email = this.$el.find('.email').val(),
password = this.$el.find('.password').val();
password = this.$el.find('.password').val(),
redirect = Ghost.Views.Utils.getUrlVariables().r;
$.ajax({
url: '/ghost/login/',
url: '/ghost/signin/',
type: 'POST',
data: {
email: email,
password: password
password: password,
redirect: redirect
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (obj, string, status) {
error: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: 'Invalid username or password',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
@ -91,15 +99,48 @@
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (obj, string, status) {
var msgobj = $.parseJSON(obj.responseText);
error: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: msgobj.message,
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}
});
}());
Ghost.Views.Forgotten = Ghost.SimpleFormView.extend({
templateName: "forgotten",
events: {
'submit #forgotten': 'submitHandler'
},
submitHandler: function (event) {
event.preventDefault();
var email = this.$el.find('.email').val();
$.ajax({
url: '/ghost/forgotten/',
type: 'POST',
data: {
email: email
},
success: function (msg) {
window.location.href = msg.redirect;
},
error: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
});
}
});
}());

View file

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global window, document, Ghost, $, _, Backbone, Countable */
(function () {
"use strict";
@ -9,11 +9,26 @@
Ghost.Views.Settings = Ghost.View.extend({
initialize: function (options) {
$(".settings-content").removeClass('active');
this.addSubview(new Settings.Sidebar({
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);
}
});
@ -23,7 +38,7 @@
initialize: function (options) {
this.render();
this.menu = this.$('.settings-menu');
this.showContent(options.pane || 'general');
this.showContent(options.pane);
},
models: {},
@ -36,14 +51,17 @@
e.preventDefault();
var item = $(e.currentTarget),
id = item.find('a').attr('href').substring(1);
this.showContent(id);
},
showContent: function (id) {
var self = this,
model;
model,
themes;
Backbone.history.navigate('/settings/' + id);
Ghost.router.navigate('/settings/' + id);
Ghost.trigger('urlchange');
if (this.pane && id === this.pane.el.id) {
return;
}
@ -52,9 +70,13 @@
this.pane = new Settings[id]({ el: '.settings-content'});
if (!this.models.hasOwnProperty(this.pane.options.modelType)) {
themes = this.models.Themes = new Ghost.Models.Themes();
model = this.models[this.pane.options.modelType] = new Ghost.Models[this.pane.options.modelType]();
model.fetch().then(function () {
self.renderPane(model);
themes.fetch().then(function () {
model.fetch().then(function () {
model.set({availableThemes: themes.toJSON()});
self.renderPane(model);
});
});
} else {
model = this.models[this.pane.options.modelType];
@ -85,7 +107,11 @@
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');
@ -94,18 +120,25 @@
checkboxClass: 'icheckbox_ghost'
});
},
saveSuccess: function () {
saveSuccess: function (model, response, options) {
// TODO: better messaging here?
Ghost.notifications.addItem({
type: 'success',
message: 'Saved',
status: 'passive'
});
},
saveError: function () {
saveError: function (model, xhr) {
Ghost.notifications.addItem({
type: 'error',
message: 'Something went wrong, not saved :(',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
},
validationError: function (message) {
Ghost.notifications.addItem({
type: 'error',
message: message,
status: 'passive'
});
}
@ -118,22 +151,78 @@
id: "general",
events: {
'click .button-save': 'saveSettings'
'click .button-save': 'saveSettings',
'click .js-modal-logo': 'showLogo',
'click .js-modal-icon': 'showIcon'
},
saveSettings: function () {
var themes = this.model.get('availableThemes');
this.model.unset('availableThemes');
this.model.save({
title: this.$('#blog-title').val(),
email: this.$('#email-address').val(),
logo: this.$('#logo').attr("src"),
icon: this.$('#icon').attr("src")
icon: this.$('#icon').attr("src"),
activeTheme: this.$('#activeTheme').val()
}, {
success: this.saveSuccess,
error: this.saveError
});
this.model.set({availableThemes: themes});
},
showLogo: function () {
var settings = this.model.toJSON();
this.showUpload('#logo', 'logo', settings.logo);
},
showIcon: function () {
var settings = this.model.toJSON();
this.showUpload('#icon', 'icon', settings.icon);
},
showUpload: function (id, key, src) {
var self = this;
this.addSubview(new Ghost.Views.Modal({
model: {
options: {
close: false,
type: "action",
style: "wide",
animation: 'fadeIn',
afterRender: function () {
this.$('.js-drop-zone').upload();
},
confirm: {
accept: {
func: function () { // The function called on acceptance
var data = {};
data[key] = this.$('.js-upload-target').attr('src');
self.model.save(data, {
success: self.saveSuccess,
error: self.saveError
});
self.render();
return true;
},
buttonClass: "button-save right",
text: "Save" // The accept button text
},
reject: {
func: function () { // The function called on rejection
return true;
},
buttonClass: true,
text: "Cancel" // The reject button text
}
},
id: id,
src: src
},
content: {
template: 'uploadImage'
}
}
}));
},
templateName: 'settings/general',
beforeRender: function () {
@ -155,12 +244,15 @@
'click .button-save': 'saveSettings'
},
saveSettings: function () {
var themes = this.model.get('availableThemes');
this.model.unset('availableThemes');
this.model.save({
description: this.$('#blog-description').val()
}, {
success: this.saveSuccess,
error: this.saveError
});
this.model.set({availableThemes: themes});
},
templateName: 'settings/content',
@ -184,6 +276,7 @@
'click .button-change-password': 'changePassword'
},
saveUser: function () {
this.model.save({
'full_name': this.$('#user-name').val(),
@ -203,13 +296,17 @@
event.preventDefault();
var self = this,
email = this.$('#user-email').val(),
oldPassword = this.$('#user-password-old').val(),
newPassword = this.$('#user-password-new').val(),
ne2Password = this.$('#user-new-password-verification').val();
if (newPassword !== ne2Password || newPassword.length < 6 || oldPassword.length < 6) {
this.saveError();
if (newPassword !== ne2Password) {
this.validationError('Your new passwords do not match');
return;
}
if (newPassword.length < 8) {
this.validationError('Your password is not long enough. It must be at least 8 chars long.');
return;
}
@ -217,7 +314,6 @@
url: '/ghost/changepw/',
type: 'POST',
data: {
email: email,
password: oldPassword,
newpassword: newPassword,
ne2password: ne2Password
@ -229,14 +325,12 @@
status: 'passive',
id: 'success-98'
});
self.$('#user-password-old').val('');
self.$('#user-password-new').val('');
self.$('#user-new-password-verification').val('');
self.$('#user-password-old, #user-password-new, #user-new-password-verification').val('');
},
error: function (obj, string, status) {
error: function (xhr) {
Ghost.notifications.addItem({
type: 'error',
message: 'Invalid username or password',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
status: 'passive'
});
}
@ -254,6 +348,22 @@
this.$('#user-bio').val(user.bio);
this.$('#user-profile-picture').attr('src', user.profile_picture);
this.$('#user-cover-picture').attr('src', user.cover_picture);
},
afterRender: function () {
var self = this;
Countable.live(document.getElementById('user-bio'), function (counter) {
if (counter.all > 180) {
self.$('.bio-container .word-count').css({color: "#e25440"});
} else {
self.$('.bio-container .word-count').css({color: "#9E9D95"});
}
self.$('.bio-container .word-count').text(200 - counter.all);
});
Settings.Pane.prototype.afterRender.call(this);
}
});

View file

@ -12,9 +12,12 @@ var config = require('./../config'),
nodefn = require('when/node/function'),
_ = require('underscore'),
Polyglot = require('node-polyglot'),
Mailer = require('./server/mail'),
models = require('./server/models'),
plugins = require('./server/plugins'),
requireTree = require('./server/require-tree'),
permissions = require('./server/permissions'),
uuid = require('node-uuid'),
// Variables
appRoot = path.resolve(__dirname, '../'),
@ -40,7 +43,7 @@ defaults = {
// ## Article Statuses
/**
* A list of atricle status types
* A list of article status types
* @type {Object}
*/
statuses = {
@ -82,6 +85,9 @@ Ghost = function () {
// Holds the available plugins
instance.availablePlugins = {};
// Holds the dbhash (mainly used for cookie secret)
instance.dbHash = undefined;
app = express();
polyglot = new Polyglot();
@ -92,8 +98,19 @@ Ghost = function () {
// there's no management here to be sure this has loaded
settings: function () { return instance.settingsCache; },
dataProvider: models,
blogGlobals: function () {
/* this is a bit of a hack until we have a better way to combine settings and config
* this data is what becomes globally available to themes */
return {
url: instance.config().env[process.env.NODE_ENV].url,
title: instance.settings().title,
description: instance.settings().description,
logo: instance.settings().logo
};
},
statuses: function () { return statuses; },
polyglot: function () { return polyglot; },
mail: new Mailer(),
getPaths: function () {
return when.all([themeDirectories, pluginDirectories]).then(function (paths) {
instance.themeDirectories = paths[0];
@ -106,7 +123,7 @@ Ghost = function () {
'appRoot': appRoot,
'themePath': themePath,
'pluginPath': pluginPath,
'activeTheme': path.join(themePath, config.activeTheme),
'activeTheme': path.join(themePath, !instance.settingsCache ? "" : instance.settingsCache.activeTheme),
'adminViews': path.join(appRoot, '/core/server/views/'),
'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'),
'lang': path.join(appRoot, '/core/shared/lang/'),
@ -123,10 +140,34 @@ Ghost = function () {
Ghost.prototype.init = function () {
var self = this;
return when.join(instance.dataProvider.init(), instance.getPaths()).then(function () {
return when.join(
instance.dataProvider.init(),
instance.getPaths(),
instance.mail.init(self)
).then(function () {
return models.Settings.populateDefaults();
}).then(function () {
return self.initPlugins();
}, errors.logAndThrowError).then(function () {
}).then(function () {
// Initialize the settings cache
return self.updateSettingsCache();
}).then(function () {
// Initialize the permissions actions and objects
return permissions.init();
}).then(function () {
// get the settings and whatnot
return when(models.Settings.read('dbHash')).then(function (dbhash) {
// we already ran this, chill
self.dbHash = dbhash.attributes.value;
return dbhash.attributes.value;
}).otherwise(function (error) {
// this is where all the "first run" functionality should go
var dbhash = uuid.v4();
return when(models.Settings.add({key: 'dbHash', value: dbhash})).then(function (returned) {
self.dbHash = dbhash;
return dbhash;
});
});
}, errors.logAndThrowError);
};
@ -144,6 +185,17 @@ Ghost.prototype.updateSettingsCache = function (settings) {
var settings = {};
_.map(result.models, function (member) {
if (!settings.hasOwnProperty(member.attributes.key)) {
if (member.attributes.key === 'activeTheme') {
member.attributes.value = member.attributes.value.substring(member.attributes.value.lastIndexOf('/') + 1);
var settingsThemePath = path.join(themePath, member.attributes.value);
fs.exists(settingsThemePath, function (exists) {
if (!exists) {
member.attributes.value = "casper";
}
settings[member.attributes.key] = member.attributes.value;
});
return;
}
settings[member.attributes.key] = member.attributes.value;
}
});
@ -261,26 +313,38 @@ Ghost.prototype.initPlugins = function (pluginsToLoad) {
// Initialise Theme or admin
Ghost.prototype.initTheme = function (app) {
var self = this;
var self = this,
hbsOptions;
return function initTheme(req, res, next) {
app.set('view engine', 'hbs');
// return the correct mime type for woff files
express['static'].mime.define({'application/font-woff': ['woff']});
if (!res.isAdmin) {
app.engine('hbs', hbs.express3(
{partialsDir: path.join(self.paths().activeTheme, 'partials')}
));
// self.globals is a hack til we have a better way of getting combined settings & config
hbsOptions = {templateOptions: {data: {blog: self.blogGlobals()}}};
if (!self.themeDirectories.hasOwnProperty(self.settings().activeTheme)) {
// Throw an error if the theme is not available...
// TODO: move this to happen on app start
errors.logAndThrowError('The currently active theme ' + self.settings().activeTheme + ' is missing.');
} else if (self.themeDirectories[self.settings().activeTheme].hasOwnProperty('partials')) {
// Check that the theme has a partials directory before trying to use it
hbsOptions.partialsDir = path.join(self.paths().activeTheme, 'partials');
}
app.engine('hbs', hbs.express3(hbsOptions));
app.set('views', self.paths().activeTheme);
} else {
app.engine('hbs', hbs.express3({partialsDir: self.paths().adminViews + 'partials'}));
app.set('views', self.paths().adminViews);
app.use('/public', express['static'](path.join(__dirname, '/client/assets')));
app.use('/public', express['static'](path.join(__dirname, '/client')));
app.use('/shared', express['static'](path.join(__dirname, '/shared/')));
}
app.use(express['static'](self.paths().activeTheme));
app.use('/shared', express['static'](path.join(__dirname, '/shared')));
app.use('/content/images', express['static'](path.join(__dirname, '/../content/images')));
next();
};
@ -289,4 +353,4 @@ Ghost.prototype.initTheme = function (app) {
// TODO: Expose the defaults for other people to see/manipulate as a static value?
// Ghost.defaults = defaults;
module.exports = Ghost;
module.exports = Ghost;

View file

@ -5,13 +5,17 @@ var Ghost = require('../ghost'),
_ = require('underscore'),
when = require('when'),
errors = require('./errorHandling'),
permissions = require('./permissions'),
canThis = permissions.canThis,
ghost = new Ghost(),
dataProvider = ghost.dataProvider,
posts,
users,
tags,
notifications,
settings,
themes,
requestHandler,
cachedSettingsRequestHandler,
settingsObject,
@ -40,7 +44,15 @@ posts = {
// **takes:** a json object with all the properties which should be updated
edit: function edit(postData) {
// **returns:** a promise for the resulting post in a json object
return dataProvider.Post.edit(postData);
if (!this.user) {
return when.reject("You do not have permission to edit this post.");
}
return canThis(this.user).edit.post(postData.id).then(function () {
return dataProvider.Post.edit(postData);
}, function () {
return when.reject("You do not have permission to edit this post.");
});
},
// #### Add
@ -48,7 +60,15 @@ posts = {
// **takes:** a json object representing a post,
add: function add(postData) {
// **returns:** a promise for the resulting post in a json object
return dataProvider.Post.add(postData);
if (!this.user) {
return when.reject("You do not have permission to add posts.");
}
return canThis(this.user).create.post().then(function () {
return dataProvider.Post.add(postData);
}, function () {
return when.reject("You do not have permission to add posts.");
});
},
// #### Destroy
@ -56,7 +76,15 @@ posts = {
// **takes:** an identifier (id or slug?)
destroy: function destroy(args) {
// **returns:** a promise for a json response with the id of the deleted post
return dataProvider.Post.destroy(args.id);
if (!this.user) {
return when.reject("You do not have permission to remove posts.");
}
return canThis(this.user).remove.post(args.id).then(function () {
return dataProvider.Post.destroy(args.id);
}, function () {
return when.reject("You do not have permission to remove posts.");
});
}
};
@ -75,6 +103,10 @@ users = {
// **takes:** an identifier (id or slug?)
read: function read(args) {
// **returns:** a promise for a single user in a json object
if (args.id === 'me') {
args = {id: this.user};
}
return dataProvider.User.read(args);
},
@ -83,6 +115,7 @@ users = {
// **takes:** a json object representing a user
edit: function edit(userData) {
// **returns:** a promise for the resulting user in a json object
userData.id = this.user;
return dataProvider.User.edit(userData);
},
@ -110,6 +143,20 @@ users = {
changePassword: function changePassword(userData) {
// **returns:** on success, returns a promise for the resulting user in a json object
return dataProvider.User.changePassword(userData);
},
forgottenPassword: function forgottenPassword(email) {
return dataProvider.User.forgottenPassword(email);
}
};
tags = {
// #### All
// **takes:** Nothing yet
all: function browse() {
// **returns:** a promise for all tags which have previously been used in a json object
return dataProvider.Tag.findAll();
}
};
@ -216,6 +263,39 @@ settings = {
}
};
// ## Themes
themes = {
// #### Browse
// **takes:** options object
browse: function browse() {
// **returns:** a promise for a themes json object
return when(ghost.paths().availableThemes).then(function (themes) {
var themeKeys = Object.keys(themes),
res = [],
i,
activeTheme = ghost.paths().activeTheme.substring(ghost.paths().activeTheme.lastIndexOf('/') + 1),
item;
for (i = 0; i < themeKeys.length; i += 1) {
//do not include hidden files
if (themeKeys[i].indexOf('.') !== 0) {
item = {};
item.name = themeKeys[i];
item.details = themes[themeKeys[i]];
if (themeKeys[i] === activeTheme) {
item.active = true;
}
res.push(item);
}
}
return res;
});
}
};
// ## Request Handlers
// ### requestHandler
@ -223,11 +303,16 @@ settings = {
// takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response
requestHandler = function (apiMethod) {
return function (req, res) {
var options = _.extend(req.body, req.query, req.params);
return apiMethod(options).then(function (result) {
var options = _.extend(req.body, req.query, req.params),
apiContext = {
user: req.session && req.session.user
};
return apiMethod.call(apiContext, options).then(function (result) {
res.json(result || {});
}, function (error) {
res.json(400, {error: error});
error = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')};
res.json(400, error);
});
};
};
@ -270,7 +355,9 @@ cachedSettingsRequestHandler = function (apiMethod) {
// Public API
module.exports.posts = posts;
module.exports.users = users;
module.exports.tags = tags;
module.exports.notifications = notifications;
module.exports.settings = settings;
module.exports.themes = themes;
module.exports.requestHandler = requestHandler;
module.exports.cachedSettingsRequestHandler = cachedSettingsRequestHandler;
module.exports.cachedSettingsRequestHandler = cachedSettingsRequestHandler;

View file

@ -13,7 +13,8 @@ var Ghost = require('../../ghost'),
ghost = new Ghost(),
dataProvider = ghost.dataProvider,
adminNavbar,
adminControllers;
adminControllers,
loginSecurity = [];
// TODO: combine path/navClass to single "slug(?)" variable with no prefix
adminNavbar = {
@ -43,6 +44,7 @@ adminNavbar = {
}
};
// TODO: make this a util or helper
function setSelected(list, name) {
_.each(list, function (item, key) {
@ -82,34 +84,50 @@ adminControllers = {
if (ext === ".jpg" || ext === ".png" || ext === ".gif") {
renameFile();
} else {
res.send("Invalid filetype");
res.send(404, "Invalid filetype");
}
},
'login': function (req, res) {
res.render('login', {
res.render('signup', {
bodyClass: 'ghost-login',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'login')
});
},
'auth': function (req, res) {
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
req.session.user = "ghostadmin";
res.json(200, {redirect: req.query.r ? '/ghost/' + req.query.r : '/ghost/'});
}, function (error) {
res.send(401);
var currentTime = process.hrtime()[0],
denied = '';
loginSecurity = _.filter(loginSecurity, function (ipTime) {
return (ipTime.time + 2 > currentTime);
});
denied = _.find(loginSecurity, function (ipTime) {
return (ipTime.ip === req.connection.remoteAddress);
});
if (!denied) {
loginSecurity.push({ip: req.connection.remoteAddress, time: process.hrtime()[0]});
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
req.session.user = user.id;
res.json(200, {redirect: req.body.redirect ? '/ghost/'
+ decodeURIComponent(req.body.redirect) : '/ghost/'});
}, function (error) {
res.json(401, {error: error.message});
});
} else {
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
}
},
changepw: function (req, res) {
api.users.changePassword({
email: req.body.email,
currentUser: req.session.user,
oldpw: req.body.password,
newpw: req.body.newpassword,
ne2pw: req.body.ne2password
}).then(function (user) {
}).then(function () {
res.json(200, {msg: 'Password changed successfully'});
}, function (error) {
res.send(401);
res.send(401, {error: error.message});
});
},
@ -120,27 +138,74 @@ adminControllers = {
adminNav: setSelected(adminNavbar, 'login')
});
},
'doRegister': function (req, res) {
var email = req.body.email,
password = req.body.password;
if (email !== '' && password.length > 5) {
api.users.add({
email_address: email,
password: password
}).then(function (user) {
res.json(200, {redirect: '/ghost/login/'});
}, function (error) {
res.json(401, {message: error.message});
api.users.add({
email_address: email,
password: password
}).then(function (user) {
if (req.session.user === undefined) {
req.session.user = user.id;
}
res.json(200, {redirect: '/ghost/'});
}, function (error) {
res.json(401, {error: error.message});
});
},
'forgotten': function (req, res) {
res.render('signup', {
bodyClass: 'ghost-forgotten',
hideNavbar: true,
adminNav: setSelected(adminNavbar, 'login')
});
},
'resetPassword': function (req, res) {
var email = req.body.email;
api.users.forgottenPassword(email).then(function (user) {
var message = {
to: email,
subject: 'Your new password',
html: "<p><strong>Hello!</strong></p>" +
"<p>You've reset your password. Here's the new one: " + user.newPassword + "</p>"
};
return ghost.mail.send(message);
}).then(function success() {
var notification = {
type: 'success',
message: 'Your password was changed successfully. Check your email for details.',
status: 'passive',
id: 'successresetpw'
};
return api.notifications.add(notification).then(function () {
res.json(200, {redirect: '/ghost/signin/'});
});
} else {
res.json(400, {message: 'The password is too short. Have at least 6 characters in there'});
}
}, function failure(error) {
res.json(401, {error: error.message});
}).otherwise(errors.logAndThrowError);
},
'logout': function (req, res) {
delete req.session.user;
req.flash('success', "You were successfully logged out");
res.redirect('/ghost/login/');
var notification = {
type: 'success',
message: 'You were successfully signed out',
status: 'passive',
id: 'successlogout'
};
return api.notifications.add(notification).then(function () {
res.redirect('/ghost/signin/');
});
},
'index': function (req, res) {
res.render('dashboard', {
@ -150,15 +215,10 @@ adminControllers = {
},
'editor': function (req, res) {
if (req.params.id !== undefined) {
api.posts.read({id: parseInt(req.params.id, 10)})
.then(function (post) {
res.render('editor', {
bodyClass: 'editor',
adminNav: setSelected(adminNavbar, 'content'),
title: post.get('title'),
content: post.get('content')
});
});
res.render('editor', {
bodyClass: 'editor',
adminNav: setSelected(adminNavbar, 'content')
});
} else {
res.render('editor', {
bodyClass: 'editor',
@ -167,24 +227,16 @@ adminControllers = {
}
},
'content': function (req, res) {
api.posts.browse({status: req.params.status || 'all'})
.then(function (page) {
res.render('content', {
bodyClass: 'manage',
adminNav: setSelected(adminNavbar, 'content'),
posts: page.posts
});
});
res.render('content', {
bodyClass: 'manage',
adminNav: setSelected(adminNavbar, 'content')
});
},
'settings': function (req, res) {
api.settings.browse()
.then(function (settings) {
res.render('settings', {
bodyClass: 'settings',
adminNav: setSelected(adminNavbar, 'settings'),
settings: settings
});
});
res.render('settings', {
bodyClass: 'settings',
adminNav: setSelected(adminNavbar, 'settings')
});
},
'debug': { /* ugly temporary stuff for managing the app before it's properly finished */
index: function (req, res) {
@ -282,7 +334,7 @@ adminControllers = {
return api.notifications.add(notification).then(function () {
delete req.session.user;
res.redirect('/ghost/login/');
res.redirect('/ghost/signin/');
});
}, function importFailure(error) {
@ -330,4 +382,4 @@ adminControllers = {
}
};
module.exports = adminControllers;
module.exports = adminControllers;

View file

@ -6,14 +6,36 @@
var Ghost = require('../../ghost'),
api = require('../api'),
RSS = require('rss'),
ghost = new Ghost(),
frontendControllers;
frontendControllers = {
'homepage': function (req, res) {
var page = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1;
api.posts.browse({page: page}).then(function (page) {
// Parse the page number
var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1;
// No negative pages
if (pageParam < 1) {
return res.redirect("/page/1/");
}
api.posts.browse({page: pageParam}).then(function (page) {
var maxPage = page.pages;
// A bit of a hack for situations with no content.
if (maxPage === 0) {
maxPage = 1;
page.pages = 1;
}
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return res.redirect("/page/" + maxPage + "/");
}
// Render the page of posts
ghost.doFilter('prePostsRender', page.posts, function (posts) {
res.render('index', {posts: posts, pagination: {page: page.page, prev: page.prev, next: page.next, limit: page.limit, total: page.total, pages: page.pages}});
});
@ -25,6 +47,60 @@ frontendControllers = {
res.render('post', {post: post});
});
});
},
'rss': function (req, res) {
// Initialize RSS
var siteUrl = ghost.config().env[process.env.NODE_ENV].url,
feed = new RSS({
title: ghost.settings().title,
description: ghost.settings().description,
generator: 'Ghost v' + ghost.settings().currentVersion,
author: ghost.settings().author,
feed_url: siteUrl + '/rss/',
site_url: siteUrl,
ttl: '60'
}),
// Parse the page number
pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1;
// No negative pages
if (pageParam < 1) {
return res.redirect("/rss/1/");
}
api.posts.browse({page: pageParam}).then(function (page) {
var maxPage = page.pages;
// A bit of a hack for situations with no content.
if (maxPage === 0) {
maxPage = 1;
page.pages = 1;
}
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return res.redirect("/rss/" + maxPage + "/");
}
ghost.doFilter('prePostsRender', page.posts, function (posts) {
posts.forEach(function (post) {
var item = {
title: post.title,
guid: post.uuid,
url: siteUrl + '/' + post.slug + '/',
date: post.published_at
};
if (post.meta_description !== null) {
item.push({ description: post.meta_description });
}
feed.item(item);
});
res.set('Content-Type', 'text/xml');
res.send(feed.xml());
});
});
}
};

View file

@ -0,0 +1,47 @@
[
{
"key": "title",
"value": "Ghost",
"type": "blog"
},
{
"key": "description",
"value": "Just a blogging platform.",
"type": "blog"
},
{
"key": "email",
"value": "ghost@example.com",
"type": "general"
},
{
"key": "activePlugins",
"value": "",
"type": "general"
},
{
"key": "activeTheme",
"value": "content/themes/casper",
"type": "general"
},
{
"key": "currentVersion",
"value": "002",
"type": "core"
},
{
"key": "installedPlugins",
"value": "[]",
"type": "core"
},
{
"key": "logo",
"value": "",
"type": "blog"
},
{
"key": "icon",
"value": "",
"type": "blog"
}
]

View file

@ -25,65 +25,6 @@ module.exports = {
}
],
settings: [
{
"uuid": uuid.v4(),
"key": "url",
"value": "http://localhost:2368",
"created_by": 1,
"updated_by": 1,
"type": "blog"
},
{
"uuid": uuid.v4(),
"key": "title",
"value": "Ghost",
"created_by": 1,
"updated_by": 1,
"type": "blog"
},
{
"uuid": uuid.v4(),
"key": "description",
"value": "Just a blogging platform.",
"created_by": 1,
"updated_by": 1,
"type": "blog"
},
{
"uuid": uuid.v4(),
"key": "email",
"value": "ghost@example.com",
"created_by": 1,
"updated_by": 1,
"type": "general"
},
{
"uuid": uuid.v4(),
"key": "activePlugins",
"value": "",
"created_by": 1,
"updated_by": 1,
"type": "general"
},
{
"uuid": uuid.v4(),
"key": "activeTheme",
"value": "content/themes/casper",
"created_by": 1,
"updated_by": 1,
"type": "general"
},
{
"uuid": uuid.v4(),
"key": "currentVersion",
"value": "001",
"created_by": 1,
"updated_by": 1,
"type": "core"
}
],
roles: [
{
"id": 1,
@ -140,4 +81,4 @@ module.exports = {
"role_id": 1
}
]
};
};

View file

@ -3,37 +3,9 @@ var uuid = require('node-uuid');
module.exports = {
posts: [],
settings: [
{
"uuid": uuid.v4(),
"key": "installedPlugins",
"value": "[]",
"created_by": 1,
"updated_by": 1,
"type": "core"
},
{
"uuid": uuid.v4(),
"key": "logo",
"value": "",
"created_by": 1,
"updated_by": 1,
"type": "blog"
},
{
"uuid": uuid.v4(),
"key": "icon",
"value": "",
"created_by": 1,
"updated_by": 1,
"type": "blog"
}
],
roles: [],
permissions: [],
permissions_roles: []
};
};

View file

@ -101,7 +101,7 @@ up = function () {
// knex('roles_users').insert(fixtures.roles_users),
knex('permissions').insert(fixtures.permissions),
knex('permissions_roles').insert(fixtures.permissions_roles),
knex('settings').insert(fixtures.settings)
knex('settings').insert({ key: 'currentVersion', 'value': '001', type: 'core' })
]);
});

View file

@ -58,14 +58,6 @@ up = function () {
})
]).then(function () {
// Once we create all of the initial tables, bootstrap any of the data
return when.all([
knex('settings').insert(fixtures.settings)
]);
}).then(function () {
// Lastly, update the current version settings to reflect this version
return knex('settings')
.where('key', 'currentVersion')
@ -84,7 +76,8 @@ down = function () {
knex.Schema.dropTableIfExists("posts_custom_data")
]);
});
// Should we also drop the currentVersion?
};
exports.up = up;
exports.down = down;
exports.down = down;

View file

@ -7,6 +7,7 @@ var _ = require('underscore'),
initialVersion = '001',
// This currentVersion string should always be the current version of Ghost,
// we could probably load it from the config file.
// - Will be possible after default-settings.json restructure
currentVersion = '003';
function getCurrentVersion() {

View file

@ -4,8 +4,10 @@ var _ = require('underscore'),
when = require('when'),
hbs = require('express-hbs'),
errors = require('../errorHandling'),
models = require('../models'),
coreHelpers;
coreHelpers = function (ghost) {
var navHelper,
paginationHelper;
@ -17,10 +19,23 @@ coreHelpers = function (ghost) {
* @param {*} options
* @return {Object} A Moment time / date object
*/
ghost.registerThemeHelper('dateFormat', function (context, options) {
ghost.registerThemeHelper('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;
}
}
var f = options.hash.format || "MMM Do, YYYY",
timeago = options.hash.timeago,
date;
if (timeago) {
date = moment(context).fromNow();
} else {
@ -29,6 +44,62 @@ coreHelpers = function (ghost) {
return date;
});
// ### URL helper
//
// *Usage example:*
// `{{url}}`
// `{{url absolute}}`
//
// Returns the URL for the current object context
// i.e. If inside a post context will return post permalink
// absolute flag outputs absolute URL, else URL is relative
ghost.registerThemeHelper('url', function (context, options) {
var output = '';
if (options && options.absolute) {
output += ghost.config().env[process.NODE_ENV].url;
}
if (models.isPost(this)) {
output += "/" + this.slug;
}
return output;
});
// ### Author Helper
//
// *Usage example:*
// `{{author}}`
//
// Returns the full name of the author of a given post, or a blank string
// if the author could not be determined.
//
ghost.registerThemeHelper('author', function (context, options) {
return this.author ? this.author.full_name : "";
});
// ### Tags Helper
//
// *Usage example:*
// `{{tags}}`
// `{{tags separator=" - "}}`
//
// Returns a string of the tags on the post.
// By default, tags are separated by commas.
//
// Note that the standard {{#each tags}} implementation is unaffected by this helper
// and can be used for more complex templates.
ghost.registerThemeHelper('tags', function (options) {
var separator = ", ",
tagNames;
if (typeof options.hash.separator === "string") {
separator = options.hash.separator;
}
tagNames = _.pluck(this.tags, 'name');
return tagNames.join(separator);
});
// ### Content Helper
//
// *Usage example:*
@ -57,6 +128,88 @@ coreHelpers = function (ghost) {
});
// ### Excerpt Helper
//
// *Usage example:*
// `{{excerpt}}`
// `{{excerpt words=50}}`
// `{{excerpt characters=256}}`
//
// Attempts to remove all HTML from the string, and then shortens the result according to the provided option.
//
// Defaults to words=50
//
// **returns** SafeString truncated, HTML-free content.
//
ghost.registerThemeHelper('excerpt', function (options) {
var truncateOptions = (options || {}).hash || {},
excerpt;
truncateOptions = _.pick(truncateOptions, ["words", "characters"]);
/*jslint regexp:true */
excerpt = String(this.content).replace(/<\/?[^>]+>/gi, "");
/*jslint regexp:false */
if (!truncateOptions.words && !truncateOptions.characters) {
truncateOptions.words = 50;
}
return new hbs.handlebars.SafeString(
downsize(excerpt, truncateOptions)
);
});
ghost.registerThemeHelper('body_class', function (options) {
var classes = [];
if (_.isString(this.path) && this.path.match(/\/page/)) {
classes.push('archive-template');
} else if (!this.path || this.path === '/' || this.path === '') {
classes.push('home-template');
} else {
classes.push('post-template');
}
return ghost.doFilter('body_class', classes, function (classes) {
var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, '');
return new hbs.handlebars.SafeString(classString.trim());
});
});
ghost.registerThemeHelper('post_class', function (options) {
var classes = ['post'];
if (this.tags) {
classes = classes.concat(this.tags.map(function (tag) { return "tag-" + tag.name; }));
}
return ghost.doFilter('post_class', classes, function (classes) {
var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, '');
return new hbs.handlebars.SafeString(classString.trim());
});
});
ghost.registerThemeHelper('ghost_head', function (options) {
var head = [];
head.push('<meta name="generator" content="Ghost ' + this.version + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">');
return ghost.doFilter('ghost_head', head, function (head) {
var headString = _.reduce(head, function (memo, item) { return memo + "\n" + item; }, '');
return new hbs.handlebars.SafeString(headString.trim());
});
});
ghost.registerThemeHelper('ghost_foot', function (options) {
var foot = [];
foot.push('<script src="/shared/vendor/jquery/jquery.js"></script>');
return ghost.doFilter('ghost_foot', foot, function (foot) {
var footString = _.reduce(foot, function (memo, item) { return memo + ' ' + item; }, '');
return new hbs.handlebars.SafeString(footString.trim());
});
});
/**
* [ description]
*
@ -168,10 +321,10 @@ coreHelpers = function (ghost) {
});
// ### Pagination Helper
// `{{paginate}}`
// `{{pagination}}`
// Outputs previous and next buttons, along with info about the current page
paginationHelper = ghost.loadTemplate('pagination').then(function (templateFn) {
ghost.registerThemeHelper('paginate', function (options) {
ghost.registerThemeHelper('pagination', function (options) {
if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) {
errors.logAndThrowError('pagination data is not an object or is a function');
return;
@ -195,6 +348,12 @@ coreHelpers = function (ghost) {
});
});
ghost.registerThemeHelper('helperMissing', function (arg) {
if (arguments.length === 2) {
return undefined;
}
errors.logError("Missing helper: '" + arg + "'");
});
// Return once the template-driven helpers have loaded
return when.join(
navHelper,
@ -202,4 +361,4 @@ coreHelpers = function (ghost) {
);
};
module.exports.loadCoreHelpers = coreHelpers;
module.exports.loadCoreHelpers = coreHelpers;

View file

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

View file

@ -1,10 +1,9 @@
<nav id="pagination" role="pagination">
{{#if next}}
<div class="previous-page"><a href="/page/{{next}}/">Older Posts →</a></div>
{{/if}}
<div class="page-number">Page {{page}}<span class="extended"> of {{pages}}</span></div>
<nav class="pagination" role="pagination">
{{#if prev}}
<div class="next-page"><a href="/page/{{prev}}/">← Newer Posts</a></div>
<a class="newer-posts" href="/page/{{prev}}/">&larr; Newer Posts</a>
{{/if}}
<span class="page-number">Page {{page}} of {{pages}}</span>
{{#if next}}
<a class="older-posts" href="/page/{{next}}/">Older Posts &rarr;</a>
{{/if}}
</nav>

116
core/server/mail.js Normal file
View file

@ -0,0 +1,116 @@
var cp = require('child_process'),
url = require('url'),
_ = require('underscore'),
when = require('when'),
nodefn = require('when/node/function'),
nodemailer = require('nodemailer');
function GhostMailer(opts) {
opts = opts || {};
this.transport = opts.transport || null;
}
// ## E-mail transport setup
// *This promise should always resolve to avoid halting Ghost::init*.
GhostMailer.prototype.init = function (ghost) {
this.ghost = ghost;
// TODO: fix circular reference ghost -> mail -> api -> ghost, remove this late require
this.api = require('./api');
var self = this,
config = ghost.config().env[process.env.NODE_ENV];
if (config.mail && config.mail.transport && config.mail.options) {
this.createTransport(config);
return when.resolve();
}
// Attempt to detect and fallback to `sendmail`
return this.detectSendmail().then(function (binpath) {
self.transport = nodemailer.createTransport('sendmail', {
path: binpath
});
self.usingSendmail();
}, function () {
self.emailDisabled();
}).ensure(function () {
return when.resolve();
});
};
GhostMailer.prototype.isWindows = function () {
return process.platform === 'win32';
};
GhostMailer.prototype.detectSendmail = function () {
if (this.isWindows()) {
return when.reject();
}
return when.promise(function (resolve, reject) {
cp.exec('which sendmail', function (err, stdout) {
if (err && !/bin\/sendmail/.test(stdout)) {
return reject();
}
resolve(stdout.toString());
});
});
};
GhostMailer.prototype.createTransport = function (config) {
this.transport = nodemailer.createTransport(config.mail.transport, _.clone(config.mail.options));
};
GhostMailer.prototype.usingSendmail = function () {
this.api.notifications.add({
type: 'info',
message: [
"Ghost is attempting to use your server's <b>sendmail</b> to send e-mail.",
"It is recommended that you explicitly configure an e-mail service,",
"see <a href=\"https://github.com/TryGhost/Ghost/wiki/\">instructions in the wiki</a>."
].join(' '),
status: 'persistent',
id: 'ghost-mail-fallback'
});
};
GhostMailer.prototype.emailDisabled = function () {
this.api.notifications.add({
type: 'warn',
message: [
"Ghost is currently unable to send e-mail.",
"See <a href=\"https://github.com/TryGhost/Ghost/wiki/\">instructions for configuring",
"an e-mail service</a>."
].join(' '),
status: 'persistent',
id: 'ghost-mail-disabled'
});
this.transport = null;
};
// Sends an e-mail message enforcing `to` (blog owner) and `from` fields
GhostMailer.prototype.send = function (message) {
if (!this.transport) {
return when.reject(new Error('Email Error: No e-mail transport configured.'));
}
if (!(message && message.subject && message.html)) {
return when.reject(new Error('Email Error: Incomplete message data.'));
}
var from = 'ghost-mailer@' + url.parse(this.ghost.config().env[process.env.NODE_ENV].url).hostname,
to = message.to || this.ghost.settings().email,
sendMail = nodefn.lift(this.transport.sendMail.bind(this.transport));
message = _.extend(message, {
from: from,
to: to,
generateTextFromHTML: true
});
return sendMail(message).otherwise(function (error) {
// Proxy the error message so we can add 'Email Error:' to the beginning to make it clearer.
error = _.isString(error) ? 'Email Error:' + error : (_.isObject(error) ? 'Email Error: ' + error.message : 'Email Error: Unknown Email Error');
return when.reject(new Error(error));
});
};
module.exports = GhostMailer;

View file

@ -2,12 +2,15 @@ var GhostBookshelf,
Bookshelf = require('bookshelf'),
moment = require('moment'),
_ = require('underscore'),
config = require('../../../config');
config = require('../../../config'),
Validator = require('validator').Validator;
// Initializes Bookshelf as its own instance, so we can modify the Models and not mess up
// others' if they're using the library outside of ghost.
GhostBookshelf = Bookshelf.Initialize('ghost', config.env[process.env.NODE_ENV || 'development'].database);
GhostBookshelf.validator = new Validator();
// The Base Model which other Ghost objects will inherit from,
// including some convenience functions as static properties on the model.
GhostBookshelf.Model = GhostBookshelf.Model.extend({
@ -36,8 +39,10 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
return attrs;
}
_.each(relations, function (key) {
attrs[key] = relations[key].toJSON();
_.each(relations, function (relation, key) {
if (key.substring(0, 7) !== "_pivot_") {
attrs[key] = relation.toJSON ? relation.toJSON() : relation;
}
});
return attrs;
@ -80,7 +85,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
edit: function (editedObj, options) {
options = options || {};
return this.forge({id: editedObj.id}).fetch(options).then(function (foundObj) {
return foundObj.set(editedObj).save();
return foundObj.save(editedObj);
});
},
@ -90,7 +95,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
/**
* Naive create
* @param editedObj
* @param newObj
* @param options (optional)
*/
add: function (newObj, options) {
@ -118,4 +123,4 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
});
module.exports = GhostBookshelf;
module.exports = GhostBookshelf;

View file

@ -6,6 +6,7 @@ module.exports = {
Role: require('./role').Role,
Permission: require('./permission').Permission,
Settings: require('./settings').Settings,
Tag: require('./tag').Tag,
init: function () {
return migrations.init();
},
@ -13,5 +14,9 @@ module.exports = {
return migrations.reset().then(function () {
return migrations.init();
});
},
isPost: function (jsonData) {
return jsonData.hasOwnProperty("content") && jsonData.hasOwnProperty("content_raw")
&& jsonData.hasOwnProperty("title") && jsonData.hasOwnProperty("slug");
}
};

View file

@ -7,6 +7,25 @@ var GhostBookshelf = require('./base'),
Permission = GhostBookshelf.Model.extend({
tableName: 'permissions',
permittedAttributes: ['id', 'name', 'object_type', 'action_type', 'object_id'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
// TODO: validate object_type, action_type and object_id
GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
roles: function () {
return this.belongsToMany(Role);
},

View file

@ -5,15 +5,23 @@ var Post,
when = require('when'),
errors = require('../errorHandling'),
Showdown = require('showdown'),
converter = new Showdown.converter(),
github = require('../../shared/vendor/showdown/extensions/github'),
converter = new Showdown.converter({extensions: [github]}),
User = require('./user').User,
config = require('../../../config'),
Tag = require('./tag').Tag,
GhostBookshelf = require('./base');
Post = GhostBookshelf.Model.extend({
tableName: 'posts',
permittedAttributes: [
'id', 'uuid', 'title', 'slug', 'content_raw', 'content', 'meta_title', 'meta_description', 'meta_keywords',
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by',
'published_at', 'published_by'
],
hasTimestamps: true,
defaults: function () {
@ -26,15 +34,27 @@ Post = GhostBookshelf.Model.extend({
initialize: function () {
this.on('creating', this.creating, this);
this.on('saving', this.updateTags, this);
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
GhostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();
return true;
},
saving: function () {
if (!this.get('title')) {
throw new Error('Post title cannot be blank');
}
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
this.set('content', converter.makeHtml(this.get('content_raw')));
this.set('title', this.get('title').trim());
if (this.hasChanged('status') && this.get('status') === 'published') {
this.set('published_at', new Date());
// This will need to go elsewhere in the API layer.
@ -42,10 +62,13 @@ Post = GhostBookshelf.Model.extend({
}
this.set('updated_by', 1);
// refactoring of ghost required in order to make these details available here
},
creating: function () {
// set any dynamic default properties
var self = this;
if (!this.get('created_by')) {
this.set('created_by', 1);
@ -72,20 +95,31 @@ Post = GhostBookshelf.Model.extend({
// Look for a post with a matching slug, append an incrementing number if so
checkIfSlugExists = function (slugToFind) {
return Post.read({slug: slugToFind}).then(function (found) {
var trimSpace;
if (!found) {
return when.resolve(slugToFind);
}
slugTryCount += 1;
// TODO: Bug out (when.reject) if over 10 tries or something?
// If this is the first time through, add the hyphen
if (slugTryCount === 2) {
slugToFind += '-';
} else {
// Otherwise, trim the number off the end
trimSpace = -(String(slugTryCount - 1).length);
slugToFind = slugToFind.slice(0, trimSpace);
}
return checkIfSlugExists(slugToFind + '-' + slugTryCount);
slugToFind += slugTryCount;
return checkIfSlugExists(slugToFind);
});
};
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
slug = title.replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
slug = title.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
// Replace dots and spaces with a dash
.replace(/(\s|\.)/g, '-')
// Convert 2 or more dashes into a single dash
@ -99,20 +133,105 @@ Post = GhostBookshelf.Model.extend({
slug = /^(ghost|ghost\-admin|admin|wp\-admin|dashboard|login|archive|archives|category|categories|tag|tags|page|pages|post|posts)$/g
.test(slug) ? slug + '-post' : slug;
//if slug is empty after trimming use "post"
if (!slug) {
slug = "post";
}
// Test for duplicate slugs.
return checkIfSlugExists(slug);
},
updateTags: function () {
var self = this,
tagOperations = [],
newTags = this.get('tags'),
tagsToDetach,
existingTagIDs,
tagsToCreateAndAdd,
tagsToAddByID,
fetchOperation;
if (!newTags) {
return;
}
fetchOperation = Post.forge({id: this.id}).fetch({withRelated: ['tags']});
return fetchOperation.then(function (thisModelWithTags) {
var existingTags = thisModelWithTags.related('tags').models;
tagsToDetach = existingTags.filter(function (existingTag) {
var tagStillRemains = newTags.some(function (newTag) {
return newTag.id === existingTag.id;
});
return !tagStillRemains;
});
if (tagsToDetach.length > 0) {
tagOperations.push(self.tags().detach(tagsToDetach));
}
// Detect any tags that have been added by ID
existingTagIDs = existingTags.map(function (existingTag) {
return existingTag.id;
});
tagsToAddByID = newTags.filter(function (newTag) {
return existingTagIDs.indexOf(newTag.id) === -1;
});
if (tagsToAddByID.length > 0) {
tagsToAddByID = _.pluck(tagsToAddByID, 'id');
tagOperations.push(self.tags().attach(tagsToAddByID));
}
// Detect any tags that have been added, but don't already exist in the database
tagsToCreateAndAdd = newTags.filter(function (newTag) {
return newTag.id === null || newTag.id === undefined;
});
tagsToCreateAndAdd.forEach(function (tagToCreateAndAdd) {
var createAndAddOperation = Tag.add({name: tagToCreateAndAdd.name}).then(function (createdTag) {
return self.tags().attach(createdTag.id);
});
tagOperations.push(createAndAddOperation);
});
return when.all(tagOperations);
});
},
// Relations
user: function () {
return this.belongsTo(User, 'created_by');
},
author: function () {
return this.belongsTo(User, 'author_id');
},
tags: function () {
return this.belongsToMany(Tag);
}
}, {
// #### findAll
// Extends base model findAll to eager-fetch author and user relationships.
findAll: function (options) {
options = options || {};
options.withRelated = [ "author", "user", "tags" ];
return GhostBookshelf.Model.findAll.call(this, options);
},
// #### findOne
// Extends base model findOne to eager-fetch author and user relationships.
findOne: function (args, options) {
options = options || {};
options.withRelated = [ "author", "user", "tags" ];
return GhostBookshelf.Model.findOne.call(this, args, options);
},
// #### findPage
// Find results by page - returns an object containing the
// information about the request (page, limit), along with the
@ -163,6 +282,8 @@ Post = GhostBookshelf.Model.extend({
postCollection.query('where', opts.where);
}
opts.withRelated = [ "author", "user", "tags" ];
// Set the limit & offset for the query, fetching
// with the opts (to specify any eager relations, etc.)
// Omitting the `page`, `limit`, `where` just to be sure
@ -221,20 +342,30 @@ Post = GhostBookshelf.Model.extend({
}, errors.logAndThrowError);
}
// TODO: This logic is temporary, will probably need to be updated
// Check if any permissions apply for this user and post.
hasPermission = _.any(userPermissions, function (perm) {
if (perm.get('object_type') !== 'post') {
// Check for matching action type and object type
if (perm.get('action_type') !== action_type ||
perm.get('object_type') !== 'post') {
return false;
}
// True, if no object_id specified, or it matches
// If asking whether we can create posts,
// and we have a create posts permission then go ahead and say yes
if (action_type === 'create' && perm.get('action_type') === action_type) {
return true;
}
// Check for either no object id or a matching one
return !perm.get('object_id') || perm.get('object_id') === postModel.id;
});
// If this is the author of the post, allow it.
hasPermission = hasPermission || userId === postModel.get('author_id');
// Moved below the permissions checks because there may not be a postModel
// in the case like canThis(user).create.post()
hasPermission = hasPermission || (postModel && userId === postModel.get('author_id'));
// Resolve if we have appropriate permissions
if (hasPermission) {
return when.resolve();
}

View file

@ -7,6 +7,25 @@ var User = require('./user').User,
Role = GhostBookshelf.Model.extend({
tableName: 'roles',
permittedAttributes: ['id', 'name', 'description'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
users: function () {
return this.belongsToMany(User);
},

View file

@ -3,18 +3,42 @@ var Settings,
uuid = require('node-uuid'),
_ = require('underscore'),
errors = require('../errorHandling'),
when = require('when');
when = require('when'),
defaultSettings = require('../data/default-settings.json');
// Each setting is saved as a separate row in the database,
// but the overlying API treats them as a single key:value mapping
Settings = GhostBookshelf.Model.extend({
tableName: 'settings',
hasTimestamps: true,
permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'],
defaults: function () {
return {
uuid: uuid.v4(),
type: 'general'
};
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
// TODO: validate value, check type is one of the allowed values etc
GhostBookshelf.validator.check(this.get('key'), "Setting key cannot be blank").notEmpty();
GhostBookshelf.validator.check(this.get('type'), "Setting type cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes);
}
}, {
read: function (_key) {
@ -34,12 +58,33 @@ Settings = GhostBookshelf.Model.extend({
// Accept an array of models as input
if (item.toJSON) { item = item.toJSON(); }
return settings.forge({ key: item.key }).fetch().then(function (setting) {
return setting.set('value', item.value).save();
if (setting) {
return setting.set('value', item.value).save();
}
return settings.forge({ key: item.key, value: item.value }).save();
}, errors.logAndThrowError);
});
},
populateDefaults: function () {
return this.findAll().then(function (allSettings) {
var usedKeys = allSettings.models.map(function (setting) { return setting.get('key'); }),
insertOperations = [];
defaultSettings.forEach(function (defaultSetting) {
var isMissingFromDB = usedKeys.indexOf(defaultSetting.key) === -1;
if (isMissingFromDB) {
insertOperations.push(Settings.forge(defaultSetting).save());
}
});
return when.all(insertOperations);
});
}
});
module.exports = {
Settings: Settings
};
};

23
core/server/models/tag.js Normal file
View file

@ -0,0 +1,23 @@
var Tag,
Tags,
Posts = require('./post').Posts,
GhostBookshelf = require('./base');
Tag = GhostBookshelf.Model.extend({
tableName: 'tags',
posts: function () {
return this.belongsToMany(Posts);
}
});
Tags = GhostBookshelf.Collection.extend({
model: Tag
});
module.exports = {
Tag: Tag,
Tags: Tags
};

View file

@ -13,25 +13,73 @@ var User,
Role = require('./role').Role,
Permission = require('./permission').Permission;
UserRole = GhostBookshelf.Model.extend({
tableName: 'roles_users'
});
function validatePasswordLength(password) {
try {
GhostBookshelf.validator.check(password, "Your password is not long enough. It must be at least 8 chars long.").len(8);
} catch (error) {
return when.reject(error);
}
return when.resolve();
}
User = GhostBookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true,
permittedAttributes: [
'id', 'uuid', 'full_name', 'password', 'email_address', 'profile_picture', 'cover_picture', 'bio', 'url', 'location',
'created_at', 'created_by', 'updated_at', 'updated_by'
],
defaults: function () {
return {
uuid: uuid.v4()
};
},
parse: function (attrs) {
// temporary alias of name for full_name (will get changed in the schema)
if (attrs.full_name && !attrs.name) {
attrs.name = attrs.full_name;
}
// temporary alias of website for url (will get changed in the schema)
if (attrs.url && !attrs.website) {
attrs.website = attrs.url;
}
return attrs;
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
GhostBookshelf.validator.check(this.get('email_address'), "Please check your email address. It does not seem to be valid.").isEmail();
GhostBookshelf.validator.check(this.get('bio'), "Your bio is too long. Please keep it to 200 chars.").len(0, 200);
if (this.get('url') && this.get('url').length > 0) {
GhostBookshelf.validator.check(this.get('url'), "Your website is not a valid URL.").isUrl();
}
return true;
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
posts: function () {
return this.hasMany(Posts, 'created_by');
},
@ -54,55 +102,58 @@ User = GhostBookshelf.Model.extend({
*/
add: function (_user) {
var User = this,
// Clone the _user so we don't expose the hashed password unnecessarily
userData = _.extend({}, _user),
fail = false,
userRoles = {
"role_id": 1,
"user_id": 1
};
var self = this,
// Clone the _user so we don't expose the hashed password unnecessarily
userData = _.extend({}, _user);
/**
* This only allows one user to be added to the database, otherwise fails.
* @param {object} user
* @author javorszky
*/
return this.forge().fetch().then(function (user) {
return validatePasswordLength(userData.password).then(function () {
return self.forge().fetch();
}).then(function (user) {
// Check if user exists
if (user) {
fail = true;
}
if (fail) {
return when.reject(new Error('A user is already registered. Only one user for now!'));
}
}).then(function () {
// Hash the provided password with bcrypt
return nodefn.call(bcrypt.hash, _user.password, null, null);
}).then(function (hash) {
// Assign the hashed password
userData.password = hash;
// Save the user with the hashed password
return GhostBookshelf.Model.add.call(self, userData);
}).then(function (addedUser) {
// Assign the userData to our created user so we can pass it back
userData = addedUser;
// Add this user to the admin role (assumes admin = role_id: 1)
return UserRole.add({role_id: 1, user_id: addedUser.id});
}).then(function (addedUserRole) {
// Return the added user as expected
return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
userData.password = hash;
GhostBookshelf.Model.add.call(UserRole, userRoles);
return GhostBookshelf.Model.add.call(User, userData);
}, errors.logAndThrowError);
}, errors.logAndThrowError);
return when.resolve(userData);
});
/**
* Temporarily replacing the function below with another one that checks
* whether there's anyone registered at all. This is due to #138
* @author javorszky
*/
/**
return this.forge({email_address: userData.email_address}).fetch().then(function (user) {
if (!!user.attributes.email_address) {
return when.reject(new Error('A user with that email address already exists.'));
}
return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
userData.password = hash;
return GhostBookshelf.Model.add.call(User, userData);
});
});
*/
// return this.forge({email_address: userData.email_address}).fetch().then(function (user) {
// if (user !== null) {
// return when.reject(new Error('A user with that email address already exists.'));
// }
// return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
// userData.password = hash;
// GhostBookshelf.Model.add.call(UserRole, userRoles);
// return GhostBookshelf.Model.add.call(User, userData);
// }, errors.logAndThrowError);
// }, errors.logAndThrowError);
},
// Finds the user by email, and checks the password
@ -112,11 +163,13 @@ User = GhostBookshelf.Model.extend({
}).fetch({require: true}).then(function (user) {
return nodefn.call(bcrypt.compare, _userdata.pw, user.get('password')).then(function (matched) {
if (!matched) {
return when.reject(new Error('Passwords do not match'));
return when.reject(new Error('Your password is incorrect'));
}
return user;
}, errors.logAndThrowError);
}, errors.logAndThrowError);
}, function (error) {
return when.reject(new Error('There is no user with that email address.'));
});
},
/**
@ -125,28 +178,47 @@ User = GhostBookshelf.Model.extend({
*
*/
changePassword: function (_userdata) {
var email = _userdata.email,
var self = this,
userid = _userdata.currentUser,
oldPassword = _userdata.oldpw,
newPassword = _userdata.newpw,
ne2Password = _userdata.ne2pw;
ne2Password = _userdata.ne2pw,
user = null;
if (newPassword !== ne2Password) {
return when.reject(new Error('Passwords aren\'t the same'));
return when.reject(new Error('Your new passwords do not match'));
}
return this.forge({
email_address: email
}).fetch({require: true}).then(function (user) {
return nodefn.call(bcrypt.compare, oldPassword, user.get('password'))
.then(function (matched) {
if (!matched) {
return when.reject(new Error('Passwords do not match'));
}
return nodefn.call(bcrypt.hash, newPassword, null, null).then(function (hash) {
user.save({password: hash});
return user;
});
});
return validatePasswordLength(newPassword).then(function () {
return self.forge({id: userid}).fetch({require: true});
}).then(function (_user) {
user = _user;
return nodefn.call(bcrypt.compare, oldPassword, user.get('password'));
}).then(function (matched) {
if (!matched) {
return when.reject(new Error('Your password is incorrect'));
}
return nodefn.call(bcrypt.hash, newPassword, null, null);
}).then(function (hash) {
user.save({password: hash});
return user;
});
},
forgottenPassword: function (email) {
var newPassword = Math.random().toString(36).slice(2, 12), // This is magick
user = null;
return this.forge({email_address: email}).fetch({require: true}).then(function (_user) {
user = _user;
return nodefn.call(bcrypt.hash, newPassword, null, null);
}).then(function (hash) {
user.save({password: hash});
return { user: user, newPassword: newPassword };
}, function (error) {
return when.reject(new Error('There is no user by that email address. Check again.'));
});
},

View file

@ -13,6 +13,14 @@ var _ = require('underscore'),
CanThisResult,
exported;
function hasActionsMap() {
// Just need to find one key in the actionsMap
return _.any(exported.actionsMap, function (val, key) {
return Object.hasOwnProperty(key);
});
}
// Base class for canThis call results
CanThisResult = function () {
this.userPermissionLoad = false;
@ -98,14 +106,16 @@ CanThisResult.prototype.beginCheck = function (user) {
var self = this,
userId = user.id || user;
if (!hasActionsMap()) {
throw new Error("No actions map found, please call permissions.init() before use.");
}
// TODO: Switch logic based on object type; user, role, post.
// Kick off the fetching of the user data
this.userPermissionLoad = UserProvider.effectivePermissions(userId);
// Iterate through the actions and their related object types
// We should have loaded these through a permissions.init() call previously
// TODO: Throw error if not init() yet?
_.each(exported.actionsMap, function (obj_types, act_type) {
// Build up the object type handlers;
// the '.post()' parts in canThis(user).edit.post()

View file

@ -11,7 +11,7 @@
<meta name="author" content="">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="shortcut icon" href="/favicon.ico">
@ -28,7 +28,7 @@
<main role="main" id="main">
<aside id="flashbar">
{{> flashes}}
{{> notifications}}
</aside>
{{{body}}}
@ -52,34 +52,41 @@
<script src="/shared/vendor/jquery/jquery.fileupload.js"></script>
<script src="/public/vendor/codemirror/codemirror.js"></script>
<script src="/public/vendor/codemirror/addon/mode/overlay.js"></script>
<script src="/public/vendor/codemirror/mode/markdown/markdown.js"></script>
<script src="/public/vendor/codemirror/mode/gfm/gfm.js"></script>
<script src="/public/vendor/showdown/showdown.js"></script>
<script src="/public/vendor/showdown/extensions/ghostdown.js"></script>
<script src="/shared/vendor/showdown/extensions/github.js"></script>
<script src="/public/vendor/shortcuts.js"></script>
<script src="/public/vendor/countable.js"></script>
<script src="/public/vendor/to-title-case.js"></script>
<script src="/public/vendor/packery.pkgd.min.js"></script>
<script src="/public/vendor/jquery.hammer.min.js"></script>
<script src="/public/init.js"></script>
<script src="/public/assets/lib/uploader.js"></script>
<script src="/public/tpl/hbs-tpl.js"></script>
<script src="/public/mobile-interactions.js"></script>
<script src="/public/toggle.js"></script>
<script src="/public/markdown-actions.js"></script>
<script src="/public/tagui.js"></script>
<script src="/public/helpers/index.js"></script>
<!-- // require '/public/models/*' -->
<script src="/public/models/post.js"></script>
<script src="/public/models/user.js"></script>
<script src="/public/models/tag.js"></script>
<script src="/public/models/widget.js"></script>
<script src="/public/models/settings.js"></script>
<script src="/public/models/themes.js"></script>
<!-- // require '/public/views/*' -->
<script src="/public/views/base.js"></script>
<script src="/public/views/dashboard.js"></script>
<script src="/public/views/blog.js"></script>
<script src="/public/views/editor.js"></script>
<script src="/public/views/editor-tag-widget.js"></script>
<script src="/public/views/login.js"></script>
<script src="/public/views/settings.js"></script>
<script src="/public/views/debug.js"></script>

View file

@ -5,7 +5,7 @@
<section class="box entry-title">
<input type="text" id="entry-title"
placeholder="{{e "editor.entry_title.placeholder" "The Post Title Gets Inserted Up Here"}}"
value="{{title}}" tabindex="1">
value="" tabindex="1">
</section>
</header>
@ -32,18 +32,17 @@
</section>
<footer id="publish-bar">
<nav>
<section id="entry-categories" href="#" class="left">
<label class="category-label" for="categories"><span class="hidden">Categories</span></label>
<div class="categories"></div>
<input type="hidden" class="category-holder" id="category-holder">
<input class="category-input" id="categories" type="text"
data-populate-hidden="#category-holder" data-input-behaviour="tag" data-populate=".categories" />
<ul class="suggestions overlay" data-populate=".categories"></ul>
<section id="entry-tags" href="#" class="left">
<label class="tag-label" for="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-actions" class="splitbutton-save">
<button type="button" class="button-save js-post-button"></button>
<a class="options up" href="#"><span class="hidden">Options</span></a>
<a class="options up" data-toggle="ul" href="#"><span class="hidden">Options</span></a>
<ul class="editor-options overlay" style="display:none">
<li data-set-status="published"><a href="#">Publish Now</a></li>
<li data-set-status="queue"><a href="#">Add to Queue</a></li>
@ -53,4 +52,4 @@
</section>
</div>
</nav>
</footer>
</footer>

View file

@ -1,6 +1,6 @@
<header id="global-header" class="navbar">
<a id="ghost" href="http://vip.tryghost.org" data-off-canvas="left"><span class="hidden">Ghost</span></a>{{! TODO: Change this to actual Ghost homepage }}
<a class="ghost-logo" href="http://vip.tryghost.org" data-off-canvas="left"><span class="hidden">Ghost</span></a>{{! TODO: Change this to actual Ghost homepage }}
<nav id="global-nav" role="navigation">
<ul id="main-menu" >
{{#each adminNav}}
@ -9,16 +9,16 @@
<li id="usermenu" class="subnav">
<a href="#" data-toggle="ul" class="dropdown">
<img class="avatar" src="/public/img/user.jpg" alt="Avatar" />
<span class="name">Ghost v{{version}}</span>
<img class="avatar" src="{{#if currentUser.profile}}{{currentUser.profile}}{{else}}/public/img/user.jpg{{/if}}" alt="Avatar" />
<span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}Ghost{{/if}} v{{version}}</span>
</a>
<ul class="overlay">
<li class="usermenu-profile"><a href="#">Your Profile</a></li>
<li class="usermenu-profile"><a href="/ghost/settings/user">Your Profile</a></li>
<li class="divider"></li>
<li class="usermenu-help"><a href="#">Help / Support</a></li>
<li class="usermenu-shortcuts"><a href="#">Keyboard Shortcuts</a></li>
<li class="divider"></li>
<li class="usermenu-signout"><a href="/logout/">Sign Out</a></li>
<li class="usermenu-signout"><a href="/signout/">Sign Out</a></li>
</ul>
</li>
</ul>

View file

@ -5,7 +5,7 @@
"admin.navbar.settings": "Settings",
"__SECTION__": "icons",
"icon.category.label": "Category",
"icon.tag.label": "Tag",
"icon.faq.label": "?",
"icon.faq.markdown.title": "What is Markdown?",
"icon.full_screen.label": "Full Screen",
@ -23,4 +23,4 @@
"editor.actions.save_draft": "Save Draft",
"editor.actions.publish": "Publish"
}
}

File diff suppressed because one or more lines are too long

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