diff --git a/.afignore b/.afignore new file mode 100644 index 0000000000..08665d9e86 --- /dev/null +++ b/.afignore @@ -0,0 +1,24 @@ +#ignore database +b-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log + +.idea/* +*.iml +projectFilesBackup + +.DS_Store + +# Ghost DB file +ghost/data/*.db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8f567c00f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +b-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +node_modules + +.idea/* +*.iml +projectFilesBackup + +.DS_Store + +# Ghost DB file +*.db + +/core/admin/assets/css +.sass-cache/ +/core/admin/assets/sass/config.rb +/core/admin/assets/sass/layouts/config.rb +/core/admin/assets/sass/modules/config.rb +/ghost/.idea/ \ No newline at end of file diff --git a/.groc.json b/.groc.json new file mode 100644 index 0000000000..a21fd648bc --- /dev/null +++ b/.groc.json @@ -0,0 +1,5 @@ +{ + "glob": ["README.md", "config.js", "app.js", "core/ghost.js", "core/admin/assets/js/*.js", "core/frontend/helpers/index.js", "core/lang/i18n.js"], + "except": ["core/admin/assets/lib/chart.min.js"], + "out": "./docs" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..240a1516d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 Ghost + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a1624f8c14..433d66f06b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ -Ghost -===== +# Ghost + +Welcome to the Ghost core repo. The code here is the result of a few stolen hours of free time hacking a proof of concept for the Kickstarter video. Pretty much everything is subject to and expected to change. + +The top priorities right now are: + +* Having a core RESTful API and consuming it internally +* Data model design & implementation - including a potential switch from JugglingDB to bookshelf.js +* Authentication and ACL +* Improving core architecture & design - modular structure, better dependency injection, testable code with tests + + + +###To Install: + +**Note:** It is highly recommended that you use the [Ghost-Vagrant](https://github.com/TryGhost/Ghost-Vagrant) setup for developing Ghost. + +1. Clone the git repo +1. cd into the project folder and run ```npm install```. + * If the install fails with errors to do with "node-gyp rebuild", follow the Sqlite3 install instructions +1. cd into /core/admin/assets and run ```compass compile --css-dir=css``` + + +Frontend can be located at [localhost:3333](localhost:3333), Admin is at [localhost:3333/ghost](localhost:3333/ghost) + + +#### 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. + +However, 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. + +I have created some of these, and they can be obtained from [this GitHub issue](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. + + +###Dependencies: + +* express.js framework +* handlebars for templating +* standard css for frontend +* sass for admin (pre-compiled) +* moment.js for time / date manipulation +* underscore for object & array utils +* showdown for converting markdown to HTML +* nodeunit for unit testing +* sqlite3 for data storage +* jugglingdb ORM for interacting with the database +* Polyglot.js for i18n + +#### Frontend libraries: + +* jQuery 1.9.1 +* showdown for converting markdown to HTML +* codemirror editor + +### Working features: + +* Dashboard + * new post link +* Admin menu + * G, dashboard, content, new post & settings menu items go to correct pages +* Content screen + * Lists all posts with correct titles (incorrect time etc) + * 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 opening data/datastore.db and emptying 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 + +### Front End Work + +A SASS compiler is required to work with the CSS in this project. + +Run ```compass compile --css-dir=css``` from /core/admin/assets. + +We also recommend [CodeKit](http://incident57.com/codekit/) (Paid/Mac) and [Scout](http://mhs.github.io/scout-app/) (Free/Mac/PC). \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000000..b8663819c9 --- /dev/null +++ b/app.js @@ -0,0 +1,80 @@ +// # Ghost main app file + +/*global require */ +(function () { + "use strict"; + + // Module dependencies. + var express = require('express'), + fs = require('fs'), + admin = require('./core/admin/controllers'), + frontend = require('./core/frontend/controllers'), + flash = require('connect-flash'), + Ghost = require('./core/ghost'), + I18n = require('./core/lang/i18n'), + helpers = require('./core/frontend/helpers'), + auth, + + // ## Variables + /** + * Create new Ghost object + * @type {Ghost} + */ + ghost = new Ghost(); + + ghost.app().configure('development', function () { + ghost.app().use(express.favicon(__dirname + '/content/images/favicon.ico')); + ghost.app().use(express.errorHandler()); + ghost.app().use(I18n.load(ghost)); + ghost.app().use(express.bodyParser()); + ghost.app().use(express.cookieParser('try-ghost')); + ghost.app().use(express.session({ cookie: { maxAge: 60000 }})); + ghost.app().use(flash()); + ghost.app().use(ghost.initTheme(ghost.app())); + }); + + /** + * Setup login details + * p.s. love it. + * + * @type {*} + */ + auth = express.basicAuth('ghostadmin', 'Wh0YouGonnaCall?'); + + helpers.loadCoreHelpers(ghost); + + + /** + * API routes.. + * @todo convert these into a RESTful, public, authenticated API! + */ + ghost.app().post('/api/v0.1/posts/create', auth, admin.posts.create); + ghost.app().post('/api/v0.1/posts/edit', auth, admin.posts.edit); + ghost.app().get('/api/v0.1/posts', auth, admin.posts.index); + + /** + * Admin routes.. + * @todo put these somewhere in admin + */ + ghost.app().get('/ghost/editor/:id', auth, admin.editor); + ghost.app().get('/ghost/editor', auth, admin.editor); + ghost.app().get('/ghost/blog', auth, admin.blog); + ghost.app().get('/ghost/settings', auth, admin.settings); + ghost.app().get('/ghost/debug', auth, admin.debug.index); + ghost.app().get('/ghost/debug/db/delete/', auth, admin.debug.dbdelete); + ghost.app().get('/ghost/debug/db/populate/', auth, admin.debug.dbpopulate); + ghost.app().get('/ghost', auth, admin.index); + + /** + * Frontend routes.. + * @todo dynamic routing, homepage generator, filters ETC ETC + */ + ghost.app().get('/:slug', frontend.single); + ghost.app().get('/', frontend.homepage); + + + ghost.app().listen(3333, function () { + console.log("Express server listening on port " + 3333); + console.log('process: ', process.env); + }); +}()); \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000000..28875e22f6 --- /dev/null +++ b/config.js @@ -0,0 +1,62 @@ +// # Ghost Configuration + +/** + * global module + **/ +(function () { + "use strict"; + + /** + * @module config + * @type {Object} + */ + var config = {}; + + // ## Admin settings + + /** + * @property {string} defaultLang + */ + config.defaultLang = 'en'; + + /** + * @property {boolean} forceI18n + */ + config.forceI18n = true; + + // ## Themes + + /** + * @property {string} themeDir + */ + + // Themes + config.themeDir = 'themes'; + + /** + * @property {string} activeTheme + */ + config.activeTheme = 'casper'; + + // ## Homepage settings + /** + * @module homepage + * @type {Object} + */ + config.homepage = {}; + + /** + * @property {number} features + */ + config.homepage.features = 1; + + /** + * @property {number} posts + */ + config.homepage.posts = 4; + + /** + * @property {Object} exports + */ + module.exports = config; +}()); \ No newline at end of file diff --git a/config.rb b/config.rb new file mode 100644 index 0000000000..960397bb55 --- /dev/null +++ b/config.rb @@ -0,0 +1,26 @@ +# Require any additional compass plugins here. + + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "core/admin/assets/css" +sass_dir = "core/admin/assets/sass" +images_dir = "core/admin/assets/img" +javascripts_dir = "core/admin/assets/js" +fonts_dir = "core/admin/assets/fonts" + +output_style = :nested + +# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +# line_comments = false +color_output = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass diff --git a/content/README.md b/content/README.md new file mode 100644 index 0000000000..8f09551936 --- /dev/null +++ b/content/README.md @@ -0,0 +1,13 @@ +#Content + +This section of the repo is the area that a normal user is allowed to add and change stuff. This is where their themes, plugins and images will live. + +By default for an install: + +* the themes directory will contain Casper +* the plugins directory will be empty +* the images directory will be empty + +Currently the plugins and images directory contain some stuff for testing. + +By default, Ghost will support very basic image uploads. It will be expected and encouraged for users to connect to a 3rd party service for improved media support and a CDN. Much like comments, we don't see supporting advanced file uploads, having a media library or being a CDN as core competencies - there are already plenty of people out there doing this much better than we can. diff --git a/content/images/DSCF1202-1-800x420.jpg b/content/images/DSCF1202-1-800x420.jpg new file mode 100644 index 0000000000..20ac957539 Binary files /dev/null and b/content/images/DSCF1202-1-800x420.jpg differ diff --git a/content/images/DSCF1308-800x420.jpg b/content/images/DSCF1308-800x420.jpg new file mode 100644 index 0000000000..18f555f468 Binary files /dev/null and b/content/images/DSCF1308-800x420.jpg differ diff --git a/content/images/DSCF1703-800x420.jpg b/content/images/DSCF1703-800x420.jpg new file mode 100644 index 0000000000..27ae159d36 Binary files /dev/null and b/content/images/DSCF1703-800x420.jpg differ diff --git a/content/images/Egypt-Vimeo-Cover-800x420.jpg b/content/images/Egypt-Vimeo-Cover-800x420.jpg new file mode 100644 index 0000000000..c56b9b68cb Binary files /dev/null and b/content/images/Egypt-Vimeo-Cover-800x420.jpg differ diff --git a/content/images/depostimg.jpg b/content/images/depostimg.jpg new file mode 100644 index 0000000000..8a31745c61 Binary files /dev/null and b/content/images/depostimg.jpg differ diff --git a/content/images/favicon.ico b/content/images/favicon.ico new file mode 100644 index 0000000000..f614472c91 Binary files /dev/null and b/content/images/favicon.ico differ diff --git a/content/images/ghost-dashboard.jpg b/content/images/ghost-dashboard.jpg new file mode 100644 index 0000000000..9ff6a2c44c Binary files /dev/null and b/content/images/ghost-dashboard.jpg differ diff --git a/content/images/ghostpost.jpg b/content/images/ghostpost.jpg new file mode 100644 index 0000000000..2eaa9667f5 Binary files /dev/null and b/content/images/ghostpost.jpg differ diff --git a/content/images/logo.png b/content/images/logo.png new file mode 100644 index 0000000000..5d6532825c Binary files /dev/null and b/content/images/logo.png differ diff --git a/content/plugins/fancyFirstChar.js b/content/plugins/fancyFirstChar.js new file mode 100644 index 0000000000..f133346be9 --- /dev/null +++ b/content/plugins/fancyFirstChar.js @@ -0,0 +1,45 @@ +/*globals exports */ +(function () { + "use strict"; + + var FancyFirstChar; + + FancyFirstChar = function (ghost) { + this.ghost = function () { + return ghost; + }; + }; + FancyFirstChar.prototype.init = function () { + this.ghost().registerFilter('prePostsRender', function (posts) { + var post, + originalContent, + newContent, + firstCharIndex = 0; + + console.log('got content to filter', posts); + + for (post in posts) { + if (posts.hasOwnProperty(post)) { + originalContent = posts[post].content; + if (originalContent.substr(0, 1) === '<') { + firstCharIndex = originalContent.indexOf('>') + 1; + } + + newContent = originalContent.substr(0, firstCharIndex); + newContent += ''; + newContent += originalContent.substr(firstCharIndex, 1); + newContent += ''; + newContent += originalContent.substr(firstCharIndex + 1, originalContent.length - firstCharIndex - 1); + + posts[post].content = newContent; + } + } + return posts; + }); + }; + + FancyFirstChar.prototype.activate = function () {}; + FancyFirstChar.prototype.deactivate = function () {}; + + module.exports = FancyFirstChar; +}()); diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000000..9268207010 --- /dev/null +++ b/core/README.md @@ -0,0 +1,12 @@ +# Core + +Core contains the bread and butter of ghost. It is currently divided up into: + +* **admin** - the views, controllers, assets and helpers for rendering & working the admin panel +* **frontend** - the controllers & helpers for creating the frontend of the blog. Views & assets live in themes +* **lang** - the current home of everything i18n, this was done as a proof of concept on a very early version of the prototype and needs love +* **shared** - basically everything to do with data & models. The sqlite db file lives in the data folder here. This is the part that needs the most work so it doesn't make much sense yet, and is also the highest priority +* **test** - currently contains two sad unit tests and a set of html prototypes of the admin UI. Really, this folder should reflect all of core. It is my personal mission to make that happen ASAP & get us linked up with Travis. +* **ghost.js** - currently both the glue that binds everything together and what gives us the API for registering themes and plugins. The initTheme function is a bit of a hack which lets us serve different views & static content up for the admin & frontend. + +This structure is by no means final and recommendations are more than welcome. \ No newline at end of file diff --git a/core/admin/assets/fonts/icons.dev.svg b/core/admin/assets/fonts/icons.dev.svg new file mode 100644 index 0000000000..d707fa219b --- /dev/null +++ b/core/admin/assets/fonts/icons.dev.svg @@ -0,0 +1,196 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/admin/assets/fonts/icons.eot b/core/admin/assets/fonts/icons.eot new file mode 100644 index 0000000000..4c59f6a9d6 Binary files /dev/null and b/core/admin/assets/fonts/icons.eot differ diff --git a/core/admin/assets/fonts/icons.svg b/core/admin/assets/fonts/icons.svg new file mode 100644 index 0000000000..1a10f9a124 --- /dev/null +++ b/core/admin/assets/fonts/icons.svg @@ -0,0 +1,196 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/admin/assets/fonts/icons.ttf b/core/admin/assets/fonts/icons.ttf new file mode 100644 index 0000000000..1defe310dd Binary files /dev/null and b/core/admin/assets/fonts/icons.ttf differ diff --git a/core/admin/assets/fonts/icons.woff b/core/admin/assets/fonts/icons.woff new file mode 100644 index 0000000000..f95f6f16ee Binary files /dev/null and b/core/admin/assets/fonts/icons.woff differ diff --git a/core/admin/assets/img/dash/CampaignMonitor@2x.png b/core/admin/assets/img/dash/CampaignMonitor@2x.png new file mode 100644 index 0000000000..895a8a3c45 Binary files /dev/null and b/core/admin/assets/img/dash/CampaignMonitor@2x.png differ diff --git a/core/admin/assets/img/dash/Facebook@2x.png b/core/admin/assets/img/dash/Facebook@2x.png new file mode 100644 index 0000000000..d14600c087 Binary files /dev/null and b/core/admin/assets/img/dash/Facebook@2x.png differ diff --git a/core/admin/assets/img/dash/GooglePlus@2x.png b/core/admin/assets/img/dash/GooglePlus@2x.png new file mode 100644 index 0000000000..4534c86e76 Binary files /dev/null and b/core/admin/assets/img/dash/GooglePlus@2x.png differ diff --git a/core/admin/assets/img/dash/Image@2x.png b/core/admin/assets/img/dash/Image@2x.png new file mode 100644 index 0000000000..e75a4861ae Binary files /dev/null and b/core/admin/assets/img/dash/Image@2x.png differ diff --git a/core/admin/assets/img/dash/PostsStats@2x.png b/core/admin/assets/img/dash/PostsStats@2x.png new file mode 100644 index 0000000000..fac316bd0a Binary files /dev/null and b/core/admin/assets/img/dash/PostsStats@2x.png differ diff --git a/core/admin/assets/img/dash/Stats@2x.png b/core/admin/assets/img/dash/Stats@2x.png new file mode 100644 index 0000000000..92cd80479b Binary files /dev/null and b/core/admin/assets/img/dash/Stats@2x.png differ diff --git a/core/admin/assets/img/dash/Time@2x.png b/core/admin/assets/img/dash/Time@2x.png new file mode 100644 index 0000000000..bb705aea39 Binary files /dev/null and b/core/admin/assets/img/dash/Time@2x.png differ diff --git a/core/admin/assets/img/dash/Twitter@2x.png b/core/admin/assets/img/dash/Twitter@2x.png new file mode 100644 index 0000000000..3ab4e06312 Binary files /dev/null and b/core/admin/assets/img/dash/Twitter@2x.png differ diff --git a/core/admin/assets/img/ghost-icon.png b/core/admin/assets/img/ghost-icon.png new file mode 100644 index 0000000000..a5673f8c47 Binary files /dev/null and b/core/admin/assets/img/ghost-icon.png differ diff --git a/core/admin/assets/img/logo.png b/core/admin/assets/img/logo.png new file mode 100644 index 0000000000..18ae21c1f4 Binary files /dev/null and b/core/admin/assets/img/logo.png differ diff --git a/core/admin/assets/img/postimg.jpg b/core/admin/assets/img/postimg.jpg new file mode 100644 index 0000000000..b181ba40e3 Binary files /dev/null and b/core/admin/assets/img/postimg.jpg differ diff --git a/core/admin/assets/img/test-icon.png b/core/admin/assets/img/test-icon.png new file mode 100644 index 0000000000..cd8374360e Binary files /dev/null and b/core/admin/assets/img/test-icon.png differ diff --git a/core/admin/assets/img/user.jpg b/core/admin/assets/img/user.jpg new file mode 100644 index 0000000000..733c41b112 Binary files /dev/null and b/core/admin/assets/img/user.jpg differ diff --git a/core/admin/assets/img/users.png b/core/admin/assets/img/users.png new file mode 100644 index 0000000000..4e32e7ba99 Binary files /dev/null and b/core/admin/assets/img/users.png differ diff --git a/core/admin/assets/js/admin-ui-temp.js b/core/admin/assets/js/admin-ui-temp.js new file mode 100644 index 0000000000..1d0ac5cccc --- /dev/null +++ b/core/admin/assets/js/admin-ui-temp.js @@ -0,0 +1,75 @@ +// # Temporary Admin UI + +/*global document, jQuery */ + +(function ($) { + "use strict"; + + // UTILS + + /** + * Allows to check contents of each element exactly + * @param obj + * @param index + * @param meta + * @param stack + * @returns {boolean} + */ + $.expr[":"].containsExact = function (obj, index, meta, stack) { + return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3]; + }; + + + $(document).ready(function () { + + // ## Set interactions for all menus + // This finds all visible '.overlay' elements and hides them upon clicking away from the element itself. + $("body").on('click', function (event) { + var $target = $(event.target); + if (!$target.parents().is(".overlay:visible") && !$target.is(".overlay:visible")) { + $("body").find(".overlay:visible").fadeOut(); + } + }); + + // EDITOR / NOTIFICATIONS + + $('.entry-content header, .entry-preview header').on('click', function () { + $('.entry-content, .entry-preview').removeClass('active'); + $(this).closest('section').addClass('active'); + }); + + $('.entry-title .icon-fullscreen').on('click', function (e) { + e.preventDefault(); + $('body').toggleClass('fullscreen'); + }); + + $('.content-list-content li').on('click', function (e) { + var $target = $(e.target).closest('li'), + $preview = $('.content-preview'); + $('.content-list-content li').removeClass('active'); + $target.addClass('active'); + // ***** + // this means a *lot* of extra gumpf is in the DOM and should really be done with AJAX when we have proper + // data API endpoints + // ideally, we need a way to bind data to views properly... backbone marionette, angular, etc + // ***** + // + /** + * @todo Remove gumpf + */ + $preview.find('.content-preview-content .wrapper').html($target.data('content')); + $preview.find('.post-controls .post-edit').attr('href', '/ghost/editor/' + $target.data('id')); + }); + + $('.options.up').on('click', function (e) { + e.stopPropagation(); + $(this).next("ul").fadeToggle(200); + }); + + $('.editor-options').on('click', 'li', function (e) { + $('.button-save').data("state", $(this).data("title")).attr('data-state', $(this).data("title")).text($(this).text()); + $('.editor-options .active').removeClass('active'); + $(this).addClass('active'); + }); + }); +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/js/blog.js b/core/admin/assets/js/blog.js new file mode 100644 index 0000000000..84a2a199af --- /dev/null +++ b/core/admin/assets/js/blog.js @@ -0,0 +1,27 @@ +/*global window, history, jQuery, Showdown, CodeMirror */ +(function ($) { + "use strict"; + + $(document).ready(function () { + + // Shadow on Markdown if scrolled + $('.content-list-content').on('scroll', function (e) { + if ($('.content-list-content').scrollTop() > 10) { + $('.content-list').addClass('scrolling'); + } else { + $('.content-list').removeClass('scrolling'); + } + }); + + // Shadow on Preview if scrolled + $('.content-preview-content').on('scroll', function (e) { + if ($('.content-preview-content').scrollTop() > 10) { + $('.content-preview').addClass('scrolling'); + } else { + $('.content-preview').removeClass('scrolling'); + } + }); + + }); + +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/js/editor.js b/core/admin/assets/js/editor.js new file mode 100644 index 0000000000..1a56d386e4 --- /dev/null +++ b/core/admin/assets/js/editor.js @@ -0,0 +1,157 @@ +// # Article Editor + +/*global window, document, history, jQuery, Showdown, CodeMirror, shortcut */ +(function ($, ShowDown, CodeMirror, shortcut) { + "use strict"; + + // ## Converter Initialisation + /** + * @property converter + * @type {ShowDown.converter} + */ + // Initialise the Showdown converter for Markdown. + // var delay; + var converter = new ShowDown.converter({extensions: ['ghostdown']}), + editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), { + mode: 'markdown', + tabMode: 'indent', + lineWrapping: true + }); + + // ## Functions + /** + * @method Update word count + * @todo Really not the best way to do things as it includes Markdown formatting along with words + * @constructor + */ + // This updates the word count on the editor preview panel. + function updateWordCount() { + var wordCount = document.getElementsByClassName('entry-word-count')[0], + editorValue = editor.getValue(); + + if (editorValue.length) { + wordCount.innerHTML = editorValue.match(/\S+/g).length + ' words'; + } + } + + /** + * @method updatePreview + * @constructor + */ + // This updates the editor preview panel. + // Currently gets called on every key press. + // Also trigger word count update + function updatePreview() { + var preview = document.getElementsByClassName('rendered-markdown')[0]; + preview.innerHTML = converter.makeHtml(editor.getValue()); + + updateWordCount(); + } + + /** + * @method Save + * @constructor + */ + // This method saves a post + function save() { + var entry = { + title: document.getElementById('entry-title').value, + markdown: editor.getValue() + }, + urlSegments = window.location.pathname.split('/'); + + if (urlSegments[2] === 'editor' && urlSegments[3] && /^[a-zA-Z0-9]+$/.test(urlSegments[2])) { + entry.id = urlSegments[3]; + $.ajax({ + url: '/api/v0.1/posts/edit', + method: 'POST', + data: entry, + success: function (data) { + console.log('response', data); + }, + error: function (error) { + console.log('error', error); + } + }); + } else { + $.ajax({ + url: '/api/v0.1/posts/create', + method: 'POST', + data: entry, + success: function (data) { + console.log('response', data); + history.pushState(data, '', '/ghost/editor/' + data.id); + }, + error: function (jqXHR, status, error) { + var errors = JSON.parse(jqXHR.responseText); + console.log('FAILED', errors); + } + }); + } + } + + // ## Main Initialisation + $(document).ready(function () { + + $('.entry-markdown header, .entry-preview header').click(function (e) { + $('.entry-markdown, .entry-preview').removeClass('active'); + $(e.target).closest('section').addClass('active'); + }); + + editor.on("change", function () { + //clearTimeout(delay); + //delay = setTimeout(updatePreview, 50); + updatePreview(); + }); + + updatePreview(); + + $('.button-save').on('click', function () { + save(); + }); + + // Sync scrolling + function syncScroll(e) { + // vars + var $codeViewport = $(e.target), + $previewViewport = $('.entry-preview-content'), + $codeContent = $('.CodeMirror-sizer'), + $previewContent = $('.rendered-markdown'), + + // calc position + codeHeight = $codeContent.height() - $codeViewport.height(), + previewHeight = $previewContent.height() - $previewViewport.height(), + ratio = previewHeight / codeHeight, + previewPostition = $codeViewport.scrollTop() * ratio; + + // apply new scroll + $previewViewport.scrollTop(previewPostition); + + } + // TODO: Debounce + $('.CodeMirror-scroll').on('scroll', syncScroll); + + // Shadow on Markdown if scrolled + $('.CodeMirror-scroll').on('scroll', function (e) { + if ($('.CodeMirror-scroll').scrollTop() > 10) { + $('.entry-markdown').addClass('scrolling'); + } else { + $('.entry-markdown').removeClass('scrolling'); + } + }); + // Shadow on Preview if scrolled + $('.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.add("Alt+Shift+Z", function () { + $('body').toggleClass('zen'); + }); + + }); +}(jQuery, Showdown, CodeMirror, shortcut)); \ No newline at end of file diff --git a/core/admin/assets/js/settings.js b/core/admin/assets/js/settings.js new file mode 100644 index 0000000000..8f458d93e4 --- /dev/null +++ b/core/admin/assets/js/settings.js @@ -0,0 +1,21 @@ +/*globals document, jQuery */ +(function ($) { + "use strict"; + + var changePage = function (e) { + var newPage = $(this).children('a').attr('href'); + + e.preventDefault(); + $('.settings-menu .active').removeClass('active'); + $(this).addClass('active'); + + $('.settings-content').fadeOut().delay(250); + $(newPage).fadeIn(); + + }; + + $(document).ready(function() { + $('.settings-menu li').on('click', changePage); + }); + +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/js/tagui.js b/core/admin/assets/js/tagui.js new file mode 100644 index 0000000000..2713e8f8d3 --- /dev/null +++ b/core/admin/assets/js/tagui.js @@ -0,0 +1,196 @@ +// ## Tag Selector UI + +/*jslint regexp: true */ // - would like to remove this +/*global jQuery, document, window */ + +(function ($) { + "use strict"; + + var suggestions, + categoryOffset, + existingTags = [ // This will be replaced by an API return. + 'quim', + 'quimtastic', + 'quimmy', + 'quimlord', + 'quickly', + 'joaquim pheonix', + 'quimcy jones' + ], + 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("
  • " + results[i] + "
  • "); + } + + 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, "$1"); + src_str = src_str.replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1$2$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 = $('' + suggestions.children(".selected").text() + ''); + 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 = $('' + searchTerm + ''); + 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 = $('' + $(e.currentTarget).text() + ''), + 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')) { + 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); + }); +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/js/toggle.js b/core/admin/assets/js/toggle.js new file mode 100644 index 0000000000..3814426a87 --- /dev/null +++ b/core/admin/assets/js/toggle.js @@ -0,0 +1,30 @@ +// # Toggle Support + +/*global document, jQuery */ +(function ($) { + "use strict"; + $(document).ready(function () { + + // ## Toggle Up In Your Grill + // Allows for toggling via data-attributes. + // ### Usage + // + $('[data-toggle]').each(function () { + var toggle = $(this).data('toggle'); + $(this).parent().children(toggle).hide(); + }); + + $('[data-toggle]').on('click', function (e) { + e.preventDefault(); + $(this).toggleClass('active'); + var toggle = $(this).data('toggle'); + $(this).parent().children(toggle).fadeToggle(100).toggleClass('open'); + }); + + }); +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/lib/chart.min.js b/core/admin/assets/lib/chart.min.js new file mode 100644 index 0000000000..ab63588108 --- /dev/null +++ b/core/admin/assets/lib/chart.min.js @@ -0,0 +1,39 @@ +var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0