Merge branch 'ember'

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

28
ghost/admin/app.js Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

75
ghost/admin/assets/vendor/loader.js vendored Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
/* global CodeMirror*/
var onChangeHandler = function (cm) {
cm.component.set('value', cm.getDoc().getValue());
};
var onScrollHandler = function (cm) {
var scrollInfo = cm.getScrollInfo(),
percentage = scrollInfo.top / scrollInfo.height,
component = cm.component;
// throttle scroll updates
component.throttle = Ember.run.throttle(component, function () {
this.set('scrollPosition', percentage);
}, 50);
};
var Codemirror = Ember.TextArea.extend({
initCodemirror: function () {
// create codemirror
this.codemirror = CodeMirror.fromTextArea(this.get('element'), {
lineWrapping: true
});
this.codemirror.component = this; // save reference to this
// propagate changes to value property
this.codemirror.on('change', onChangeHandler);
// on scroll update scrollPosition property
this.codemirror.on('scroll', onScrollHandler);
}.on('didInsertElement'),
removeThrottle: function () {
Ember.run.cancel(this.throttle);
}.on('willDestroyElement'),
removeCodemirrorHandlers: function () {
// not sure if this is needed.
this.codemirror.off('change', onChangeHandler);
this.codemirror.off('scroll', onScrollHandler);
}.on('willDestroyElement')
});
export default Codemirror;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,269 @@
var posts = [
{
"id": 4,
"uuid": "4dc16b9e-bf90-44c9-97c5-40a0a81e8297",
"title": "This post is featured",
"slug": "this-post-is-featured",
"markdown": "Lorem **ipsum** dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.",
"html": "<p>Lorem <strong>ipsum<\/strong> dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.<\/p>",
"image": null,
"featured": 1,
"page": 0,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": "2014-02-15T23:27:08.000Z",
"published_by": 1,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "Bill Murray",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 3,
"uuid": "4dc16b9e-bf90-44c9-97c5-40a0a81e8297",
"title": "Example page entry",
"slug": "example-page-entry",
"markdown": "Lorem **ipsum** dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.",
"html": "<p>Lorem <strong>ipsum<\/strong> dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.<\/p>",
"image": null,
"featured": 0,
"page": 1,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "Slimer",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 2,
"uuid": "4dc1cb9e-bf90-44c9-97c5-40a8381e8297",
"title": "Dummy draft post",
"slug": "dummy-draft-post",
"markdown": "Lorem **ipsum** dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.",
"html": "<p>Lorem <strong>ipsum<\/strong> dolor sit amet, consectetur adipiscing elit. Fusce id felis nec est suscipit scelerisque vitae eu arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam erat volutpat. Sed pellentesque metus vel velit tincidunt aliquet. Nunc condimentum tempus convallis. Sed tincidunt, leo et congue blandit, lorem tortor imperdiet sapien, et porttitor turpis nisl sed tellus. In ultrices urna sit amet mauris suscipit adipiscing.<\/p>",
"image": null,
"featured": 0,
"page": 0,
"status": "draft",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T23:27:08.000Z",
"created_by": 1,
"updated_at": "2014-02-15T23:27:08.000Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
]
},
{
"id": 1,
"uuid": "4b96025d-050c-47ff-8bd4-047e4843b302",
"title": "Welcome to Ghost",
"slug": "welcome-to-ghost",
"markdown": "You're live! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. You can manage your content by signing in to the admin area at `<your blog URL>\/ghost\/`. When you arrive, you can select this post from a list on the left and see a preview of it on the right. Click the little pencil icon at the top of the preview to edit this post and read the next section!\n\n## Getting Started\n\nGhost uses something called Markdown for writing. Essentially, it's a shorthand way to manage your post formatting as you write!\n\nWriting in Markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use *shortcuts* to **style** your content. For example, a list:\n\n* Item number one\n* Item number two\n * A nested item\n* A final item\n\nor with numbers!\n\n1. Remember to buy some milk\n2. Drink the milk\n3. Tweet that I remembered to buy the milk, and drank it\n\n### Links\n\nWant to link to a source? No problem. If you paste in url, like http:\/\/ghost.org - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to [the Ghost website](http:\/\/ghost.org). Neat.\n\n### What about Images?\n\nImages work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:\n\n![The Ghost Logo](https:\/\/ghost.org\/images\/ghost.png)\n\nNot sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:\n\n![A bowl of bananas]\n\n\n### Quoting\n\nSometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.\n\n> Wisdomous - it's definitely a word.\n\n### Working with Code\n\nGot a streak of geek? We've got you covered there, too. You can write inline `<code>` blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.\n\n .awesome-thing {\n display: block;\n width: 100%;\n }\n\n### Ready for a Break? \n\nThrow 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.\n\n---\n\n### Advanced Usage\n\nThere's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.\n\n<input type=\"text\" placeholder=\"I'm an input field!\" \/>\n\nThat should be enough to get you started. Have fun - and let us know what you think :)",
"html": "<p>You're live! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. You can manage your content by signing in to the admin area at <code>&lt;your blog URL&gt;\/ghost\/<\/code>. When you arrive, you can select this post from a list on the left and see a preview of it on the right. Click the little pencil icon at the top of the preview to edit this post and read the next section!<\/p>\n\n<h2 id=\"gettingstarted\">Getting Started<\/h2>\n\n<p>Ghost uses something called Markdown for writing. Essentially, it's a shorthand way to manage your post formatting as you write!<\/p>\n\n<p>Writing in Markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use <em>shortcuts<\/em> to <strong>style<\/strong> your content. For example, a list:<\/p>\n\n<ul>\n<li>Item number one<\/li>\n<li>Item number two\n<ul><li>A nested item<\/li><\/ul><\/li>\n<li>A final item<\/li>\n<\/ul>\n\n<p>or with numbers!<\/p>\n\n<ol>\n<li>Remember to buy some milk <\/li>\n<li>Drink the milk <\/li>\n<li>Tweet that I remembered to buy the milk, and drank it<\/li>\n<\/ol>\n\n<h3 id=\"links\">Links<\/h3>\n\n<p>Want to link to a source? No problem. If you paste in url, like <a href='http:\/\/ghost.org'>http:\/\/ghost.org<\/a> - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to <a href=\"http:\/\/ghost.org\">the Ghost website<\/a>. Neat.<\/p>\n\n<h3 id=\"whataboutimages\">What about Images?<\/h3>\n\n<p>Images work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:<\/p>\n\n<p><img src=\"https:\/\/ghost.org\/images\/ghost.png\" alt=\"The Ghost Logo\" \/><\/p>\n\n<p>Not sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:<\/p>\n\n<h3 id=\"quoting\">Quoting<\/h3>\n\n<p>Sometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.<\/p>\n\n<blockquote>\n <p>Wisdomous - it's definitely a word.<\/p>\n<\/blockquote>\n\n<h3 id=\"workingwithcode\">Working with Code<\/h3>\n\n<p>Got a streak of geek? We've got you covered there, too. You can write inline <code>&lt;code&gt;<\/code> blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.<\/p>\n\n<pre><code>.awesome-thing {\n display: block;\n width: 100%;\n}\n<\/code><\/pre>\n\n<h3 id=\"readyforabreak\">Ready for a Break?<\/h3>\n\n<p>Throw 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.<\/p>\n\n<hr \/>\n\n<h3 id=\"advancedusage\">Advanced Usage<\/h3>\n\n<p>There's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.<\/p>\n\n<p><input type=\"text\" placeholder=\"I'm an input field!\" \/><\/p>\n\n<p>That should be enough to get you started. Have fun - and let us know what you think :)<\/p>",
"image": null,
"featured": 0,
"page": 0,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": "2014-02-15T20:02:01.000Z",
"created_by": 1,
"updated_at": "2014-02-15T20:02:01.000Z",
"updated_by": 1,
"published_at": "2014-02-15T20:02:01.000Z",
"published_by": 1,
"author": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"user": {
"id": 1,
"uuid": "ba9c67e4-8046-4b8c-9349-0eed3cca7529",
"name": "manuel_mitasch",
"slug": "manuel_mitasch",
"email": "manuel@cms.mine.nu",
"image": null,
"cover": null,
"bio": null,
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:25.000Z",
"updated_at": "2014-02-15T20:02:25.000Z"
},
"tags": [
{
"id": 1,
"uuid": "406edaaf-5b1c-4199-b297-2af90b1de1a7",
"name": "Getting Started",
"slug": "getting-started",
"description": null,
"parent_id": null,
"meta_title": null,
"meta_description": null,
"created_at": "2014-02-15T20:02:01.000Z",
"created_by": 1,
"updated_at": "2014-02-15T20:02:01.000Z",
"updated_by": 1
}
]
}
];
export default posts;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
/* global moment */
var formatTimeago = Ember.Handlebars.makeBoundHelper(function (timeago) {
return moment(timeago).fromNow();
// stefanpenner says cool for small number of timeagos.
// For large numbers moment sucks => single Ember.Object based clock better
// https://github.com/manuelmitasch/ghost-admin-ember-demo/commit/fba3ab0a59238290c85d4fa0d7c6ed1be2a8a82e#commitcomment-5396524
});
export default formatTimeago;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
ghost/admin/routes/new.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
<footer id="publish-bar">
<nav>
<section id="entry-tags" href="#" class="left">
<label class="tag-label" for="tags" title="Tags"><span class="hidden">Tags</span></label>
<div class="tags"></div>
<input type="hidden" class="tags-holder" id="tags-holder">
<input class="tag-input" id="tags" type="text" data-input-behaviour="tag" />
<ul class="suggestions overlay"></ul>
</section>
<div class="right">
<section id="entry-controls">
<a class="post-settings" title="Post Settings" {{action 'editSettings'}}><span class="hidden">Post Settings</span></a>
<!-- @TODO Use Ghost Popover (#2565) and style arrow down -->
{{view "post-settings-menu-view"}}
</section>
<section id="entry-actions" class="js-publish-splitbutton splitbutton-save">
<button type="button" class="js-publish-button button-save">Save Draft</button>
<a class="options up" data-toggle="ul" href="#" title="Post Settings"><span class="hidden">Post Settings</span></a>
{{!-- @TODO: implement popover --}}
<ul class="editor-options overlay" style="display:none">
<li data-set-status="published"><a href="#"></a></li>
<li data-set-status="draft"><a href="#"></a></li>
</ul>
</section>
</div>
</nav>
</footer>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
<section class="entry-container">
<header>
<section class="box entry-title">
{{input type="text" id="entry-title" placeholder="Your Post Title" value=title tabindex="1"}}
</section>
</header>
<section class="entry-markdown active">
<header class="floatingheader">
<small>Markdown</small>
<a class="markdown-help" href="" {{action "openModal" "markdown"}}><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
{{-codemirror value=markdown scrollPosition=view.scrollPosition}}
</section>
</section>
<section class="entry-preview">
<header class="floatingheader">
<small>Preview <span class="entry-word-count js-entry-word-count">{{count-words markdown}} words</span></small>
</header>
<section class="entry-preview-content">
{{-markdown markdown=markdown scrollPosition=view.scrollPosition}}
</section>
</section>
</section>
{{partial 'publish-bar'}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
TODO

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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