Use es6 across client and add ember-suave to enforce rules

no issue
- add ember-suave dependency
- upgrade grunt-jscs dependency
- add a new .jscsrc for the client's tests directory that extends from client's base .jscsrc
- separate client tests in Gruntfile jscs task so they pick up the test's .jscsrc
- standardize es6 usage across client
This commit is contained in:
Kevin Ansfield 2015-10-28 11:36:45 +00:00
parent 9437b8eeef
commit 3d6856614f
262 changed files with 3560 additions and 3159 deletions

View File

@ -126,8 +126,7 @@ var _ = require('lodash'),
client: {
options: {
esnext: true,
disallowObjectController: true
config: 'core/client/.jscsrc'
},
files: {
@ -135,6 +134,7 @@ var _ = require('lodash'),
'core/client/**/*.js',
'!core/client/node_modules/**/*.js',
'!core/client/bower_components/**/*.js',
'!core/client/tests/**/*.js',
'!core/client/tmp/**/*.js',
'!core/client/dist/**/*.js',
'!core/client/vendor/**/*.js'
@ -142,6 +142,18 @@ var _ = require('lodash'),
}
},
client_tests: {
options: {
config: 'core/client/tests/.jscsrc'
},
files: {
src: [
'core/client/tests/**/*.js'
]
}
},
server: {
files: {
src: [

14
core/client/.jscsrc Normal file
View File

@ -0,0 +1,14 @@
{
"preset": "ember-suave",
"validateIndentation": 4,
"disallowSpacesInFunction": null,
"disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInFunctionDeclaration": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideObjectBrackets": "all",
"requireCommentsToIncludeAccess": null,
"requireSpacesInsideObjectBrackets": null
}

View File

@ -2,7 +2,7 @@ import EmbeddedRelationAdapter from 'ghost/adapters/embedded-relation-adapter';
export default EmbeddedRelationAdapter.extend({
shouldBackgroundReloadRecord: function () {
shouldBackgroundReloadRecord() {
return false;
}

View File

@ -1,21 +1,22 @@
import Ember from 'ember';
import DS from 'ember-data';
import ghostPaths from 'ghost/utils/ghost-paths';
import Ember from 'ember';
const {inject} = Ember;
const {RESTAdapter} = DS;
export default DS.RESTAdapter.extend({
export default RESTAdapter.extend({
host: window.location.origin,
namespace: ghostPaths().apiRoot.slice(1),
session: inject.service('session'),
shouldBackgroundReloadRecord: function () {
shouldBackgroundReloadRecord() {
return false;
},
query: function (store, type, query) {
var id;
query(store, type, query) {
let id;
if (query.id) {
id = query.id;
@ -25,9 +26,9 @@ export default DS.RESTAdapter.extend({
return this.ajax(this.buildURL(type.modelName, id), 'GET', {data: query});
},
buildURL: function (type, id) {
buildURL(type, id) {
// Ensure trailing slashes
var url = this._super(type, id);
let url = this._super(type, id);
if (url.slice(-1) !== '/') {
url += '/';
@ -42,15 +43,15 @@ export default DS.RESTAdapter.extend({
// response body for successful DELETEs.
// Non-2xx (failure) responses will still work correctly as Ember will turn
// them into rejected promises.
deleteRecord: function () {
var response = this._super.apply(this, arguments);
deleteRecord() {
let response = this._super(...arguments);
return response.then(function () {
return response.then(() => {
return null;
});
},
handleResponse: function (status) {
handleResponse(status) {
if (status === 401) {
if (this.get('session.isAuthenticated')) {
this.get('session').invalidate();

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
import BaseAdapter from 'ghost/adapters/base';
const {get, isNone} = Ember;
// EmbeddedRelationAdapter will augment the query object in calls made to
// DS.Store#findRecord, findAll, query, and queryRecord with the correct "includes"
// (?include=relatedType) by introspecting on the provided subclass of the DS.Model.
@ -12,16 +14,16 @@ import BaseAdapter from 'ghost/adapters/base';
// roles: DS.hasMany('role', { embedded: 'always' }) => ?include=roles
export default BaseAdapter.extend({
find: function (store, type, id, snapshot) {
find(store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'find'), 'GET');
},
findRecord: function (store, type, id, snapshot) {
findRecord(store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord'), 'GET');
},
findAll: function (store, type, sinceToken) {
var query, url;
findAll(store, type, sinceToken) {
let query, url;
if (sinceToken) {
query = {since: sinceToken};
@ -32,60 +34,59 @@ export default BaseAdapter.extend({
return this.ajax(url, 'GET', {data: query});
},
query: function (store, type, query) {
query(store, type, query) {
return this._super(store, type, this.buildQuery(store, type.modelName, query));
},
queryRecord: function (store, type, query) {
queryRecord(store, type, query) {
return this._super(store, type, this.buildQuery(store, type.modelName, query));
},
createRecord: function (store, type, snapshot) {
createRecord(store, type, snapshot) {
return this.saveRecord(store, type, snapshot, {method: 'POST'});
},
updateRecord: function (store, type, snapshot) {
var options = {
updateRecord(store, type, snapshot) {
let options = {
method: 'PUT',
id: Ember.get(snapshot, 'id')
id: get(snapshot, 'id')
};
return this.saveRecord(store, type, snapshot, options);
},
saveRecord: function (store, type, snapshot, options) {
options = options || {};
saveRecord(store, type, snapshot, options) {
let _options = options || {};
let url = this.buildIncludeURL(store, type.modelName, _options.id, snapshot, 'createRecord');
let payload = this.preparePayload(store, type, snapshot);
var url = this.buildIncludeURL(store, type.modelName, options.id, snapshot, 'createRecord'),
payload = this.preparePayload(store, type, snapshot);
return this.ajax(url, options.method, payload);
return this.ajax(url, _options.method, payload);
},
preparePayload: function (store, type, snapshot) {
var serializer = store.serializerFor(type.modelName),
payload = {};
preparePayload(store, type, snapshot) {
let serializer = store.serializerFor(type.modelName);
let payload = {};
serializer.serializeIntoHash(payload, type, snapshot);
return {data: payload};
},
buildIncludeURL: function (store, modelName, id, snapshot, requestType, query) {
var url = this.buildURL(modelName, id, snapshot, requestType, query),
includes = this.getEmbeddedRelations(store, modelName);
buildIncludeURL(store, modelName, id, snapshot, requestType, query) {
let includes = this.getEmbeddedRelations(store, modelName);
let url = this.buildURL(modelName, id, snapshot, requestType, query);
if (includes.length) {
url += '?include=' + includes.join(',');
url += `?include=${includes.join(',')}`;
}
return url;
},
buildQuery: function (store, modelName, options) {
var toInclude = this.getEmbeddedRelations(store, modelName),
query = options || {},
deDupe = {};
buildQuery(store, modelName, options) {
let deDupe = {};
let toInclude = this.getEmbeddedRelations(store, modelName);
let query = options || {};
if (toInclude.length) {
// If this is a find by id, build a query object and attach the includes
@ -93,7 +94,7 @@ export default BaseAdapter.extend({
query = {};
query.id = options;
query.include = toInclude.join(',');
} else if (typeof options === 'object' || Ember.isNone(options)) {
} else if (typeof options === 'object' || isNone(options)) {
// If this is a find all (no existing query object) build one and attach
// the includes.
// If this is a find with an existing query object then merge the includes
@ -101,7 +102,7 @@ export default BaseAdapter.extend({
query = query || {};
toInclude = toInclude.concat(query.include ? query.include.split(',') : []);
toInclude.forEach(function (include) {
toInclude.forEach((include) => {
deDupe[include] = true;
});
@ -112,9 +113,9 @@ export default BaseAdapter.extend({
return query;
},
getEmbeddedRelations: function (store, modelName) {
var model = store.modelFor(modelName),
ret = [];
getEmbeddedRelations(store, modelName) {
let model = store.modelFor(modelName);
let ret = [];
// Iterate through the model's relationships and build a list
// of those that need to be pulled in via "include" from the API

View File

@ -1,9 +1,9 @@
import ApplicationAdapter from 'ghost/adapters/application';
export default ApplicationAdapter.extend({
updateRecord: function (store, type, record) {
var data = {},
serializer = store.serializerFor(type.modelName);
updateRecord(store, type, record) {
let data = {};
let serializer = store.serializerFor(type.modelName);
// remove the fake id that we added onto the model.
delete record.id;
@ -14,6 +14,6 @@ export default ApplicationAdapter.extend({
// use the ApplicationAdapter's buildURL method but do not
// pass in an id.
return this.ajax(this.buildURL(type.modelName), 'PUT', {data: data});
return this.ajax(this.buildURL(type.modelName), 'PUT', {data});
}
});

View File

@ -2,14 +2,14 @@ import ApplicationAdapter from 'ghost/adapters/application';
import SlugUrl from 'ghost/mixins/slug-url';
export default ApplicationAdapter.extend(SlugUrl, {
find: function (store, type, id) {
return this.findQuery(store, type, {id: id, status: 'all'});
find(store, type, id) {
return this.findQuery(store, type, {id, status: 'all'});
},
// TODO: This is needed because the API currently expects you to know the
// status of the record before retrieving by ID. Quick fix is to always
// include status=all in the query
findRecord: function (store, type, id, snapshot) {
findRecord(store, type, id, snapshot) {
let url = this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord');
url += '&status=all';
@ -17,7 +17,7 @@ export default ApplicationAdapter.extend(SlugUrl, {
return this.ajax(url, 'GET');
},
findAll: function (store, type, id) {
return this.query(store, type, {id: id, status: 'all'});
findAll(store, type, id) {
return this.query(store, type, {id, status: 'all'});
}
});

View File

@ -5,12 +5,14 @@ import 'ghost/utils/link-component';
import 'ghost/utils/text-field';
import config from './config/environment';
const {Application} = Ember;
Ember.MODEL_FACTORY_INJECTIONS = true;
var App = Ember.Application.extend({
let App = Application.extend({
Resolver,
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver: Resolver
podModulePrefix: config.podModulePrefix
});
loadInitializers(App, config.modulePrefix);

View File

@ -1,12 +1,11 @@
import ghostPaths from 'ghost/utils/ghost-paths';
var UploadUi,
Ghost = ghostPaths();
let Ghost = ghostPaths();
UploadUi = function ($dropzone, settings) {
var $url = '<div class="js-url"><input class="url js-upload-url gh-input" type="url" placeholder="http://"/></div>',
$cancel = '<a class="image-cancel icon-trash js-cancel" title="Delete"><span class="hidden">Delete</span></a>',
$progress = $('<div />', {
let UploadUi = function ($dropzone, settings) {
let $url = '<div class="js-url"><input class="url js-upload-url gh-input" type="url" placeholder="http://"/></div>';
let $cancel = '<a class="image-cancel icon-trash js-cancel" title="Delete"><span class="hidden">Delete</span></a>';
let $progress = $('<div />', {
class: 'js-upload-progress progress progress-success active',
role: 'progressbar',
'aria-valuemin': '0',
@ -17,38 +16,36 @@ UploadUi = function ($dropzone, settings) {
}));
$.extend(this, {
complete: function (result) {
var self = this;
complete(result) {
function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({width: width, height: height}).css({display: 'block'});
$dropzone.find('img.js-upload-target').attr({width, height}).css({display: 'block'});
$dropzone.find('.fileupload-loading').remove();
$dropzone.css({height: 'auto'});
$dropzone.delay(250).animate({opacity: 100}, 1000, function () {
$dropzone.delay(250).animate({opacity: 100}, 1000, () => {
$('.js-button-accept').prop('disabled', false);
self.init();
this.init();
});
}
function animateDropzone($img) {
$dropzone.animate({opacity: 0}, 250, function () {
$dropzone.animate({opacity: 0}, 250, () => {
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
$dropzone.css({minHeight: 0});
self.removeExtras();
$dropzone.animate({height: $img.height()}, 250, function () {
this.removeExtras();
$dropzone.animate({height: $img.height()}, 250, () => {
showImage($img.width(), $img.height());
});
});
}
function preLoadImage() {
var $img = $dropzone.find('img.js-upload-target')
let $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.subdir + '/ghost/img/loadingcat.gif" />');
$progress.animate({opacity: 0}, 250, () => {
$dropzone.find('span.media').after(`<img class="fileupload-loading" src="${Ghost.subdir}/ghost/img/loadingcat.gif" />`);
});
$img.one('load', function () {
$img.one('load', () => {
$dropzone.trigger('uploadsuccess', [result]);
animateDropzone($img);
}).attr('src', result);
@ -56,12 +53,10 @@ UploadUi = function ($dropzone, settings) {
preLoadImage();
},
bindFileUpload: function () {
var self = this;
bindFileUpload() {
$dropzone.find('.js-fileupload').fileupload().fileupload('option', {
url: Ghost.apiRoot + '/uploads/',
add: function (e, data) {
url: `${Ghost.apiRoot}/uploads/`,
add(e, data) {
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', true);
$dropzone.find('.js-fileupload').removeClass('right');
@ -69,7 +64,7 @@ UploadUi = function ($dropzone, settings) {
$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 () {
.animate({opacity: 0}, 250, () => {
$dropzone.find('div.description').hide().css({opacity: 100});
if (settings.progressbar) {
$dropzone.find('div.js-fail').after($progress);
@ -79,15 +74,15 @@ UploadUi = function ($dropzone, settings) {
});
},
dropZone: settings.fileStorage ? $dropzone : null,
progressall: function (e, data) {
progressall(e, data) {
/*jshint unused:false*/
var progress = parseInt(data.loaded / data.total * 100, 10);
let progress = parseInt(data.loaded / data.total * 100, 10);
if (settings.progressbar) {
$dropzone.trigger('uploadprogress', [progress, data]);
$progress.find('.js-upload-progress-bar').css('width', progress + '%');
$progress.find('.js-upload-progress-bar').css('width', `${progress}%`);
}
},
fail: function (e, data) {
fail(e, data) {
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', false);
$dropzone.trigger('uploadfailure', [data.result]);
@ -100,21 +95,21 @@ UploadUi = function ($dropzone, settings) {
$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.find('button.js-fail').on('click', () => {
$dropzone.css({minHeight: 0});
$dropzone.find('div.description').show();
self.removeExtras();
self.init();
this.removeExtras();
this.init();
});
},
done: function (e, data) {
done(e, data) {
/*jshint unused:false*/
self.complete(data.result);
this.complete(data.result);
}
});
},
buildExtras: function () {
buildExtras() {
if (!$dropzone.find('span.media')[0]) {
$dropzone.prepend('<span class="media"><span class="hidden">Image Upload</span></span>');
}
@ -135,13 +130,11 @@ UploadUi = function ($dropzone, settings) {
// }
},
removeExtras: function () {
removeExtras() {
$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;
initWithDropzone() {
// This is the start point if no image exists
$dropzone.find('img.js-upload-target').css({display: 'none'});
$dropzone.find('div.description').show();
@ -150,24 +143,23 @@ UploadUi = function ($dropzone, settings) {
this.buildExtras();
this.bindFileUpload();
if (!settings.fileStorage) {
self.initUrl();
this.initUrl();
return;
}
$dropzone.find('a.image-url').on('click', function () {
self.initUrl();
$dropzone.find('a.image-url').on('click', () => {
this.initUrl();
});
},
initUrl: function () {
var self = this, val;
initUrl() {
this.removeExtras();
$dropzone.addClass('image-uploader-url').removeClass('pre-image-uploader');
$dropzone.find('.js-fileupload').addClass('right');
$dropzone.find('.js-cancel').on('click', function () {
$dropzone.find('.js-cancel').on('click', () => {
$dropzone.find('.js-url').remove();
$dropzone.find('.js-fileupload').removeClass('right');
$dropzone.trigger('imagecleared');
self.removeExtras();
self.initWithDropzone();
this.removeExtras();
this.initWithDropzone();
});
$dropzone.find('div.description').before($url);
@ -177,16 +169,17 @@ UploadUi = function ($dropzone, settings) {
$dropzone.find('div.description').hide();
}
$dropzone.find('.js-button-accept').on('click', function () {
val = $dropzone.find('.js-upload-url').val();
$dropzone.find('.js-button-accept').on('click', () => {
let 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();
this.initWithDropzone();
} else {
self.complete(val);
this.complete(val);
}
});
@ -195,37 +188,35 @@ UploadUi = function ($dropzone, settings) {
$dropzone.append('<a class="image-upload icon-photos" title="Add image"><span class="hidden">Upload</span></a>');
}
$dropzone.find('a.image-upload').on('click', function () {
$dropzone.find('a.image-upload').on('click', () => {
$dropzone.find('.js-url').remove();
$dropzone.find('.js-fileupload').removeClass('right');
self.initWithDropzone();
this.initWithDropzone();
});
},
initWithImage: function () {
var self = this;
initWithImage() {
// This is the start point if an image already exists
this.removeExtras();
$dropzone.removeClass('image-uploader image-uploader-url').addClass('pre-image-uploader');
$dropzone.find('div.description').hide();
$dropzone.find('img.js-upload-target').show();
$dropzone.append($cancel);
$dropzone.find('.js-cancel').on('click', function () {
$dropzone.find('.js-cancel').on('click', () => {
$dropzone.find('img.js-upload-target').attr({src: ''});
$dropzone.find('div.description').show();
$dropzone.trigger('imagecleared');
$dropzone.delay(250).animate({opacity: 100}, 1000, function () {
self.init();
$dropzone.delay(250).animate({opacity: 100}, 1000, () => {
this.init();
});
$dropzone.trigger('uploadsuccess', 'http://');
self.initWithDropzone();
this.initWithDropzone();
});
},
init: function () {
var imageTarget = $dropzone.find('img.js-upload-target');
init() {
let imageTarget = $dropzone.find('img.js-upload-target');
// First check if field image is defined by checking for js-upload-target class
if (!imageTarget[0]) {
// This ensures there is an image we can hook into to display uploaded image
@ -239,7 +230,7 @@ UploadUi = function ($dropzone, settings) {
}
},
reset: function () {
reset() {
$dropzone.find('.js-url').remove();
$dropzone.find('.js-fileupload').removeClass('right');
this.removeExtras();
@ -249,17 +240,15 @@ UploadUi = function ($dropzone, settings) {
};
export default function (options) {
var settings = $.extend({
let settings = $.extend({
progressbar: true,
editor: false,
fileStorage: true
}, options);
return this.each(function () {
var $dropzone = $(this),
ui;
ui = new UploadUi($dropzone, settings);
let $dropzone = $(this);
let ui = new UploadUi($dropzone, settings);
$(this).attr('data-uploaderui', true);
this.uploaderUi = ui;
ui.init();

View File

@ -1,21 +1,25 @@
import Ember from 'ember';
import Authenticator from 'ember-simple-auth/authenticators/oauth2-password-grant';
const {computed, inject} = Ember;
export default Authenticator.extend({
config: Ember.inject.service(),
ghostPaths: Ember.inject.service('ghost-paths'),
config: inject.service(),
ghostPaths: inject.service('ghost-paths'),
serverTokenEndpoint: Ember.computed('ghostPaths.apiRoot', function () {
return this.get('ghostPaths.apiRoot') + '/authentication/token';
serverTokenEndpoint: computed('ghostPaths.apiRoot', function () {
return `${this.get('ghostPaths.apiRoot')}/authentication/token`;
}),
serverTokenRevocationEndpoint: Ember.computed('ghostPaths.apiRoot', function () {
return this.get('ghostPaths.apiRoot') + '/authentication/revoke';
serverTokenRevocationEndpoint: computed('ghostPaths.apiRoot', function () {
return `${this.get('ghostPaths.apiRoot')}/authentication/revoke`;
}),
makeRequest: function (url, data) {
makeRequest(url, data) {
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
data.client_id = this.get('config.clientId');
data.client_secret = this.get('config.clientSecret');
/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
return this._super(url, data);
}
});

View File

@ -1,18 +1,20 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, on, run} = Ember;
export default Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: false,
linkClasses: null,
unfocusLink: Ember.on('click', function () {
unfocusLink: on('click', function () {
this.$('a').blur();
}),
actions: {
setActive: function (value) {
Ember.run.schedule('afterRender', this, function () {
setActive(value) {
run.schedule('afterRender', this, function () {
this.set('active', value);
});
}

View File

@ -1,16 +1,18 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
export default Component.extend({
tagName: 'article',
classNames: ['gh-alert'],
classNameBindings: ['typeClass'],
notifications: Ember.inject.service(),
notifications: inject.service(),
typeClass: Ember.computed('message.type', function () {
var classes = '',
type = this.get('message.type'),
typeMapping;
typeClass: computed('message.type', function () {
let type = this.get('message.type');
let classes = '';
let typeMapping;
typeMapping = {
success: 'green',
@ -20,14 +22,14 @@ export default Ember.Component.extend({
};
if (typeMapping[type] !== undefined) {
classes += 'gh-alert-' + typeMapping[type];
classes += `gh-alert-${typeMapping[type]}`;
}
return classes;
}),
actions: {
closeNotification: function () {
closeNotification() {
this.get('notifications').closeNotification(this.get('message'));
}
}

View File

@ -1,14 +1,17 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject, observer} = Ember;
const {alias} = computed;
export default Component.extend({
tagName: 'aside',
classNames: 'gh-alerts',
notifications: Ember.inject.service(),
notifications: inject.service(),
messages: Ember.computed.alias('notifications.alerts'),
messages: alias('notifications.alerts'),
messageCountObserver: Ember.observer('messages.[]', function () {
messageCountObserver: observer('messages.[]', function () {
this.sendAction('notify', this.get('messages').length);
})
});

View File

@ -1,12 +1,14 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, observer} = Ember;
export default Component.extend({
classNames: ['gh-app'],
showSettingsMenu: false,
toggleSettingsMenuBodyClass: Ember.observer('showSettingsMenu', function () {
var showSettingsMenu = this.get('showSettingsMenu');
toggleSettingsMenuBodyClass: observer('showSettingsMenu', function () {
let showSettingsMenu = this.get('showSettingsMenu');
Ember.$('body').toggleClass('settings-menu-expanded', showSettingsMenu);
})

View File

@ -1,6 +1,9 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, inject} = Ember;
export default Component.extend({
tagName: '',
config: Ember.inject.service()
config: inject.service()
});

View File

@ -1,14 +1,13 @@
/* global CodeMirror */
import Ember from 'ember';
export default Ember.Component.extend({
const {Component} = Ember;
// DOM stuff
export default Component.extend({
classNameBindings: ['isFocused:focused'],
isFocused: false,
value: '', // make sure a value exists
_editor: null, // reference to CodeMirror editor
isFocused: false,
// options for the editor
lineNumbers: true,
@ -16,9 +15,11 @@ export default Ember.Component.extend({
mode: 'htmlmixed',
theme: 'xq-light',
didInsertElement: function () {
var options = this.getProperties('lineNumbers', 'indentUnit', 'mode', 'theme'),
editor = new CodeMirror(this.get('element'), options);
_editor: null, // reference to CodeMirror editor
didInsertElement() {
let options = this.getProperties('lineNumbers', 'indentUnit', 'mode', 'theme');
let editor = new CodeMirror(this.get('element'), options);
editor.getDoc().setValue(this.get('value'));
@ -34,10 +35,9 @@ export default Ember.Component.extend({
this._editor = editor;
},
willDestroyElement: function () {
var editor = this._editor.getWrapperElement();
willDestroyElement() {
let editor = this._editor.getWrapperElement();
editor.parentNode.removeChild(editor);
this._editor = null;
}
});

View File

@ -12,17 +12,19 @@ Example:
import Ember from 'ember';
export default Ember.Component.extend({
const {Component} = Ember;
export default Component.extend({
classNames: ['content-cover'],
onClick: null,
onMouseEnter: null,
click: function () {
click() {
this.sendAction('onClick');
},
mouseEnter: function () {
mouseEnter() {
this.sendAction('onMouseEnter');
}
});

View File

@ -1,21 +1,23 @@
import Ember from 'ember';
import setScrollClassName from 'ghost/utils/set-scroll-classname';
export default Ember.Component.extend({
const {Component, run} = Ember;
export default Component.extend({
classNames: ['content-preview-content'],
content: null,
didInsertElement: function () {
var el = this.$();
didInsertElement() {
let el = this.$();
el.on('scroll', Ember.run.bind(el, setScrollClassName, {
el.on('scroll', run.bind(el, setScrollClassName, {
target: el.closest('.content-preview'),
offset: 10
}));
},
didReceiveAttrs: function (options) {
didReceiveAttrs(options) {
// adjust when didReceiveAttrs gets both newAttrs and oldAttrs
if (options.newAttrs.content && this.get('content') !== options.newAttrs.content.value) {
let el = this.$();
@ -26,8 +28,8 @@ export default Ember.Component.extend({
}
},
willDestroyElement: function () {
var el = this.$();
willDestroyElement() {
let el = this.$();
el.off('scroll');
}

View File

@ -1,9 +1,11 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
export default Component.extend({
tagName: 'section',
classNames: ['gh-view', 'content-view-container'],
mediaQueries: Ember.inject.service(),
previewIsHidden: Ember.computed.reads('mediaQueries.maxWidth900')
mediaQueries: inject.service(),
previewIsHidden: computed.reads('mediaQueries.maxWidth900')
});

View File

@ -1,7 +1,9 @@
import Ember from 'ember';
import DropdownMixin from 'ghost/mixins/dropdown-mixin';
export default Ember.Component.extend(DropdownMixin, {
const {Component, inject} = Ember;
export default Component.extend(DropdownMixin, {
tagName: 'button',
attributeBindings: 'role',
role: 'button',
@ -9,10 +11,10 @@ export default Ember.Component.extend(DropdownMixin, {
// matches with the dropdown this button toggles
dropdownName: null,
dropdown: Ember.inject.service(),
dropdown: inject.service(),
// Notify dropdown service this dropdown should be toggled
click: function (event) {
click(event) {
this._super(event);
this.get('dropdown').toggleDropdown(this.get('dropdownName'), this);
}

View File

@ -1,7 +1,9 @@
import Ember from 'ember';
import DropdownMixin from 'ghost/mixins/dropdown-mixin';
export default Ember.Component.extend(DropdownMixin, {
const {Component, computed, inject} = Ember;
export default Component.extend(DropdownMixin, {
classNames: 'dropdown',
classNameBindings: ['fadeIn:fade-in-scale:fade-out', 'isOpen:open:closed'],
@ -15,29 +17,28 @@ export default Ember.Component.extend(DropdownMixin, {
isOpen: false,
// Managed the toggle between the fade-in and fade-out classes
fadeIn: Ember.computed('isOpen', 'closing', function () {
fadeIn: computed('isOpen', 'closing', function () {
return this.get('isOpen') && !this.get('closing');
}),
dropdown: Ember.inject.service(),
dropdown: inject.service(),
open: function () {
open() {
this.set('isOpen', true);
this.set('closing', false);
this.set('button.isOpen', true);
},
close: function () {
var self = this;
close() {
this.set('closing', true);
if (this.get('button')) {
this.set('button.isOpen', false);
}
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => {
if (event.originalEvent.animationName === 'fade-out') {
Ember.run(self, function () {
Ember.run(this, function () {
if (this.get('closing')) {
this.set('isOpen', false);
this.set('closing', false);
@ -48,12 +49,12 @@ export default Ember.Component.extend(DropdownMixin, {
},
// Called by the dropdown service when any dropdown button is clicked.
toggle: function (options) {
var isClosing = this.get('closing'),
isOpen = this.get('isOpen'),
name = this.get('name'),
button = this.get('button'),
targetDropdownName = options.target;
toggle(options) {
let isClosing = this.get('closing');
let isOpen = this.get('isOpen');
let name = this.get('name');
let targetDropdownName = options.target;
let button = this.get('button');
if (name === targetDropdownName && (!isOpen || isClosing)) {
if (!button) {
@ -66,7 +67,7 @@ export default Ember.Component.extend(DropdownMixin, {
}
},
click: function (event) {
click(event) {
this._super(event);
if (this.get('closeOnClick')) {
@ -74,19 +75,19 @@ export default Ember.Component.extend(DropdownMixin, {
}
},
didInsertElement: function () {
this._super();
didInsertElement() {
let dropdownService = this.get('dropdown');
var dropdownService = this.get('dropdown');
this._super(...arguments);
dropdownService.on('close', this, this.close);
dropdownService.on('toggle', this, this.toggle);
},
willDestroyElement: function () {
this._super();
willDestroyElement() {
let dropdownService = this.get('dropdown');
var dropdownService = this.get('dropdown');
this._super(...arguments);
dropdownService.off('close', this, this.close);
dropdownService.off('toggle', this, this.toggle);

View File

@ -3,20 +3,22 @@ import EditorAPI from 'ghost/mixins/ed-editor-api';
import EditorShortcuts from 'ghost/mixins/ed-editor-shortcuts';
import EditorScroll from 'ghost/mixins/ed-editor-scroll';
export default Ember.TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
const {TextArea, run} = Ember;
export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
focus: false,
/**
* Tell the controller about focusIn events, will trigger an autosave on a new document
*/
focusIn: function () {
focusIn() {
this.sendAction('onFocusIn');
},
/**
* Sets the focus of the textarea if needed
*/
setFocus: function () {
setFocus() {
if (this.get('focus')) {
this.$().val(this.$().val()).focus();
}
@ -25,17 +27,17 @@ export default Ember.TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
/**
* Sets up properties at render time
*/
didInsertElement: function () {
this._super();
didInsertElement() {
this._super(...arguments);
this.setFocus();
this.sendAction('setEditor', this);
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
afterRenderEvent: function () {
afterRenderEvent() {
if (this.get('focus') && this.get('focusCursorAtEnd')) {
this.setSelection('end');
}
@ -44,16 +46,16 @@ export default Ember.TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
/**
* Disable editing in the textarea (used while an upload is in progress)
*/
disable: function () {
var textarea = this.get('element');
disable() {
let textarea = this.get('element');
textarea.setAttribute('readonly', 'readonly');
},
/**
* Reenable editing in the textarea
*/
enable: function () {
var textarea = this.get('element');
enable() {
let textarea = this.get('element');
textarea.removeAttribute('readonly');
}
});

View File

@ -1,39 +1,43 @@
import Ember from 'ember';
import uploader from 'ghost/assets/lib/uploader';
export default Ember.Component.extend({
config: Ember.inject.service(),
const {Component, inject, run} = Ember;
export default Component.extend({
config: inject.service(),
_scrollWrapper: null,
didInsertElement: function () {
didInsertElement() {
this._scrollWrapper = this.$().closest('.entry-preview-content');
this.adjustScrollPosition(this.get('scrollPosition'));
Ember.run.scheduleOnce('afterRender', this, this.dropzoneHandler);
run.scheduleOnce('afterRender', this, this.dropzoneHandler);
},
didReceiveAttrs: function (attrs) {
if (!attrs.oldAttrs) { return; }
didReceiveAttrs(attrs) {
if (!attrs.oldAttrs) {
return;
}
if (attrs.newAttrs.scrollPosition && attrs.newAttrs.scrollPosition.value !== attrs.oldAttrs.scrollPosition.value) {
this.adjustScrollPosition(attrs.newAttrs.scrollPosition.value);
}
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
Ember.run.scheduleOnce('afterRender', this, this.dropzoneHandler);
run.scheduleOnce('afterRender', this, this.dropzoneHandler);
}
},
adjustScrollPosition: function (scrollPosition) {
var scrollWrapper = this._scrollWrapper;
adjustScrollPosition(scrollPosition) {
let scrollWrapper = this._scrollWrapper;
if (scrollWrapper) {
scrollWrapper.scrollTop(scrollPosition);
}
},
dropzoneHandler: function () {
var dropzones = $('.js-drop-zone[data-uploaderui!="true"]');
dropzoneHandler() {
let dropzones = $('.js-drop-zone[data-uploaderui!="true"]');
if (dropzones.length) {
uploader.call(dropzones, {
@ -41,10 +45,10 @@ export default Ember.Component.extend({
fileStorage: this.get('config.fileStorage')
});
dropzones.on('uploadstart', Ember.run.bind(this, 'sendAction', 'uploadStarted'));
dropzones.on('uploadfailure', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadSuccess'));
dropzones.on('uploadstart', run.bind(this, 'sendAction', 'uploadStarted'));
dropzones.on('uploadfailure', run.bind(this, 'sendAction', 'uploadFinished'));
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadFinished'));
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadSuccess'));
// Set the current height so we can listen
this.sendAction('updateHeight', this.$().height());

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed} = Ember;
export default Component.extend({
tagName: 'section',
classNames: ['splitbtn', 'js-publish-splitbutton'],
classNameBindings: ['isNew:unsaved'],
@ -12,36 +14,36 @@ export default Ember.Component.extend({
submitting: false,
// Tracks whether we're going to change the state of the post on save
isDangerous: Ember.computed('isPublished', 'willPublish', function () {
isDangerous: computed('isPublished', 'willPublish', function () {
return this.get('isPublished') !== this.get('willPublish');
}),
publishText: Ember.computed('isPublished', 'postOrPage', function () {
return this.get('isPublished') ? 'Update ' + this.get('postOrPage') : 'Publish Now';
publishText: computed('isPublished', 'postOrPage', function () {
return this.get('isPublished') ? `Update ${this.get('postOrPage')}` : 'Publish Now';
}),
draftText: Ember.computed('isPublished', function () {
draftText: computed('isPublished', function () {
return this.get('isPublished') ? 'Unpublish' : 'Save Draft';
}),
deleteText: Ember.computed('postOrPage', function () {
return 'Delete ' + this.get('postOrPage');
deleteText: computed('postOrPage', function () {
return `Delete ${this.get('postOrPage')}`;
}),
saveText: Ember.computed('willPublish', 'publishText', 'draftText', function () {
saveText: computed('willPublish', 'publishText', 'draftText', function () {
return this.get('willPublish') ? this.get('publishText') : this.get('draftText');
}),
actions: {
save: function () {
save() {
this.sendAction('save');
},
setSaveType: function (saveType) {
setSaveType(saveType) {
this.sendAction('setSaveType', saveType);
},
delete: function () {
delete() {
this.sendAction('delete');
}
}

View File

@ -1,54 +1,37 @@
import Ember from 'ember';
import setScrollClassName from 'ghost/utils/set-scroll-classname';
export default Ember.Component.extend({
const {Component, computed, run} = Ember;
const {equal} = computed;
export default Component.extend({
tagName: 'section',
classNames: ['gh-view'],
scheduleAfterRender: function () {
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
didInsertElement: function () {
this.scheduleAfterRender();
},
afterRenderEvent: function () {
var $previewViewPort = this.$('.js-entry-preview-content');
// cache these elements for use in other methods
this.set('$previewViewPort', $previewViewPort);
this.set('$previewContent', this.$('.js-rendered-markdown'));
$previewViewPort.on('scroll', Ember.run.bind($previewViewPort, setScrollClassName, {
target: this.$('.js-entry-preview'),
offset: 10
}));
},
willDestroyElement: function () {
// removes scroll handlers from the view
this.get('$previewViewPort').off('scroll');
},
// updated when gh-ed-editor component scrolls
editorScrollInfo: null,
// updated when markdown is rendered
height: null,
activeTab: 'markdown',
markdownActive: equal('activeTab', 'markdown'),
previewActive: equal('activeTab', 'preview'),
// HTML Preview listens to scrollPosition and updates its scrollTop value
// This property receives scrollInfo from the textEditor, and height from the preview pane, and will update the
// scrollPosition value such that when either scrolling or typing-at-the-end of the text editor the preview pane
// stays in sync
scrollPosition: Ember.computed('editorScrollInfo', 'height', function () {
if (!this.get('editorScrollInfo') || !this.get('$previewContent') || !this.get('$previewViewPort')) {
scrollPosition: computed('editorScrollInfo', 'height', function () {
let scrollInfo = this.get('editorScrollInfo');
let $previewContent = this.get('$previewContent');
let $previewViewPort = this.get('$previewViewPort');
if (!scrollInfo || !$previewContent || !$previewViewPort) {
return 0;
}
var scrollInfo = this.get('editorScrollInfo'),
previewHeight = this.get('$previewContent').height() - this.get('$previewViewPort').height(),
previewPosition,
ratio;
let previewHeight = $previewContent.height() - $previewViewPort.height();
let previewPosition, ratio;
ratio = previewHeight / scrollInfo.diff;
previewPosition = scrollInfo.top * ratio;
@ -56,12 +39,34 @@ export default Ember.Component.extend({
return previewPosition;
}),
activeTab: 'markdown',
markdownActive: Ember.computed.equal('activeTab', 'markdown'),
previewActive: Ember.computed.equal('activeTab', 'preview'),
scheduleAfterRender() {
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
didInsertElement() {
this.scheduleAfterRender();
},
afterRenderEvent() {
let $previewViewPort = this.$('.js-entry-preview-content');
// cache these elements for use in other methods
this.set('$previewViewPort', $previewViewPort);
this.set('$previewContent', this.$('.js-rendered-markdown'));
$previewViewPort.on('scroll', run.bind($previewViewPort, setScrollClassName, {
target: this.$('.js-entry-preview'),
offset: 10
}));
},
willDestroyElement() {
// removes scroll handlers from the view
this.get('$previewViewPort').off('scroll');
},
actions: {
selectTab: function (tab) {
selectTab(tab) {
this.set('activeTab', tab);
}
}

View File

@ -1,5 +1,7 @@
import Ember from 'ember';
const {Component, computed, isEmpty} = Ember;
/**
* Renders one random error message when passed a DS.Errors object
* and a property name. The message will be one of the ones associated with
@ -8,23 +10,23 @@ import Ember from 'ember';
* @param {DS.Errors} errors The DS.Errors object
* @param {string} property The property name
*/
export default Ember.Component.extend({
export default Component.extend({
tagName: 'p',
classNames: ['response'],
errors: null,
property: '',
isVisible: Ember.computed.notEmpty('errors'),
isVisible: computed.notEmpty('errors'),
message: Ember.computed('errors.[]', 'property', function () {
var property = this.get('property'),
errors = this.get('errors'),
messages = [],
index;
message: computed('errors.[]', 'property', function () {
let property = this.get('property');
let errors = this.get('errors');
let messages = [];
let index;
if (!Ember.isEmpty(errors) && errors.get(property)) {
errors.get(property).forEach(function (error) {
if (!isEmpty(errors) && errors.get(property)) {
errors.get(property).forEach((error) => {
messages.push(error);
});
index = Math.floor(Math.random() * messages.length);

View File

@ -1,10 +1,11 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component} = Ember;
export default Component.extend({
_file: null,
uploadButtonText: 'Text',
uploadButtonDisabled: true,
onUpload: null,
@ -12,14 +13,14 @@ export default Ember.Component.extend({
shouldResetForm: true,
change: function (event) {
change(event) {
this.set('uploadButtonDisabled', false);
this.sendAction('onAdd');
this._file = event.target.files[0];
},
actions: {
upload: function () {
upload() {
if (!this.get('uploadButtonDisabled') && this._file) {
this.sendAction('onUpload', this._file);
}
@ -29,7 +30,7 @@ export default Ember.Component.extend({
// Reset form
if (this.get('shouldResetForm')) {
this.$().closest('form').get(0).reset();
this.$().closest('form')[0].reset();
}
}
}

View File

@ -2,21 +2,22 @@ import Ember from 'ember';
import InfiniteScrollMixin from 'ghost/mixins/infinite-scroll';
import setScrollClassName from 'ghost/utils/set-scroll-classname';
export default Ember.Component.extend(InfiniteScrollMixin, {
didRender: function () {
this._super();
const {Component, run} = Ember;
var el = this.$();
export default Component.extend(InfiniteScrollMixin, {
didRender() {
let el = this.$();
el.on('scroll', Ember.run.bind(el, setScrollClassName, {
this._super(...arguments);
el.on('scroll', run.bind(el, setScrollClassName, {
target: el.closest('.content-list'),
offset: 10
}));
},
willDestroyElement: function () {
this._super();
willDestroyElement() {
this._super(...arguments);
this.$().off('scroll');
}
});

View File

@ -1,4 +1,6 @@
import Ember from 'ember';
import InfiniteScrollMixin from 'ghost/mixins/infinite-scroll';
export default Ember.Component.extend(InfiniteScrollMixin);
const {Component} = Ember;
export default Component.extend(InfiniteScrollMixin);

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
import TextInputMixin from 'ghost/mixins/text-input';
export default Ember.TextField.extend(TextInputMixin, {
const {TextField} = Ember;
export default TextField.extend(TextInputMixin, {
classNames: 'gh-input'
});

View File

@ -1,11 +1,13 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component} = Ember;
export default Component.extend({
tagName: 'main',
classNames: ['gh-main'],
ariaRole: 'main',
mouseEnter: function () {
mouseEnter() {
this.sendAction('onMouseEnter');
}
});

View File

@ -1,24 +1,25 @@
/*
This cute little component has two jobs.
On desktop, it toggles autoNav behaviour. It tracks
that state via the maximise property, and uses the
state to render the appropriate icon.
On mobile, it renders a closing icon, and clicking it
closes the mobile menu
*/
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
/*
This cute little component has two jobs.
On desktop, it toggles autoNav behaviour. It tracks
that state via the maximise property, and uses the
state to render the appropriate icon.
On mobile, it renders a closing icon, and clicking it
closes the mobile menu
*/
export default Component.extend({
classNames: ['gh-menu-toggle'],
mediaQueries: Ember.inject.service(),
isMobile: Ember.computed.reads('mediaQueries.isMobile'),
mediaQueries: inject.service(),
isMobile: computed.reads('mediaQueries.isMobile'),
maximise: false,
iconClass: Ember.computed('maximise', 'isMobile', function () {
iconClass: computed('maximise', 'isMobile', function () {
if (this.get('maximise') && !this.get('isMobile')) {
return 'icon-maximise';
} else {
@ -26,7 +27,7 @@ export default Ember.Component.extend({
}
}),
click: function () {
click() {
if (this.get('isMobile')) {
this.sendAction('mobileAction');
} else {

View File

@ -1,59 +1,66 @@
import Ember from 'ember';
export default Ember.Component.extend({
didInsertElement: function () {
this.$('.js-modal-container, .js-modal-background').addClass('fade-in open');
this.$('.js-modal').addClass('open');
},
const {Component, computed} = Ember;
close: function () {
var self = this;
function K() {
return this;
}
this.$('.js-modal, .js-modal-background').removeClass('fade-in').addClass('fade-out');
// The background should always be the last thing to fade out, so check on that instead of the content
this.$('.js-modal-background').on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
if (event.originalEvent.animationName === 'fade-out') {
self.$('.js-modal, .js-modal-background').removeClass('open');
}
});
this.sendAction();
},
export default Component.extend({
confirmaccept: 'confirmAccept',
confirmreject: 'confirmReject',
actions: {
closeModal: function () {
this.close();
},
confirm: function (type) {
this.sendAction('confirm' + type);
this.close();
},
noBubble: Ember.K
},
klass: computed('type', 'style', function () {
let classNames = [];
klass: Ember.computed('type', 'style', function () {
var classNames = [];
classNames.push(this.get('type') ? 'modal-' + this.get('type') : 'modal');
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);
this.get('style').split(',').forEach((style) => {
classNames.push(`modal-style-${style}`);
});
}
return classNames.join(' ');
}),
acceptButtonClass: Ember.computed('confirm.accept.buttonClass', function () {
acceptButtonClass: computed('confirm.accept.buttonClass', function () {
return this.get('confirm.accept.buttonClass') ? this.get('confirm.accept.buttonClass') : 'btn btn-green';
}),
rejectButtonClass: Ember.computed('confirm.reject.buttonClass', function () {
rejectButtonClass: computed('confirm.reject.buttonClass', function () {
return this.get('confirm.reject.buttonClass') ? this.get('confirm.reject.buttonClass') : 'btn btn-red';
})
}),
didInsertElement() {
this.$('.js-modal-container, .js-modal-background').addClass('fade-in open');
this.$('.js-modal').addClass('open');
},
close() {
this.$('.js-modal, .js-modal-background').removeClass('fade-in').addClass('fade-out');
// The background should always be the last thing to fade out, so check on that instead of the content
this.$('.js-modal-background').on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => {
if (event.originalEvent.animationName === 'fade-out') {
this.$('.js-modal, .js-modal-background').removeClass('open');
}
});
this.sendAction();
},
actions: {
closeModal() {
this.close();
},
confirm(type) {
this.sendAction(`confirm${type}`);
this.close();
},
noBubble: K
}
});

View File

@ -1,33 +1,35 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, inject} = Ember;
export default Component.extend({
tagName: 'nav',
classNames: ['gh-nav'],
classNameBindings: ['open'],
config: Ember.inject.service(),
session: Ember.inject.service(),
open: false,
mouseEnter: function () {
config: inject.service(),
session: inject.service(),
mouseEnter() {
this.sendAction('onMouseEnter');
},
actions: {
toggleAutoNav: function () {
toggleAutoNav() {
this.sendAction('toggleMaximise');
},
openModal: function (modal) {
openModal(modal) {
this.sendAction('openModal', modal);
},
closeMobileMenu: function () {
closeMobileMenu() {
this.sendAction('closeMobileMenu');
},
openAutoNav: function () {
openAutoNav() {
this.sendAction('openAutoNav');
}
}

View File

@ -1,13 +1,14 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, run} = Ember;
export default Component.extend({
tagName: 'section',
classNames: 'gh-view',
didInsertElement: function () {
var navContainer = this.$('.js-gh-blognav'),
navElements = '.gh-blognav-item:not(.gh-blognav-item:last-child)',
self = this;
didInsertElement() {
let navContainer = this.$('.js-gh-blognav');
let navElements = '.gh-blognav-item:not(.gh-blognav-item:last-child)';
this._super(...arguments);
@ -15,21 +16,21 @@ export default Ember.Component.extend({
handle: '.gh-blognav-grab',
items: navElements,
start: function (event, ui) {
Ember.run(function () {
start(event, ui) {
run(() => {
ui.item.data('start-index', ui.item.index());
});
},
update: function (event, ui) {
Ember.run(function () {
self.sendAction('moveItem', ui.item.data('start-index'), ui.item.index());
update(event, ui) {
run(() => {
this.sendAction('moveItem', ui.item.data('start-index'), ui.item.index());
});
}
});
},
willDestroyElement: function () {
willDestroyElement() {
this.$('.ui-sortable').sortable('destroy');
}
});

View File

@ -1,11 +1,10 @@
import Ember from 'ember';
var joinUrlParts,
isRelative;
const {TextField, computed, run} = Ember;
joinUrlParts = function (url, path) {
let joinUrlParts = function (url, path) {
if (path[0] !== '/' && url.slice(-1) !== '/') {
path = '/' + path;
path = `/${path}`;
} else if (path[0] === '/' && url.slice(-1) === '/') {
path = path.slice(1);
}
@ -13,19 +12,27 @@ joinUrlParts = function (url, path) {
return url + path;
};
isRelative = function (url) {
let isRelative = function (url) {
// "protocol://", "//example.com", "scheme:", "#anchor", & invalid paths
// should all be treated as absolute
return !url.match(/\s/) && !validator.isURL(url) && !url.match(/^(\/\/|#|[a-zA-Z0-9\-]+:)/);
};
export default Ember.TextField.extend({
export default TextField.extend({
classNames: 'gh-input',
classNameBindings: ['fakePlaceholder'],
didReceiveAttrs: function () {
var url = this.get('url'),
baseUrl = this.get('baseUrl');
isBaseUrl: computed('baseUrl', 'value', function () {
return this.get('baseUrl') === this.get('value');
}),
fakePlaceholder: computed('isBaseUrl', 'hasFocus', function () {
return this.get('isBaseUrl') && this.get('last') && !this.get('hasFocus');
}),
didReceiveAttrs() {
let baseUrl = this.get('baseUrl');
let url = this.get('url');
// if we have a relative url, create the absolute url to be displayed in the input
if (isRelative(url)) {
@ -35,28 +42,20 @@ export default Ember.TextField.extend({
this.set('value', url);
},
isBaseUrl: Ember.computed('baseUrl', 'value', function () {
return this.get('baseUrl') === this.get('value');
}),
fakePlaceholder: Ember.computed('isBaseUrl', 'hasFocus', function () {
return this.get('isBaseUrl') && this.get('last') && !this.get('hasFocus');
}),
focusIn: function (event) {
focusIn(event) {
this.set('hasFocus', true);
if (this.get('isBaseUrl')) {
// position the cursor at the end of the input
Ember.run.next(function (el) {
var length = el.value.length;
run.next(function (el) {
let {length} = el.value;
el.setSelectionRange(length, length);
}, event.target);
}
},
keyDown: function (event) {
keyDown(event) {
// delete the "placeholder" value all at once
if (this.get('isBaseUrl') && (event.keyCode === 8 || event.keyCode === 46)) {
this.set('value', '');
@ -70,7 +69,7 @@ export default Ember.TextField.extend({
}
},
keyPress: function (event) {
keyPress(event) {
// enter key
if (event.keyCode === 13) {
event.preventDefault();
@ -80,19 +79,21 @@ export default Ember.TextField.extend({
return true;
},
focusOut: function () {
focusOut() {
this.set('hasFocus', false);
this.notifyUrlChanged();
},
notifyUrlChanged: function () {
this.set('value', this.get('value').trim());
notifyUrlChanged() {
let value = this.get('value').trim();
let urlParts = document.createElement('a');
let baseUrl = this.get('baseUrl');
let baseUrlParts = document.createElement('a');
let url = value;
var url = this.get('value'),
urlParts = document.createElement('a'),
baseUrl = this.get('baseUrl'),
baseUrlParts = document.createElement('a');
// ensure value property is trimmed
this.set('value', value);
// leverage the browser's native URI parsing
urlParts.href = url;
@ -117,7 +118,7 @@ export default Ember.TextField.extend({
url = url.replace(baseUrlParts.host, '');
url = url.replace(baseUrlParts.pathname, '');
if (!url.match(/^\//)) {
url = '/' + url;
url = `/${url}`;
}
}

View File

@ -1,21 +1,23 @@
import Ember from 'ember';
import ValidationStateMixin from 'ghost/mixins/validation-state';
export default Ember.Component.extend(ValidationStateMixin, {
const {Component, computed} = Ember;
export default Component.extend(ValidationStateMixin, {
classNames: 'gh-blognav-item',
classNameBindings: ['errorClass'],
attributeBindings: ['order:data-order'],
order: Ember.computed.readOnly('navItem.order'),
errors: Ember.computed.readOnly('navItem.errors'),
order: computed.readOnly('navItem.order'),
errors: computed.readOnly('navItem.errors'),
errorClass: Ember.computed('hasError', function () {
errorClass: computed('hasError', function () {
if (this.get('hasError')) {
return 'gh-blognav-item--error';
}
}),
keyPress: function (event) {
keyPress(event) {
// enter key
if (event.keyCode === 13) {
event.preventDefault();
@ -26,15 +28,15 @@ export default Ember.Component.extend(ValidationStateMixin, {
},
actions: {
addItem: function () {
addItem() {
this.sendAction('addItem');
},
deleteItem: function (item) {
deleteItem(item) {
this.sendAction('deleteItem', item);
},
updateUrl: function (value) {
updateUrl(value) {
this.sendAction('updateUrl', value, this.get('navItem'));
}
}

View File

@ -1,18 +1,20 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
export default Component.extend({
tagName: 'article',
classNames: ['gh-notification', 'gh-notification-passive'],
classNameBindings: ['typeClass'],
message: null,
notifications: Ember.inject.service(),
notifications: inject.service(),
typeClass: Ember.computed('message.type', function () {
var classes = '',
type = this.get('message.type'),
typeMapping;
typeClass: computed('message.type', function () {
let type = this.get('message.type');
let classes = '';
let typeMapping;
typeMapping = {
success: 'green',
@ -21,28 +23,26 @@ export default Ember.Component.extend({
};
if (typeMapping[type] !== undefined) {
classes += 'gh-notification-' + typeMapping[type];
classes += `gh-notification-${typeMapping[type]}`;
}
return classes;
}),
didInsertElement: function () {
var self = this;
self.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
didInsertElement() {
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => {
if (event.originalEvent.animationName === 'fade-out') {
self.get('notifications').closeNotification(self.get('message'));
this.get('notifications').closeNotification(this.get('message'));
}
});
},
willDestroyElement: function () {
willDestroyElement() {
this.$().off('animationend webkitAnimationEnd oanimationend MSAnimationEnd');
},
actions: {
closeNotification: function () {
closeNotification() {
this.get('notifications').closeNotification(this.get('message'));
}
}

View File

@ -1,10 +1,13 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
const {alias} = computed;
export default Component.extend({
tagName: 'aside',
classNames: 'gh-notifications',
notifications: Ember.inject.service(),
notifications: inject.service(),
messages: Ember.computed.alias('notifications.notifications')
messages: alias('notifications.notifications')
});

View File

@ -1,18 +1,24 @@
import Ember from 'ember';
import DropdownButton from 'ghost/components/gh-dropdown-button';
const {inject} = Ember;
function K() {
return this;
}
export default DropdownButton.extend({
dropdown: Ember.inject.service(),
dropdown: inject.service(),
click: Ember.K,
click: K,
mouseEnter: function (event) {
this._super(event);
mouseEnter() {
this._super(...arguments);
this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
},
mouseLeave: function (event) {
this._super(event);
mouseLeave() {
this._super(...arguments);
this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
}
});

View File

@ -1,7 +1,9 @@
import Ember from 'ember';
import GhostDropdown from 'ghost/components/gh-dropdown';
const {inject} = Ember;
export default GhostDropdown.extend({
classNames: 'ghost-popover',
dropdown: Ember.inject.service()
dropdown: inject.service()
});

View File

@ -1,6 +1,9 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
const {alias, equal} = computed;
export default Component.extend({
tagName: 'li',
classNameBindings: ['active', 'isFeatured:featured', 'isPage:page'],
@ -8,60 +11,56 @@ export default Ember.Component.extend({
active: false,
previewIsHidden: false,
ghostPaths: Ember.inject.service('ghost-paths'),
isFeatured: alias('post.featured'),
isPage: alias('post.page'),
isPublished: equal('post.status', 'published'),
isFeatured: Ember.computed.alias('post.featured'),
ghostPaths: inject.service('ghost-paths'),
isPage: Ember.computed.alias('post.page'),
isPublished: Ember.computed.equal('post.status', 'published'),
authorName: Ember.computed('post.author.name', 'post.author.email', function () {
authorName: computed('post.author.name', 'post.author.email', function () {
return this.get('post.author.name') || this.get('post.author.email');
}),
authorAvatar: Ember.computed('post.author.image', function () {
authorAvatar: computed('post.author.image', function () {
return this.get('post.author.image') || this.get('ghostPaths.url').asset('/shared/img/user-image.png');
}),
authorAvatarBackground: Ember.computed('authorAvatar', function () {
authorAvatarBackground: computed('authorAvatar', function () {
return Ember.String.htmlSafe(`background-image: url(${this.get('authorAvatar')})`);
}),
viewOrEdit: Ember.computed('previewIsHidden', function () {
viewOrEdit: computed('previewIsHidden', function () {
return this.get('previewIsHidden') ? 'editor.edit' : 'posts.post';
}),
click: function () {
click() {
this.sendAction('onClick', this.get('post'));
},
doubleClick: function () {
doubleClick() {
this.sendAction('onDoubleClick', this.get('post'));
},
didInsertElement: function () {
didInsertElement() {
this.addObserver('active', this, this.scrollIntoView);
},
willDestroyElement: function () {
willDestroyElement() {
this.removeObserver('active', this, this.scrollIntoView);
},
scrollIntoView: function () {
scrollIntoView() {
if (!this.get('active')) {
return;
}
var element = this.$(),
offset = element.offset().top,
elementHeight = element.height(),
container = Ember.$('.js-content-scrollbox'),
containerHeight = container.height(),
currentScroll = container.scrollTop(),
isBelowTop,
isAboveBottom,
isOnScreen;
let element = this.$();
let offset = element.offset().top;
let elementHeight = element.height();
let container = $('.js-content-scrollbox');
let containerHeight = container.height();
let currentScroll = container.scrollTop();
let isBelowTop, isAboveBottom, isOnScreen;
isAboveBottom = offset < containerHeight;
isBelowTop = offset > elementHeight;

View File

@ -1,5 +1,8 @@
import Ember from 'ember';
const {Component, computed, inject, run} = Ember;
const {notEmpty} = computed;
/**
* A component to manage a user profile image. By default it just handles picture uploads,
* but if passed a bound 'email' property it will render the user's gravatar image
@ -14,7 +17,7 @@ import Ember from 'ember';
* @property {String} defaultImage String containing the background-image css property of the default user profile image
* @property {String} imageBackground String containing the background-image css property with the gravatar url
*/
export default Ember.Component.extend({
export default Component.extend({
email: '',
size: 90,
debounce: 300,
@ -23,35 +26,35 @@ export default Ember.Component.extend({
hasUploadedImage: false,
fileStorage: true,
ghostPaths: Ember.inject.service('ghost-paths'),
displayGravatar: Ember.computed.notEmpty('validEmail'),
ghostPaths: inject.service('ghost-paths'),
displayGravatar: notEmpty('validEmail'),
init: function () {
init() {
this._super(...arguments);
// Fire this immediately in case we're initialized with a valid email
this.trySetValidEmail();
},
defaultImage: Ember.computed('ghostPaths', function () {
const url = this.get('ghostPaths.url').asset('/shared/img/user-image.png');
defaultImage: computed('ghostPaths', function () {
let url = this.get('ghostPaths.url').asset('/shared/img/user-image.png');
return Ember.String.htmlSafe(`background-image: url(${url})`);
}),
trySetValidEmail: function () {
trySetValidEmail() {
if (!this.get('isDestroyed')) {
const email = this.get('email');
let email = this.get('email');
this.set('validEmail', validator.isEmail(email) ? email : '');
}
},
didReceiveAttrs: function (attrs) {
const timeout = parseInt(attrs.newAttrs.throttle || this.get('debounce'));
Ember.run.debounce(this, 'trySetValidEmail', timeout);
didReceiveAttrs(attrs) {
let timeout = parseInt(attrs.newAttrs.throttle || this.get('debounce'));
run.debounce(this, 'trySetValidEmail', timeout);
},
imageBackground: Ember.computed('validEmail', 'size', function () {
const email = this.get('validEmail'),
size = this.get('size');
imageBackground: computed('validEmail', 'size', function () {
let email = this.get('validEmail');
let size = this.get('size');
let style = '';
if (email) {
@ -61,9 +64,9 @@ export default Ember.Component.extend({
return Ember.String.htmlSafe(style);
}),
didInsertElement: function () {
var size = this.get('size'),
uploadElement = this.$('.js-file-input');
didInsertElement() {
let size = this.get('size');
let uploadElement = this.$('.js-file-input');
// while theoretically the 'add' and 'processalways' functions could be
// added as properties of the hash passed to fileupload(), for some reason
@ -77,26 +80,27 @@ export default Ember.Component.extend({
maxNumberOfFiles: 1,
autoUpload: false
})
.on('fileuploadadd', Ember.run.bind(this, this.queueFile))
.on('fileuploadprocessalways', Ember.run.bind(this, this.triggerPreview));
.on('fileuploadadd', run.bind(this, this.queueFile))
.on('fileuploadprocessalways', run.bind(this, this.triggerPreview));
},
willDestroyElement: function () {
willDestroyElement() {
if (this.$('.js-file-input').data()['blueimp-fileupload']) {
this.$('.js-file-input').fileupload('destroy');
}
},
queueFile: function (e, data) {
const fileName = data.files[0].name;
queueFile(e, data) {
let fileName = data.files[0].name;
if ((/\.(gif|jpe?g|png|svg?z)$/i).test(fileName)) {
this.sendAction('setImage', data);
}
},
triggerPreview: function (e, data) {
const file = data.files[data.index];
triggerPreview(e, data) {
let file = data.files[data.index];
if (file.preview) {
this.set('hasUploadedImage', true);
// necessary jQuery code because file.preview is a raw DOM object

View File

@ -1,8 +1,12 @@
/* global key */
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
/* global key */
export default Ember.Component.extend({
const {Component, RSVP, computed, inject, observer} = Ember;
const {filterBy} = computed;
export default Component.extend({
selection: null,
content: [],
@ -10,48 +14,49 @@ export default Ember.Component.extend({
contentExpiry: 10 * 1000,
contentExpiresAt: false,
posts: Ember.computed.filterBy('content', 'category', 'Posts'),
pages: Ember.computed.filterBy('content', 'category', 'Pages'),
users: Ember.computed.filterBy('content', 'category', 'Users'),
tags: Ember.computed.filterBy('content', 'category', 'Tags'),
posts: filterBy('content', 'category', 'Posts'),
pages: filterBy('content', 'category', 'Pages'),
users: filterBy('content', 'category', 'Users'),
tags: filterBy('content', 'category', 'Tags'),
_store: Ember.inject.service('store'),
_routing: Ember.inject.service('-routing'),
_selectize: Ember.computed(function () {
_store: inject.service('store'),
_routing: inject.service('-routing'),
_selectize: computed(function () {
return this.$('select')[0].selectize;
}),
refreshContent: function () {
var promises = [],
now = new Date(),
contentExpiry = this.get('contentExpiry'),
contentExpiresAt = this.get('contentExpiresAt'),
self = this;
refreshContent() {
let promises = [];
let now = new Date();
let contentExpiry = this.get('contentExpiry');
let contentExpiresAt = this.get('contentExpiresAt');
if (self.get('isLoading') || contentExpiresAt > now) { return; }
if (this.get('isLoading') || contentExpiresAt > now) {
return;
}
self.set('isLoading', true);
this.set('isLoading', true);
promises.pushObject(this._loadPosts());
promises.pushObject(this._loadUsers());
promises.pushObject(this._loadTags());
Ember.RSVP.all(promises).then(function () { }).finally(function () {
self.set('isLoading', false);
self.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
RSVP.all(promises).then(() => { }).finally(() => {
this.set('isLoading', false);
this.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
});
},
_loadPosts: function () {
var store = this.get('_store'),
postsUrl = store.adapterFor('post').urlForQuery({}, 'post') + '/',
postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all', staticPages: 'all'},
content = this.get('content'),
self = this;
_loadPosts() {
let store = this.get('_store');
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
let postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all', staticPages: 'all'};
let content = this.get('content');
return ajax(postsUrl, {data: postsQuery}).then(function (posts) {
content.removeObjects(self.get('posts'));
content.removeObjects(self.get('pages'));
content.pushObjects(posts.posts.map(function (post) {
return ajax(postsUrl, {data: postsQuery}).then((posts) => {
content.removeObjects(this.get('posts'));
content.removeObjects(this.get('pages'));
content.pushObjects(posts.posts.map((post) => {
return {
id: `post.${post.id}`,
title: post.title,
@ -61,16 +66,15 @@ export default Ember.Component.extend({
});
},
_loadUsers: function () {
var store = this.get('_store'),
usersUrl = store.adapterFor('user').urlForQuery({}, 'user') + '/',
usersQuery = {fields: 'name,slug', limit: 'all'},
content = this.get('content'),
self = this;
_loadUsers() {
let store = this.get('_store');
let usersUrl = `${store.adapterFor('user').urlForQuery({}, 'user')}/`;
let usersQuery = {fields: 'name,slug', limit: 'all'};
let content = this.get('content');
return ajax(usersUrl, {data: usersQuery}).then(function (users) {
content.removeObjects(self.get('users'));
content.pushObjects(users.users.map(function (user) {
return ajax(usersUrl, {data: usersQuery}).then((users) => {
content.removeObjects(this.get('users'));
content.pushObjects(users.users.map((user) => {
return {
id: `user.${user.slug}`,
title: user.name,
@ -80,16 +84,15 @@ export default Ember.Component.extend({
});
},
_loadTags: function () {
var store = this.get('_store'),
tagsUrl = store.adapterFor('tag').urlForQuery({}, 'tag') + '/',
tagsQuery = {fields: 'name,slug', limit: 'all'},
content = this.get('content'),
self = this;
_loadTags() {
let store = this.get('_store');
let tagsUrl = `${store.adapterFor('tag').urlForQuery({}, 'tag')}/`;
let tagsQuery = {fields: 'name,slug', limit: 'all'};
let content = this.get('content');
return ajax(tagsUrl, {data: tagsQuery}).then(function (tags) {
content.removeObjects(self.get('tags'));
content.pushObjects(tags.tags.map(function (tag) {
return ajax(tagsUrl, {data: tagsQuery}).then((tags) => {
content.removeObjects(this.get('tags'));
content.pushObjects(tags.tags.map((tag) => {
return {
id: `tag.${tag.slug}`,
title: tag.name,
@ -99,80 +102,81 @@ export default Ember.Component.extend({
});
},
_keepSelectionClear: Ember.observer('selection', function () {
_keepSelectionClear: observer('selection', function () {
if (this.get('selection') !== null) {
this.set('selection', null);
}
}),
_setKeymasterScope: function () {
_setKeymasterScope() {
key.setScope('search-input');
},
_resetKeymasterScope: function () {
_resetKeymasterScope() {
key.setScope('default');
},
willDestroy: function () {
willDestroy() {
this._resetKeymasterScope();
},
actions: {
openSelected: function (selected) {
var transition = null,
self = this;
openSelected(selected) {
let transition = null;
if (!selected) { return; }
if (!selected) {
return;
}
if (selected.category === 'Posts' || selected.category === 'Pages') {
let id = selected.id.replace('post.', '');
transition = self.get('_routing.router').transitionTo('editor.edit', id);
transition = this.get('_routing.router').transitionTo('editor.edit', id);
}
if (selected.category === 'Users') {
let id = selected.id.replace('user.', '');
transition = self.get('_routing.router').transitionTo('team.user', id);
transition = this.get('_routing.router').transitionTo('team.user', id);
}
if (selected.category === 'Tags') {
let id = selected.id.replace('tag.', '');
transition = self.get('_routing.router').transitionTo('settings.tags.tag', id);
transition = this.get('_routing.router').transitionTo('settings.tags.tag', id);
}
transition.then(function () {
if (self.get('_selectize').$control_input.is(':focus')) {
self._setKeymasterScope();
transition.then(() => {
if (this.get('_selectize').$control_input.is(':focus')) {
this._setKeymasterScope();
}
});
},
focusInput: function () {
focusInput() {
this.get('_selectize').focus();
},
onInit: function () {
var selectize = this.get('_selectize'),
html = '<div class="dropdown-empty-message">Nothing found&hellip;</div>';
onInit() {
let selectize = this.get('_selectize');
let html = '<div class="dropdown-empty-message">Nothing found&hellip;</div>';
selectize.$empty_results_container = $(html);
selectize.$empty_results_container.hide();
selectize.$dropdown.append(selectize.$empty_results_container);
},
onFocus: function () {
onFocus() {
this._setKeymasterScope();
this.refreshContent();
},
onBlur: function () {
var selectize = this.get('_selectize');
onBlur() {
let selectize = this.get('_selectize');
this._resetKeymasterScope();
selectize.$empty_results_container.hide();
},
onType: function () {
var selectize = this.get('_selectize');
onType() {
let selectize = this.get('_selectize');
if (!selectize.hasOptions) {
selectize.open();

View File

@ -1,28 +1,34 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed} = Ember;
const {reads} = computed;
function K() {
return this;
}
export default Component.extend({
content: null,
prompt: null,
optionValuePath: 'id',
optionLabelPath: 'title',
selection: null,
action: Ember.K, // action to fire on change
action: K, // action to fire on change
// shadow the passed-in `selection` to avoid
// leaking changes to it via a 2-way binding
_selection: Ember.computed.reads('selection'),
_selection: reads('selection'),
actions: {
change: function () {
var selectEl = this.$('select')[0],
selectedIndex = selectEl.selectedIndex,
content = this.get('content'),
change() {
let [selectEl] = this.$('select');
let {selectedIndex} = selectEl;
// decrement index by 1 if we have a prompt
hasPrompt = !!this.get('prompt'),
contentIndex = hasPrompt ? selectedIndex - 1 : selectedIndex,
// decrement index by 1 if we have a prompt
let hasPrompt = !!this.get('prompt');
let contentIndex = hasPrompt ? selectedIndex - 1 : selectedIndex;
selection = content.objectAt(contentIndex);
let selection = this.get('content').objectAt(contentIndex);
// set the local, shadowed selection to avoid leaking
// changes to `selection` out via 2-way binding

View File

@ -1,30 +1,34 @@
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
import Ember from 'ember';
import EmberSelectizeComponent from 'ember-cli-selectize/components/ember-selectize';
const {computed, isBlank, get, on, run} = Ember;
const emberA = Ember.A;
export default EmberSelectizeComponent.extend({
selectizeOptions: Ember.computed(function () {
const options = this._super(...arguments);
selectizeOptions: computed(function () {
let options = this._super(...arguments);
options.onChange = Ember.run.bind(this, '_onChange');
options.onChange = run.bind(this, '_onChange');
return options;
}),
_dontOpenWhenBlank: Ember.on('didInsertElement', function () {
var openOnFocus = this.get('openOnFocus');
_dontOpenWhenBlank: on('didInsertElement', function () {
let openOnFocus = this.get('openOnFocus');
if (!openOnFocus) {
Ember.run.schedule('afterRender', this, function () {
var selectize = this._selectize;
run.schedule('afterRender', this, function () {
let selectize = this._selectize;
if (selectize) {
selectize.on('dropdown_open', function () {
if (Ember.isBlank(selectize.$control_input.val())) {
if (isBlank(selectize.$control_input.val())) {
selectize.close();
}
});
selectize.on('type', function (filter) {
if (Ember.isBlank(filter)) {
if (isBlank(filter)) {
selectize.close();
}
});
@ -37,15 +41,15 @@ export default EmberSelectizeComponent.extend({
* Event callback that is triggered when user creates a tag
* - modified to pass the caret position to the action
*/
_create: function (input, callback) {
var caret = this._selectize.caretPos;
_create(input, callback) {
let caret = this._selectize.caretPos;
// Delete user entered text
this._selectize.setTextboxValue('');
// Send create action
// allow the observers and computed properties to run first
Ember.run.schedule('actions', this, function () {
run.schedule('actions', this, function () {
this.sendAction('create-item', input, caret);
});
// We cancel the creation here, so it's up to you to include the created element
@ -53,10 +57,10 @@ export default EmberSelectizeComponent.extend({
callback(null);
},
_addSelection: function (obj) {
var _valuePath = this.get('_valuePath'),
val = Ember.get(obj, _valuePath),
caret = this._selectize.caretPos;
_addSelection(obj) {
let _valuePath = this.get('_valuePath');
let val = get(obj, _valuePath);
let caret = this._selectize.caretPos;
// caret position is always 1 more than the desired index as this method
// is called after selectize has inserted the item and the caret has moved
@ -65,15 +69,15 @@ export default EmberSelectizeComponent.extend({
this.get('selection').insertAt(caret, obj);
Ember.run.schedule('actions', this, function () {
run.schedule('actions', this, function () {
this.sendAction('add-item', obj);
this.sendAction('add-value', val);
});
},
_onChange: function (args) {
const selection = Ember.get(this, 'selection'),
valuePath = Ember.get(this, '_valuePath');
_onChange(args) {
let selection = Ember.get(this, 'selection');
let valuePath = Ember.get(this, '_valuePath');
if (!args || !selection || !Ember.isArray(selection) || args.length !== selection.length) {
return;
@ -87,11 +91,13 @@ export default EmberSelectizeComponent.extend({
return;
}
let reorderedSelection = Ember.A([]);
let reorderedSelection = emberA([]);
args.forEach(function (value) {
const obj = selection.find(function (item) {
return (Ember.get(item, valuePath) + '') === value;
args.forEach((value) => {
let obj = selection.find(function (item) {
// jscs:disable
return (get(item, valuePath) + '') === value;
// jscs:enable
});
if (obj) {

View File

@ -1,7 +1,9 @@
/*jshint scripturl:true*/
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, on} = Ember;
export default Component.extend({
tagName: 'a',
anchor: '',
classNames: ['sr-only', 'sr-only-focusable'],
@ -13,9 +15,9 @@ export default Ember.Component.extend({
// anchor behaviors or ignored
href: Ember.String.htmlSafe('javascript:;'),
scrollTo: Ember.on('click', function () {
var anchor = this.get('anchor'),
$el = Ember.$(anchor);
scrollTo: on('click', function () {
let anchor = this.get('anchor');
let $el = Ember.$(anchor);
if ($el) {
// Scrolls to the top of main content or whatever

View File

@ -1,6 +1,9 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, observer, run} = Ember;
const {equal} = computed;
export default Component.extend({
tagName: 'button',
buttonText: '',
submitting: false,
@ -12,9 +15,9 @@ export default Ember.Component.extend({
attributeBindings: ['disabled', 'type', 'tabindex'],
// Must be set on the controller
disabled: Ember.computed.equal('showSpinner', true),
disabled: equal('showSpinner', true),
click: function () {
click() {
if (this.get('action')) {
this.sendAction('action');
return false;
@ -22,13 +25,13 @@ export default Ember.Component.extend({
return true;
},
toggleSpinner: Ember.observer('submitting', function () {
var submitting = this.get('submitting'),
timeout = this.get('showSpinnerTimeout');
toggleSpinner: observer('submitting', function () {
let submitting = this.get('submitting');
let timeout = this.get('showSpinnerTimeout');
if (submitting) {
this.set('showSpinner', true);
this.set('showSpinnerTimeout', Ember.run.later(this, function () {
this.set('showSpinnerTimeout', run.later(this, function () {
if (!this.get('submitting')) {
this.set('showSpinner', false);
}
@ -39,7 +42,7 @@ export default Ember.Component.extend({
}
}),
setSize: Ember.observer('showSpinner', function () {
setSize: observer('showSpinner', function () {
if (this.get('showSpinner') && this.get('autoWidth')) {
this.$().width(this.$().width());
this.$().height(this.$().height());
@ -49,7 +52,7 @@ export default Ember.Component.extend({
}
}),
willDestroy: function () {
Ember.run.cancel(this.get('showSpinnerTimeout'));
willDestroy() {
run.cancel(this.get('showSpinnerTimeout'));
}
});

View File

@ -1,28 +1,31 @@
import Ember from 'ember';
const {Component, computed} = Ember;
const {alias} = computed;
// See gh-tabs-manager.js for use
export default Ember.Component.extend({
export default Component.extend({
classNameBindings: ['active'],
tabsManager: Ember.computed(function () {
tabsManager: computed(function () {
return this.nearestWithProperty('isTabsManager');
}),
tab: Ember.computed('tabsManager.tabs.[]', 'tabsManager.tabPanes.[]', function () {
var index = this.get('tabsManager.tabPanes').indexOf(this),
tabs = this.get('tabsManager.tabs');
tab: computed('tabsManager.tabs.[]', 'tabsManager.tabPanes.[]', function () {
let index = this.get('tabsManager.tabPanes').indexOf(this);
let tabs = this.get('tabsManager.tabs');
return tabs && tabs.objectAt(index);
}),
active: Ember.computed.alias('tab.active'),
active: alias('tab.active'),
willRender: function () {
willRender() {
// Register with the tabs manager
this.get('tabsManager').registerTabPane(this);
},
willDestroyElement: function () {
willDestroyElement() {
// Deregister with the tabs manager
this.get('tabsManager').unregisterTabPane(this);
}

View File

@ -1,30 +1,32 @@
import Ember from 'ember';
const {Component, computed} = Ember;
// See gh-tabs-manager.js for use
export default Ember.Component.extend({
tabsManager: Ember.computed(function () {
export default Component.extend({
tabsManager: computed(function () {
return this.nearestWithProperty('isTabsManager');
}),
active: Ember.computed('tabsManager.activeTab', function () {
active: computed('tabsManager.activeTab', function () {
return this.get('tabsManager.activeTab') === this;
}),
index: Ember.computed('tabsManager.tabs.[]', function () {
index: computed('tabsManager.tabs.[]', function () {
return this.get('tabsManager.tabs').indexOf(this);
}),
// Select on click
click: function () {
click() {
this.get('tabsManager').select(this);
},
willRender: function () {
willRender() {
// register the tabs with the tab manager
this.get('tabsManager').registerTab(this);
},
willDestroyElement: function () {
willDestroyElement() {
// unregister the tabs with the tab manager
this.get('tabsManager').unregisterTab(this);
}

View File

@ -1,4 +1,7 @@
import Ember from 'ember';
const {Component} = Ember;
/**
Heavily inspired by ic-tabs (https://github.com/instructure/ic-tabs)
@ -48,35 +51,35 @@ Both tab and tab-pane elements have an "active"
class applied when they are active.
*/
export default Ember.Component.extend({
export default Component.extend({
activeTab: null,
tabs: [],
tabPanes: [],
// Used by children to find this tabsManager
isTabsManager: true,
// Called when a gh-tab is clicked.
select: function (tab) {
select(tab) {
this.set('activeTab', tab);
this.sendAction('selected');
},
// Used by children to find this tabsManager
isTabsManager: true,
// Register tabs and their panes to allow for
// interaction between components.
registerTab: function (tab) {
registerTab(tab) {
this.get('tabs').addObject(tab);
},
unregisterTab: function (tab) {
unregisterTab(tab) {
this.get('tabs').removeObject(tab);
},
registerTabPane: function (tabPane) {
registerTabPane(tabPane) {
this.get('tabPanes').addObject(tabPane);
},
unregisterTabPane: function (tabPane) {
unregisterTabPane(tabPane) {
this.get('tabPanes').removeObject(tabPane);
}
});

View File

@ -2,9 +2,9 @@
import Ember from 'ember';
import boundOneWay from 'ghost/utils/bound-one-way';
const {get} = Ember;
const {Component, Handlebars, computed, get, inject} = Ember;
export default Ember.Component.extend({
export default Component.extend({
tag: null,
@ -16,12 +16,12 @@ export default Ember.Component.extend({
isViewingSubview: false,
config: Ember.inject.service(),
config: inject.service(),
mediaQueries: Ember.inject.service(),
isMobile: Ember.computed.reads('mediaQueries.maxWidth600'),
title: Ember.computed('tag.isNew', function () {
title: computed('tag.isNew', function () {
if (this.get('tag.isNew')) {
return 'New Tag';
} else {
@ -29,25 +29,25 @@ export default Ember.Component.extend({
}
}),
seoTitle: Ember.computed('scratchName', 'scratchMetaTitle', function () {
seoTitle: computed('scratchName', 'scratchMetaTitle', function () {
let metaTitle = this.get('scratchMetaTitle') || '';
metaTitle = metaTitle.length > 0 ? metaTitle : this.get('scratchName');
if (metaTitle && metaTitle.length > 70) {
metaTitle = metaTitle.substring(0, 70).trim();
metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = Ember.String.htmlSafe(metaTitle + '&hellip;');
metaTitle = Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = Ember.String.htmlSafe(`${metaTitle}&hellip;`);
}
return metaTitle;
}),
seoURL: Ember.computed('scratchSlug', function () {
const blogUrl = this.get('config.blogUrl'),
seoSlug = this.get('scratchSlug') || '';
seoURL: computed('scratchSlug', function () {
let blogUrl = this.get('config.blogUrl');
let seoSlug = this.get('scratchSlug') || '';
let seoURL = blogUrl + '/tag/' + seoSlug;
let seoURL = `${blogUrl}/tag/${seoSlug}`;
// only append a slash to the URL if the slug exists
if (seoSlug) {
@ -56,73 +56,73 @@ export default Ember.Component.extend({
if (seoURL.length > 70) {
seoURL = seoURL.substring(0, 70).trim();
seoURL = Ember.String.htmlSafe(seoURL + '&hellip;');
seoURL = Ember.String.htmlSafe(`${seoURL}&hellip;`);
}
return seoURL;
}),
seoDescription: Ember.computed('scratchDescription', 'scratchMetaDescription', function () {
seoDescription: computed('scratchDescription', 'scratchMetaDescription', function () {
let metaDescription = this.get('scratchMetaDescription') || '';
metaDescription = metaDescription.length > 0 ? metaDescription : this.get('scratchDescription');
if (metaDescription && metaDescription.length > 156) {
metaDescription = metaDescription.substring(0, 156).trim();
metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription);
metaDescription = Ember.String.htmlSafe(metaDescription + '&hellip;');
metaDescription = Handlebars.Utils.escapeExpression(metaDescription);
metaDescription = Ember.String.htmlSafe(`${metaDescription}&hellip;`);
}
return metaDescription;
}),
didReceiveAttrs: function (attrs) {
didReceiveAttrs(attrs) {
if (get(attrs, 'newAttrs.tag.value.id') !== get(attrs, 'oldAttrs.tag.value.id')) {
this.reset();
}
},
reset: function () {
reset() {
this.set('isViewingSubview', false);
if (this.$()) {
this.$('.settings-menu-pane').scrollTop(0);
}
},
focusIn: function () {
focusIn() {
key.setScope('tag-settings-form');
},
focusOut: function () {
focusOut() {
key.setScope('default');
},
actions: {
setProperty: function (property, value) {
setProperty(property, value) {
this.attrs.setProperty(property, value);
},
setCoverImage: function (image) {
setCoverImage(image) {
this.attrs.setProperty('image', image);
},
clearCoverImage: function () {
clearCoverImage() {
this.attrs.setProperty('image', '');
},
setUploaderReference: function () {
setUploaderReference() {
// noop
},
openMeta: function () {
openMeta() {
this.set('isViewingSubview', true);
},
closeMeta: function () {
closeMeta() {
this.set('isViewingSubview', false);
},
deleteTag: function () {
deleteTag() {
this.sendAction('openModal', 'delete-tag', this.get('tag'));
}
}

View File

@ -1,28 +1,29 @@
import Ember from 'ember';
const {isBlank} = Ember;
const {Component, computed, inject, isBlank, observer, run} = Ember;
const {equal, reads} = computed;
export default Ember.Component.extend({
export default Component.extend({
classNames: ['view-container'],
classNameBindings: ['isMobile'],
mediaQueries: Ember.inject.service(),
mediaQueries: inject.service(),
tags: null,
selectedTag: null,
isMobile: Ember.computed.reads('mediaQueries.maxWidth600'),
isEmpty: Ember.computed.equal('tags.length', 0),
isMobile: reads('mediaQueries.maxWidth600'),
isEmpty: equal('tags.length', 0),
init: function () {
init() {
this._super(...arguments);
Ember.run.schedule('actions', this, this.fireMobileChangeActions);
run.schedule('actions', this, this.fireMobileChangeActions);
},
displaySettingsPane: Ember.computed('isEmpty', 'selectedTag', 'isMobile', function () {
const isEmpty = this.get('isEmpty'),
selectedTag = this.get('selectedTag'),
isMobile = this.get('isMobile');
displaySettingsPane: computed('isEmpty', 'selectedTag', 'isMobile', function () {
let isEmpty = this.get('isEmpty');
let selectedTag = this.get('selectedTag');
let isMobile = this.get('isMobile');
// always display settings pane for blank-slate on mobile
if (isMobile && isEmpty) {
@ -38,7 +39,7 @@ export default Ember.Component.extend({
return true;
}),
fireMobileChangeActions: Ember.observer('isMobile', function () {
fireMobileChangeActions: observer('isMobile', function () {
if (!this.get('isMobile')) {
this.sendAction('leftMobile');
}

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
import TextInputMixin from 'ghost/mixins/text-input';
export default Ember.TextArea.extend(TextInputMixin, {
const {TextArea} = Ember;
export default TextArea.extend(TextInputMixin, {
classNames: 'gh-input'
});

View File

@ -1,12 +1,14 @@
import Ember from 'ember';
/*global device*/
import Ember from 'ember';
export default Ember.TextField.extend({
const {TextField, computed, on} = Ember;
export default TextField.extend({
focus: true,
classNames: 'gh-input',
attributeBindings: ['autofocus'],
autofocus: Ember.computed(function () {
autofocus: computed(function () {
if (this.get('focus')) {
return (device.ios()) ? false : 'autofocus';
}
@ -14,7 +16,7 @@ export default Ember.TextField.extend({
return false;
}),
focusField: Ember.on('didInsertElement', function () {
focusField: on('didInsertElement', function () {
// This fix is required until Mobile Safari has reliable
// autofocus, select() or focus() support
if (this.get('focus') && !device.ios()) {
@ -22,8 +24,8 @@ export default Ember.TextField.extend({
}
}),
trimValue: Ember.on('focusOut', function () {
var text = this.$().val();
trimValue: on('focusOut', function () {
let text = this.$().val();
this.$().val(text.trim());
})
});

View File

@ -3,44 +3,50 @@ import ModalDialog from 'ghost/components/gh-modal-dialog';
import upload from 'ghost/assets/lib/uploader';
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
const {inject, isEmpty} = Ember;
export default ModalDialog.extend({
layoutName: 'components/gh-modal-dialog',
config: Ember.inject.service(),
config: inject.service(),
didInsertElement: function () {
this._super();
didInsertElement() {
this._super(...arguments);
upload.call(this.$('.js-drop-zone'), {fileStorage: this.get('config.fileStorage')});
},
keyDown: function () {
keyDown() {
this.setErrorState(false);
},
setErrorState: function (state) {
setErrorState(state) {
if (state) {
this.$('.js-upload-url').addClass('error');
} else {
this.$('.js-upload-url').removeClass('error');
}
},
confirm: {
reject: {
func: function () { // The function called on rejection
return true;
},
buttonClass: 'btn btn-default',
text: 'Cancel' // The reject button text
text: 'Cancel', // The reject button text
func() { // The function called on rejection
return true;
}
},
accept: {
buttonClass: 'btn btn-blue right',
text: 'Save', // The accept button text: 'Save'
func: function () {
var imageType = 'model.' + this.get('imageType'),
value;
func() {
let imageType = `model.${this.get('imageType')}`;
let value;
if (this.$('.js-upload-url').val()) {
value = this.$('.js-upload-url').val();
if (!Ember.isEmpty(value) && !cajaSanitizers.url(value)) {
if (!isEmpty(value) && !cajaSanitizers.url(value)) {
this.setErrorState(true);
return {message: 'Image URI is not valid'};
}
@ -55,12 +61,13 @@ export default ModalDialog.extend({
},
actions: {
closeModal: function () {
closeModal() {
this.sendAction();
},
confirm: function (type) {
var result,
func = this.get('confirm.' + type + '.func');
confirm(type) {
let func = this.get(`confirm.${type}.func`);
let result;
if (typeof func === 'function') {
result = func.apply(this);
@ -68,7 +75,7 @@ export default ModalDialog.extend({
if (!result.message) {
this.sendAction();
this.sendAction('confirm' + type);
this.sendAction(`confirm${type}`);
}
}
}

View File

@ -1,18 +1,20 @@
import Ember from 'ember';
import uploader from 'ghost/assets/lib/uploader';
export default Ember.Component.extend({
const {Component, computed, get, inject, isEmpty, run} = Ember;
export default Component.extend({
classNames: ['image-uploader', 'js-post-image-upload'],
config: Ember.inject.service(),
config: inject.service(),
imageSource: Ember.computed('image', function () {
imageSource: computed('image', function () {
return this.get('image') || '';
}),
// removes event listeners from the uploader
removeListeners: function () {
var $this = this.$();
removeListeners() {
let $this = this.$();
$this.off();
$this.find('.js-cancel').off();
@ -22,20 +24,19 @@ export default Ember.Component.extend({
// between transitions Glimmer will re-use the existing elements including
// those that arealready decorated by jQuery. The following works around
// situations where the image is changed without a full teardown/rebuild
didReceiveAttrs: function (attrs) {
var oldValue = attrs.oldAttrs && Ember.get(attrs.oldAttrs, 'image.value'),
newValue = attrs.newAttrs && Ember.get(attrs.newAttrs, 'image.value'),
self = this;
didReceiveAttrs(attrs) {
let oldValue = attrs.oldAttrs && get(attrs.oldAttrs, 'image.value');
let newValue = attrs.newAttrs && get(attrs.newAttrs, 'image.value');
// always reset when we receive a blank image
// - handles navigating to populated image from blank image
if (Ember.isEmpty(newValue) && !Ember.isEmpty(oldValue)) {
self.$()[0].uploaderUi.reset();
if (isEmpty(newValue) && !isEmpty(oldValue)) {
this.$()[0].uploaderUi.reset();
}
// re-init if we receive a new image but the uploader is blank
// - handles back button navigating from blank image to populated image
if (!Ember.isEmpty(newValue) && this.$()) {
if (!isEmpty(newValue) && this.$()) {
if (this.$('.js-upload-target').attr('src') === '') {
this.$()[0].uploaderUi.reset();
this.$()[0].uploaderUi.initWithImage();
@ -43,34 +44,31 @@ export default Ember.Component.extend({
}
},
didInsertElement: function () {
didInsertElement() {
this.send('initUploader');
},
willDestroyElement: function () {
willDestroyElement() {
this.removeListeners();
},
actions: {
initUploader: function () {
var ref,
el = this.$(),
self = this;
ref = uploader.call(el, {
initUploader() {
let el = this.$();
let ref = uploader.call(el, {
editor: true,
fileStorage: this.get('config.fileStorage')
});
el.on('uploadsuccess', function (event, result) {
el.on('uploadsuccess', (event, result) => {
if (result && result !== '' && result !== 'http://') {
Ember.run(self, function () {
run(this, function () {
this.sendAction('uploaded', result);
});
}
});
el.on('imagecleared', Ember.run.bind(self, 'sendAction', 'canceled'));
el.on('imagecleared', run.bind(this, 'sendAction', 'canceled'));
this.sendAction('initUploader', ref);
}

View File

@ -1,26 +1,30 @@
import Ember from 'ember';
const {Component, computed, inject} = Ember;
/*
Example usage:
{{gh-url-preview prefix="tag" slug=theSlugValue tagName="p" classNames="description"}}
*/
export default Ember.Component.extend({
export default Component.extend({
classNames: 'ghost-url-preview',
prefix: null,
slug: null,
config: Ember.inject.service(),
config: inject.service(),
url: Ember.computed('slug', function () {
url: computed('slug', function () {
// Get the blog URL and strip the scheme
var blogUrl = this.get('config.blogUrl'),
noSchemeBlogUrl = blogUrl.substr(blogUrl.indexOf('://') + 3), // Remove `http[s]://`
let blogUrl = this.get('config.blogUrl');
// Remove `http[s]://`
let noSchemeBlogUrl = blogUrl.substr(blogUrl.indexOf('://') + 3);
// Get the prefix and slug values
prefix = this.get('prefix') ? this.get('prefix') + '/' : '',
slug = this.get('slug') ? this.get('slug') + '/' : '',
// Get the prefix and slug values
let prefix = this.get('prefix') ? `${this.get('prefix')}/` : '';
let slug = this.get('slug') ? `${this.get('slug')}/` : '';
// Join parts of the URL together with slashes
theUrl = noSchemeBlogUrl + '/' + prefix + slug;
// Join parts of the URL together with slashes
let theUrl = `${noSchemeBlogUrl}/${prefix}${slug}`;
return theUrl;
})

View File

@ -1,24 +1,26 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
export default Component.extend({
tagName: '',
user: null,
ghostPaths: Ember.inject.service('ghost-paths'),
ghostPaths: inject.service('ghost-paths'),
userDefault: Ember.computed('ghostPaths', function () {
userDefault: computed('ghostPaths', function () {
return this.get('ghostPaths.url').asset('/shared/img/user-image.png');
}),
userImageBackground: Ember.computed('user.image', 'userDefault', function () {
var url = this.get('user.image') || this.get('userDefault');
userImageBackground: computed('user.image', 'userDefault', function () {
let url = this.get('user.image') || this.get('userDefault');
return Ember.String.htmlSafe(`background-image: url(${url})`);
}),
lastLogin: Ember.computed('user.last_login', function () {
var lastLogin = this.get('user.last_login');
lastLogin: computed('user.last_login', function () {
let lastLogin = this.get('user.last_login');
return lastLogin ? lastLogin.fromNow() : '(Never)';
})

View File

@ -1,28 +1,29 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component, computed, inject} = Ember;
export default Component.extend({
tagName: '',
user: null,
isSending: false,
notifications: Ember.inject.service(),
notifications: inject.service(),
createdAt: Ember.computed('user.created_at', function () {
var createdAt = this.get('user.created_at');
createdAt: computed('user.created_at', function () {
let createdAt = this.get('user.created_at');
return createdAt ? createdAt.fromNow() : '';
}),
actions: {
resend: function () {
var user = this.get('user'),
notifications = this.get('notifications'),
self = this;
resend() {
let user = this.get('user');
let notifications = this.get('notifications');
this.set('isSending', true);
user.resendInvite().then(function (result) {
var notificationText = 'Invitation resent! (' + user.get('email') + ')';
user.resendInvite().then((result) => {
let notificationText = `Invitation resent! (${user.get('email')})`;
// If sending the invitation email fails, the API will still return a status of 201
// but the user's status in the response object will be 'invited-pending'.
@ -33,32 +34,31 @@ export default Ember.Component.extend({
notifications.showNotification(notificationText);
notifications.closeAlerts('invite.resend');
}
}).catch(function (error) {
}).catch((error) => {
notifications.showAPIError(error, {key: 'invite.resend'});
}).finally(function () {
self.set('isSending', false);
}).finally(() => {
this.set('isSending', false);
});
},
revoke: function () {
var user = this.get('user'),
email = user.get('email'),
notifications = this.get('notifications'),
self = this;
revoke() {
let user = this.get('user');
let email = user.get('email');
let notifications = this.get('notifications');
// reload the user to get the most up-to-date information
user.reload().then(function () {
user.reload().then(() => {
if (user.get('invited')) {
user.destroyRecord().then(function () {
var notificationText = 'Invitation revoked. (' + email + ')';
user.destroyRecord().then(() => {
let notificationText = `Invitation revoked. (${email})`;
notifications.showNotification(notificationText);
notifications.closeAlerts('invite.revoke');
}).catch(function (error) {
}).catch((error) => {
notifications.showAPIError(error, {key: 'invite.revoke'});
});
} else {
// if the user is no longer marked as "invited", then show a warning and reload the route
self.sendAction('reload');
this.sendAction('reload');
notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true, key: 'invite.revoke.already-accepted'});
}
});

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
import ValidationStateMixin from 'ghost/mixins/validation-state';
const {Component, computed} = Ember;
/**
* Handles the CSS necessary to show a specific property state. When passed a
* DS.Errors object and a property name, if the DS.Errors object has errors for
@ -8,12 +10,12 @@ import ValidationStateMixin from 'ghost/mixins/validation-state';
* @param {DS.Errors} errors The DS.Errors object
* @param {string} property Name of the property
*/
export default Ember.Component.extend(ValidationStateMixin, {
export default Component.extend(ValidationStateMixin, {
classNameBindings: ['errorClass'],
errorClass: Ember.computed('property', 'hasError', 'hasValidated.[]', function () {
let hasValidated = this.get('hasValidated'),
property = this.get('property');
errorClass: computed('property', 'hasError', 'hasValidated.[]', function () {
let hasValidated = this.get('hasValidated');
let property = this.get('property');
if (hasValidated && hasValidated.contains(property)) {
return this.get('hasError') ? 'error' : 'success';

View File

@ -1,10 +1,13 @@
import Ember from 'ember';
export default Ember.Component.extend({
const {Component} = Ember;
export default Component.extend({
tagName: 'h2',
classNames: ['view-title'],
actions: {
openMobileMenu: function () {
openMobileMenu() {
this.sendAction('openMobileMenu');
}
}

View File

@ -1,10 +1,12 @@
import Ember from 'ember';
export default Ember.Controller.extend({
const {Controller} = Ember;
export default Controller.extend({
updateNotificationCount: 0,
actions: {
updateNotificationChange: function (count) {
updateNotificationChange(count) {
this.set('updateNotificationCount', count);
}
}

View File

@ -1,22 +1,22 @@
import Ember from 'ember';
export default Ember.Controller.extend({
dropdown: Ember.inject.service(),
const {Controller, computed, inject} = Ember;
// jscs: disable
signedOut: Ember.computed.match('currentPath', /(signin|signup|setup|reset)/),
// jscs: enable
export default Controller.extend({
dropdown: inject.service(),
signedOut: computed.match('currentPath', /(signin|signup|setup|reset)/),
topNotificationCount: 0,
showMobileMenu: false,
showSettingsMenu: false,
autoNav: false,
autoNavOpen: Ember.computed('autoNav', {
get: function () {
autoNavOpen: computed('autoNav', {
get() {
return false;
},
set: function (key, value) {
set(key, value) {
if (this.get('autoNav')) {
return value;
}
@ -25,23 +25,23 @@ export default Ember.Controller.extend({
}),
actions: {
topNotificationChange: function (count) {
topNotificationChange(count) {
this.set('topNotificationCount', count);
},
toggleAutoNav: function () {
toggleAutoNav() {
this.toggleProperty('autoNav');
},
openAutoNav: function () {
openAutoNav() {
this.set('autoNavOpen', true);
},
closeAutoNav: function () {
closeAutoNav() {
this.set('autoNavOpen', false);
},
closeMobileMenu: function () {
closeMobileMenu() {
this.set('showMobileMenu', false);
}
}

View File

@ -1,9 +1,11 @@
import Ember from 'ember';
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
export default Ember.Controller.extend(EditorControllerMixin, {
const {Controller} = Ember;
export default Controller.extend(EditorControllerMixin, {
actions: {
openDeleteModal: function () {
openDeleteModal() {
this.send('openModal', 'delete-post', this.get('model'));
}
}

View File

@ -1,18 +1,23 @@
import Ember from 'ember';
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
export default Ember.Controller.extend(EditorControllerMixin, {
const {Controller} = Ember;
function K() {
return this;
}
export default Controller.extend(EditorControllerMixin, {
// Overriding autoSave on the base controller, as the new controller shouldn't be autosaving
autoSave: Ember.K,
autoSave: K,
actions: {
/**
* Redirect to editor after the first save
*/
save: function (options) {
var self = this;
return this._super(options).then(function (model) {
save(options) {
return this._super(options).then((model) => {
if (model.get('id')) {
self.replaceRoute('editor.edit', model);
this.replaceRoute('editor.edit', model);
}
});
}

View File

@ -1,15 +1,20 @@
import Ember from 'ember';
export default Ember.Controller.extend({
code: Ember.computed('content.status', function () {
const {Controller, computed} = Ember;
export default Controller.extend({
stack: false,
code: computed('content.status', function () {
return this.get('content.status') > 200 ? this.get('content.status') : 500;
}),
message: Ember.computed('content.statusText', function () {
message: computed('content.statusText', function () {
if (this.get('code') === 404) {
return 'Page not found';
}
return this.get('content.statusText') !== 'error' ? this.get('content.statusText') : 'Internal Server Error';
}),
stack: false
})
});

View File

@ -1,20 +1,14 @@
import Ember from 'ember';
export default Ember.Controller.extend(Ember.PromiseProxyMixin, {
init: function () {
var promise;
const {Controller, PromiseProxyMixin, computed} = Ember;
const {alias} = computed;
promise = this.store.query('setting', {type: 'blog,theme'}).then(function (settings) {
return settings.get('firstObject');
});
export default Controller.extend(PromiseProxyMixin, {
this.set('promise', promise);
},
setting: alias('content'),
setting: Ember.computed.alias('content'),
labs: Ember.computed('isSettled', 'setting.labs', function () {
var value = {};
labs: computed('isSettled', 'setting.labs', function () {
let value = {};
if (this.get('isFulfilled')) {
try {
@ -27,7 +21,15 @@ export default Ember.Controller.extend(Ember.PromiseProxyMixin, {
return value;
}),
publicAPI: Ember.computed('config.publicAPI', 'labs.publicAPI', function () {
publicAPI: computed('config.publicAPI', 'labs.publicAPI', function () {
return this.get('config.publicAPI') || this.get('labs.publicAPI');
})
}),
init() {
let promise = this.store.query('setting', {type: 'blog,theme'}).then((settings) => {
return settings.get('firstObject');
});
this.set('promise', promise);
}
});

View File

@ -1,5 +1,8 @@
import Ember from 'ember';
export default Ember.Controller.extend({
generatedHTML: Ember.computed.alias('model.generatedHTML')
const {Controller, computed} = Ember;
const {alias} = computed;
export default Controller.extend({
generatedHTML: alias('model.generatedHTML')
});

View File

@ -1,29 +1,11 @@
import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend({
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
const {Controller, inject} = Ember;
actions: {
confirmAccept: function () {
var self = this;
ajax(this.get('ghostPaths.url').api('db'), {
type: 'DELETE'
}).then(function () {
self.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'});
self.store.unloadAll('post');
self.store.unloadAll('tag');
}).catch(function (response) {
self.get('notifications').showAPIError(response, {key: 'all-content.delete'});
});
},
confirmReject: function () {
return false;
}
},
export default Controller.extend({
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
confirm: {
accept: {
@ -34,5 +16,23 @@ export default Ember.Controller.extend({
text: 'Cancel',
buttonClass: 'btn btn-default btn-minor'
}
},
actions: {
confirmAccept() {
ajax(this.get('ghostPaths.url').api('db'), {
type: 'DELETE'
}).then(() => {
this.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'});
this.store.unloadAll('post');
this.store.unloadAll('tag');
}).catch((response) => {
this.get('notifications').showAPIError(response, {key: 'all-content.delete'});
});
},
confirmReject() {
return false;
}
}
});

View File

@ -1,30 +1,10 @@
import Ember from 'ember';
export default Ember.Controller.extend({
dropdown: Ember.inject.service(),
notifications: Ember.inject.service(),
const {Controller, inject} = Ember;
actions: {
confirmAccept: function () {
var self = this,
model = this.get('model');
// definitely want to clear the data store and post of any unsaved, client-generated tags
model.updateTags();
model.destroyRecord().then(function () {
self.get('dropdown').closeDropdowns();
self.get('notifications').closeAlerts('post.delete');
self.transitionToRoute('posts.index');
}, function () {
self.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'});
});
},
confirmReject: function () {
return false;
}
},
export default Controller.extend({
dropdown: inject.service(),
notifications: inject.service(),
confirm: {
accept: {
@ -35,5 +15,26 @@ export default Ember.Controller.extend({
text: 'Cancel',
buttonClass: 'btn btn-default btn-minor'
}
},
actions: {
confirmAccept() {
let model = this.get('model');
// definitely want to clear the data store and post of any unsaved, client-generated tags
model.updateTags();
model.destroyRecord().then(() => {
this.get('dropdown').closeDropdowns();
this.get('notifications').closeAlerts('post.delete');
this.transitionToRoute('posts.index');
}, () => {
this.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'});
});
},
confirmReject() {
return false;
}
}
});

View File

@ -11,7 +11,7 @@ export default Controller.extend({
}),
actions: {
confirmAccept: function () {
confirmAccept() {
let tag = this.get('model');
this.send('closeMenus');
@ -27,7 +27,7 @@ export default Controller.extend({
});
},
confirmReject: function () {
confirmReject() {
return false;
}
},

View File

@ -1,47 +1,30 @@
import Ember from 'ember';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
const {Controller, PromiseProxyMixin, computed, inject} = Ember;
const {alias} = computed;
userPostCount: Ember.computed('model.id', function () {
var promise,
query = {
filter: `author:${this.get('model.slug')}`,
status: 'all'
};
export default Controller.extend({
notifications: inject.service(),
promise = this.store.query('post', query).then(function (results) {
userPostCount: computed('model.id', function () {
let query = {
filter: `author:${this.get('model.slug')}`,
status: 'all'
};
let promise = this.store.query('post', query).then((results) => {
return results.meta.pagination.total;
});
return Ember.Object.extend(Ember.PromiseProxyMixin, {
count: Ember.computed.alias('content'),
return Ember.Object.extend(PromiseProxyMixin, {
count: alias('content'),
inflection: Ember.computed('count', function () {
inflection: computed('count', function () {
return this.get('count') > 1 ? 'posts' : 'post';
})
}).create({promise: promise});
}).create({promise});
}),
actions: {
confirmAccept: function () {
var self = this,
user = this.get('model');
user.destroyRecord().then(function () {
self.get('notifications').closeAlerts('user.delete');
self.store.unloadAll('post');
self.transitionToRoute('team');
}, function () {
self.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'});
});
},
confirmReject: function () {
return false;
}
},
confirm: {
accept: {
text: 'Delete User',
@ -51,5 +34,23 @@ export default Ember.Controller.extend({
text: 'Cancel',
buttonClass: 'btn btn-default btn-minor'
}
},
actions: {
confirmAccept() {
let user = this.get('model');
user.destroyRecord().then(() => {
this.get('notifications').closeAlerts('user.delete');
this.store.unloadAll('post');
this.transitionToRoute('team');
}, () => {
this.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'});
});
},
confirmReject() {
return false;
}
}
});

View File

@ -1,29 +1,29 @@
import Ember from 'ember';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
notifications: Ember.inject.service(),
const {Controller, computed, inject, observer} = Ember;
export default Controller.extend(ValidationEngine, {
notifications: inject.service(),
validationType: 'signup',
role: null,
authorRole: null,
roles: Ember.computed(function () {
roles: computed(function () {
return this.store.query('role', {permissions: 'assign'});
}),
// Used to set the initial value for the dropdown
authorRoleObserver: Ember.observer('roles.@each.role', function () {
var self = this;
authorRoleObserver: observer('roles.@each.role', function () {
this.get('roles').then((roles) => {
let authorRole = roles.findBy('name', 'Author');
this.get('roles').then(function (roles) {
var authorRole = roles.findBy('name', 'Author');
this.set('authorRole', authorRole);
self.set('authorRole', authorRole);
if (!self.get('role')) {
self.set('role', authorRole);
if (!this.get('role')) {
this.set('role', authorRole);
}
});
}),
@ -37,69 +37,68 @@ export default Ember.Controller.extend(ValidationEngine, {
}
},
confirmReject() {
return false;
},
actions: {
setRole: function (role) {
setRole(role) {
this.set('role', role);
},
confirmAccept: function () {
var email = this.get('email'),
role = this.get('role'),
validationErrors = this.get('errors.messages'),
self = this,
newUser;
confirmAccept() {
let email = this.get('email');
let role = this.get('role');
let validationErrors = this.get('errors.messages');
let newUser;
// reset the form and close the modal
this.set('email', '');
this.set('role', self.get('authorRole'));
this.set('role', this.get('authorRole'));
this.store.findAll('user', {reload: true}).then(function (result) {
var invitedUser = result.findBy('email', email);
this.store.findAll('user', {reload: true}).then((result) => {
let invitedUser = result.findBy('email', email);
if (invitedUser) {
if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') {
self.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn', key: 'invite.send.already-invited'});
this.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn', key: 'invite.send.already-invited'});
} else {
self.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn', key: 'invite.send.user-exists'});
this.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn', key: 'invite.send.user-exists'});
}
} else {
newUser = self.store.createRecord('user', {
email: email,
status: 'invited',
role: role
newUser = this.store.createRecord('user', {
email,
role,
status: 'invited'
});
newUser.save().then(function () {
var notificationText = 'Invitation sent! (' + email + ')';
newUser.save().then(() => {
let notificationText = `Invitation sent! (${email})`;
// If sending the invitation email fails, the API will still return a status of 201
// but the user's status in the response object will be 'invited-pending'.
if (newUser.get('status') === 'invited-pending') {
self.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'});
this.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'});
} else {
self.get('notifications').closeAlerts('invite.send');
self.get('notifications').showNotification(notificationText);
this.get('notifications').closeAlerts('invite.send');
this.get('notifications').showNotification(notificationText);
}
}).catch(function (errors) {
}).catch((errors) => {
newUser.deleteRecord();
// TODO: user model includes ValidationEngine mixin so
// save is overridden in order to validate, we probably
// want to use inline-validations here and only show an
// alert if we have an actual error
if (errors) {
self.get('notifications').showErrors(errors, {key: 'invite.send'});
this.get('notifications').showErrors(errors, {key: 'invite.send'});
} else if (validationErrors) {
self.get('notifications').showAlert(validationErrors.toString(), {type: 'error', key: 'invite.send.validation-error'});
this.get('notifications').showAlert(validationErrors.toString(), {type: 'error', key: 'invite.send.validation-error'});
}
}).finally(function () {
self.get('errors').clear();
}).finally(() => {
this.get('errors').clear();
});
}
});
},
confirmReject: function () {
return false;
}
}
});

View File

@ -1,18 +1,32 @@
import Ember from 'ember';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
const {Controller, computed, inject, isArray} = Ember;
const {alias} = computed;
args: Ember.computed.alias('model'),
export default Controller.extend({
notifications: inject.service(),
args: alias('model'),
confirm: {
accept: {
text: 'Leave',
buttonClass: 'btn btn-red'
},
reject: {
text: 'Stay',
buttonClass: 'btn btn-default btn-minor'
}
},
actions: {
confirmAccept: function () {
var args = this.get('args'),
editorController,
confirmAccept() {
let args = this.get('args');
let editorController,
model,
transition;
if (Ember.isArray(args)) {
if (isArray(args)) {
editorController = args[0];
transition = args[1];
model = editorController.get('model');
@ -44,18 +58,7 @@ export default Ember.Controller.extend({
transition.retry();
},
confirmReject: function () {
}
},
confirm: {
accept: {
text: 'Leave',
buttonClass: 'btn btn-red'
},
reject: {
text: 'Stay',
buttonClass: 'btn btn-default btn-minor'
confirmReject() {
}
}
});

View File

@ -1,57 +1,56 @@
import Ember from 'ember';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, computed, inject} = Ember;
export default Controller.extend(ValidationEngine, {
validationType: 'signin',
submitting: false,
application: Ember.inject.controller(),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
application: inject.controller(),
notifications: inject.service(),
session: inject.service(),
identification: Ember.computed('session.user.email', function () {
identification: computed('session.user.email', function () {
return this.get('session.user.email');
}),
actions: {
authenticate: function () {
var appController = this.get('application'),
authStrategy = 'authenticator:oauth2',
self = this;
authenticate() {
let appController = this.get('application');
let authStrategy = 'authenticator:oauth2';
appController.set('skipAuthSuccessHandler', true);
this.get('session').authenticate(authStrategy, this.get('identification'), this.get('password')).then(function () {
self.send('closeModal');
self.set('password', '');
self.get('notifications').closeAlerts('post.save');
}).catch(function () {
this.get('session').authenticate(authStrategy, this.get('identification'), this.get('password')).then(() => {
this.send('closeModal');
this.set('password', '');
this.get('notifications').closeAlerts('post.save');
}).catch(() => {
// if authentication fails a rejected promise will be returned.
// it needs to be caught so it doesn't generate an exception in the console,
// but it's actually "handled" by the sessionAuthenticationFailed action handler.
}).finally(function () {
self.toggleProperty('submitting');
}).finally(() => {
this.toggleProperty('submitting');
appController.set('skipAuthSuccessHandler', undefined);
});
},
validateAndAuthenticate: function () {
var self = this;
validateAndAuthenticate() {
this.toggleProperty('submitting');
// Manually trigger events for input fields, ensuring legacy compatibility with
// browsers and password managers that don't send proper events on autofill
$('#login').find('input').trigger('change');
this.validate({format: false}).then(function () {
self.send('authenticate');
}).catch(function (errors) {
self.get('notifications').showErrors(errors);
this.validate({format: false}).then(() => {
this.send('authenticate');
}).catch((errors) => {
this.get('notifications').showErrors(errors);
});
},
confirmAccept: function () {
confirmAccept() {
this.send('validateAndAuthenticate');
}
}

View File

@ -1,48 +1,12 @@
import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend({
dropdown: Ember.inject.service(),
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
const {Controller, inject, isArray} = Ember;
actions: {
confirmAccept: function () {
var user = this.get('model'),
url = this.get('ghostPaths.url').api('users', 'owner'),
self = this;
self.get('dropdown').closeDropdowns();
ajax(url, {
type: 'PUT',
data: {
owner: [{
id: user.get('id')
}]
}
}).then(function (response) {
// manually update the roles for the users that just changed roles
// because store.pushPayload is not working with embedded relations
if (response && Ember.isArray(response.users)) {
response.users.forEach(function (userJSON) {
var user = self.store.peekRecord('user', userJSON.id),
role = self.store.peekRecord('role', userJSON.roles[0].id);
user.set('role', role);
});
}
self.get('notifications').showAlert('Ownership successfully transferred to ' + user.get('name'), {type: 'success', key: 'owner.transfer.success'});
}).catch(function (error) {
self.get('notifications').showAPIError(error, {key: 'owner.transfer'});
});
},
confirmReject: function () {
return false;
}
},
export default Controller.extend({
dropdown: inject.service(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
confirm: {
accept: {
@ -53,5 +17,42 @@ export default Ember.Controller.extend({
text: 'Cancel',
buttonClass: 'btn btn-default btn-minor'
}
},
actions: {
confirmAccept() {
let user = this.get('model');
let url = this.get('ghostPaths.url').api('users', 'owner');
this.get('dropdown').closeDropdowns();
ajax(url, {
type: 'PUT',
data: {
owner: [{
id: user.get('id')
}]
}
}).then((response) => {
// manually update the roles for the users that just changed roles
// because store.pushPayload is not working with embedded relations
if (response && isArray(response.users)) {
response.users.forEach((userJSON) => {
let user = this.store.peekRecord('user', userJSON.id);
let role = this.store.peekRecord('role', userJSON.roles[0].id);
user.set('role', role);
});
}
this.get('notifications').showAlert(`Ownership successfully transferred to ${user.get('name')}`, {type: 'success', key: 'owner.transfer.success'});
}).catch((error) => {
this.get('notifications').showAPIError(error, {key: 'owner.transfer'});
});
},
confirmReject() {
return false;
}
}
});

View File

@ -1,22 +1,24 @@
import Ember from 'ember';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
const {Controller, inject} = Ember;
export default Controller.extend({
notifications: inject.service(),
acceptEncoding: 'image/*',
actions: {
confirmAccept: function () {
var notifications = this.get('notifications');
confirmAccept() {
let notifications = this.get('notifications');
this.get('model').save().then(function (model) {
this.get('model').save().then((model) => {
return model;
}).catch(function (err) {
}).catch((err) => {
notifications.showAPIError(err, {key: 'image.upload'});
});
},
confirmReject: function () {
confirmReject() {
return false;
}
}

View File

@ -5,48 +5,48 @@ import SlugGenerator from 'ghost/models/slug-generator';
import boundOneWay from 'ghost/utils/bound-one-way';
import isNumber from 'ghost/utils/isNumber';
export default Ember.Controller.extend(SettingsMenuMixin, {
const {ArrayProxy, Controller, Handlebars, PromiseProxyMixin, RSVP, computed, guidFor, inject, isArray, observer, run} = Ember;
export default Controller.extend(SettingsMenuMixin, {
debounceId: null,
lastPromise: null,
selectedAuthor: null,
uploaderReference: null,
application: Ember.inject.controller(),
config: Ember.inject.service(),
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
application: inject.controller(),
config: inject.service(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
session: inject.service(),
initializeSelectedAuthor: Ember.observer('model', function () {
var self = this;
return this.get('model.author').then(function (author) {
self.set('selectedAuthor', author);
initializeSelectedAuthor: observer('model', function () {
return this.get('model.author').then((author) => {
this.set('selectedAuthor', author);
return author;
});
}),
authors: Ember.computed(function () {
authors: computed(function () {
// Loaded asynchronously, so must use promise proxies.
var deferred = {};
let deferred = {};
deferred.promise = this.store.query('user', {limit: 'all'}).then(function (users) {
deferred.promise = this.store.query('user', {limit: 'all'}).then((users) => {
return users.rejectBy('id', 'me').sortBy('name');
}).then(function (users) {
return users.filter(function (user) {
}).then((users) => {
return users.filter((user) => {
return user.get('active');
});
});
return Ember.ArrayProxy
.extend(Ember.PromiseProxyMixin)
return ArrayProxy
.extend(PromiseProxyMixin)
.create(deferred);
}),
/*jshint unused:false */
publishedAtValue: Ember.computed('model.published_at', {
get: function () {
var pubDate = this.get('model.published_at');
publishedAtValue: computed('model.published_at', {
get() {
let pubDate = this.get('model.published_at');
if (pubDate) {
return formatDate(pubDate);
@ -54,7 +54,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return formatDate(moment());
},
set: function (key, value) {
set(key, value) {
// We're using a fake setter to reset
// the cache for this property
return formatDate(moment());
@ -65,7 +65,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
slugValue: boundOneWay('model.slug'),
// Lazy load the slug generator
slugGenerator: Ember.computed(function () {
slugGenerator: computed(function () {
return SlugGenerator.create({
ghostPaths: this.get('ghostPaths'),
slugType: 'post'
@ -73,21 +73,20 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
}),
// Requests slug from title
generateAndSetSlug: function (destination) {
var self = this,
title = this.get('model.titleScratch'),
afterSave = this.get('lastPromise'),
promise;
generateAndSetSlug(destination) {
let title = this.get('model.titleScratch');
let afterSave = this.get('lastPromise');
let promise;
// Only set an "untitled" slug once per post
if (title === '(Untitled)' && this.get('model.slug')) {
return;
}
promise = Ember.RSVP.resolve(afterSave).then(function () {
return self.get('slugGenerator').generateSlug(title).then(function (slug) {
self.set(destination, slug);
}).catch(function () {
promise = RSVP.resolve(afterSave).then(() => {
return this.get('slugGenerator').generateSlug(title).then((slug) => {
this.set(destination, slug);
}).catch(() => {
// Nothing to do (would be nice to log this somewhere though),
// but a rejected promise needs to be handled here so that a resolved
// promise is returned.
@ -100,25 +99,24 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
metaTitleScratch: boundOneWay('model.meta_title'),
metaDescriptionScratch: boundOneWay('model.meta_description'),
seoTitle: Ember.computed('model.titleScratch', 'metaTitleScratch', function () {
var metaTitle = this.get('metaTitleScratch') || '';
seoTitle: computed('model.titleScratch', 'metaTitleScratch', function () {
let metaTitle = this.get('metaTitleScratch') || '';
metaTitle = metaTitle.length > 0 ? metaTitle : this.get('model.titleScratch');
if (metaTitle.length > 70) {
metaTitle = metaTitle.substring(0, 70).trim();
metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = Ember.String.htmlSafe(metaTitle + '&hellip;');
metaTitle = Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = Ember.String.htmlSafe(`${metaTitle}&hellip;`);
}
return metaTitle;
}),
seoDescription: Ember.computed('model.scratch', 'metaDescriptionScratch', function () {
var metaDescription = this.get('metaDescriptionScratch') || '',
el,
html = '',
placeholder;
seoDescription: computed('model.scratch', 'metaDescriptionScratch', function () {
let metaDescription = this.get('metaDescriptionScratch') || '';
let html = '';
let el, placeholder;
if (metaDescription.length > 0) {
placeholder = metaDescription;
@ -133,27 +131,25 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
}
// Strip HTML
placeholder = $('<div />', {html: html}).text();
placeholder = $('<div />', {html}).text();
// Replace new lines and trim
// jscs: disable
placeholder = placeholder.replace(/\n+/g, ' ').trim();
// jscs: enable
}
if (placeholder.length > 156) {
// Limit to 156 characters
placeholder = placeholder.substring(0, 156).trim();
placeholder = Ember.Handlebars.Utils.escapeExpression(placeholder);
placeholder = Ember.String.htmlSafe(placeholder + '&hellip;');
placeholder = Handlebars.Utils.escapeExpression(placeholder);
placeholder = Ember.String.htmlSafe(`${placeholder}&hellip;`);
}
return placeholder;
}),
seoURL: Ember.computed('model.slug', 'config.blogUrl', function () {
var blogUrl = this.get('config.blogUrl'),
seoSlug = this.get('model.slug') ? this.get('model.slug') : '',
seoURL = blogUrl + '/' + seoSlug;
seoURL: computed('model.slug', 'config.blogUrl', function () {
let blogUrl = this.get('config.blogUrl');
let seoSlug = this.get('model.slug') ? this.get('model.slug') : '';
let seoURL = `${blogUrl}/${seoSlug}`;
// only append a slash to the URL if the slug exists
if (seoSlug) {
@ -162,7 +158,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
if (seoURL.length > 70) {
seoURL = seoURL.substring(0, 70).trim();
seoURL = Ember.String.htmlSafe(seoURL + '&hellip;');
seoURL = Ember.String.htmlSafe(`${seoURL}&hellip;`);
}
return seoURL;
@ -170,61 +166,58 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
// observe titleScratch, keeping the post's slug in sync
// with it until saved for the first time.
addTitleObserver: Ember.observer('model', function () {
addTitleObserver: observer('model', function () {
if (this.get('model.isNew') || this.get('model.title') === '(Untitled)') {
this.addObserver('model.titleScratch', this, 'titleObserver');
}
}),
titleObserver: function () {
var debounceId,
title = this.get('model.title');
titleObserver() {
let title = this.get('model.title');
let debounceId;
// generate a slug if a post is new and doesn't have a title yet or
// if the title is still '(Untitled)' and the slug is unaltered.
if ((this.get('model.isNew') && !title) || title === '(Untitled)') {
debounceId = Ember.run.debounce(this, 'generateAndSetSlug', 'model.slug', 700);
debounceId = run.debounce(this, 'generateAndSetSlug', 'model.slug', 700);
}
this.set('debounceId', debounceId);
},
// live-query of all tags for tag input autocomplete
availableTags: Ember.computed(function () {
return this.get('store').filter('tag', {limit: 'all'}, function () {
availableTags: computed(function () {
return this.get('store').filter('tag', {limit: 'all'}, () => {
return true;
});
}),
showErrors: function (errors) {
errors = Ember.isArray(errors) ? errors : [errors];
showErrors(errors) {
errors = isArray(errors) ? errors : [errors];
this.get('notifications').showErrors(errors);
},
actions: {
discardEnter: function () {
discardEnter() {
return false;
},
togglePage: function () {
var self = this;
togglePage() {
this.toggleProperty('model.page');
// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
if (this.get('model.isNew')) {
return;
}
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
toggleFeatured: function () {
var self = this;
toggleFeatured() {
this.toggleProperty('model.featured');
// If this is a new post. Don't save the model. Defer the save
@ -233,21 +226,19 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return;
}
this.get('model').save(this.get('saveOptions')).catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save(this.get('saveOptions')).catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
/**
* triggered by user manually changing slug
*/
updateSlug: function (newSlug) {
var slug = this.get('model.slug'),
self = this;
updateSlug(newSlug) {
let slug = this.get('model.slug');
newSlug = newSlug || slug;
newSlug = newSlug && newSlug.trim();
// Ignore unchanged slugs or candidate slugs that are empty
@ -258,7 +249,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return;
}
this.get('slugGenerator').generateSlug(newSlug).then(function (serverSlug) {
this.get('slugGenerator').generateSlug(newSlug).then((serverSlug) => {
// If after getting the sanitized and unique slug back from the API
// we end up with a slug that matches the existing slug, abort the change
if (serverSlug === slug) {
@ -272,35 +263,35 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
// get the last token out of the slug candidate and see if it's a number
var slugTokens = serverSlug.split('-'),
check = Number(slugTokens.pop());
let slugTokens = serverSlug.split('-');
let check = Number(slugTokens.pop());
// if the candidate slug is the same as the existing slug except
// for the incrementor then the existing slug should be used
if (isNumber(check) && check > 0) {
if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
self.set('slugValue', slug);
this.set('slugValue', slug);
return;
}
}
self.set('model.slug', serverSlug);
this.set('model.slug', serverSlug);
if (self.hasObserverFor('model.titleScratch')) {
self.removeObserver('model.titleScratch', self, 'titleObserver');
if (this.hasObserverFor('model.titleScratch')) {
this.removeObserver('model.titleScratch', this, 'titleObserver');
}
// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
if (self.get('model.isNew')) {
if (this.get('model.isNew')) {
return;
}
return self.get('model').save();
}).catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
return this.get('model').save();
}).catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
@ -309,11 +300,10 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
* Action sent by post settings menu view.
* (#1351)
*/
setPublishedAt: function (userInput) {
var errMessage = '',
newPublishedAt = parseDateString(userInput),
publishedAt = this.get('model.published_at'),
self = this;
setPublishedAt(userInput) {
let newPublishedAt = parseDateString(userInput);
let publishedAt = this.get('model.published_at');
let errMessage = '';
if (!userInput) {
// Clear out the published_at field for a draft
@ -354,16 +344,16 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return;
}
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
setMetaTitle: function (metaTitle) {
var property = 'meta_title',
model = this.get('model'),
currentTitle = model.get(property) || '';
setMetaTitle(metaTitle) {
let property = 'meta_title';
let model = this.get('model');
let currentTitle = model.get(property) || '';
// Only update if the title has changed
if (currentTitle === metaTitle) {
@ -381,10 +371,10 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
model.save();
},
setMetaDescription: function (metaDescription) {
var property = 'meta_description',
model = this.get('model'),
currentDescription = model.get(property) || '';
setMetaDescription(metaDescription) {
let property = 'meta_description';
let model = this.get('model');
let currentDescription = model.get(property) || '';
// Only update if the description has changed
if (currentDescription === metaDescription) {
@ -402,56 +392,51 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
model.save();
},
setCoverImage: function (image) {
var self = this;
setCoverImage(image) {
this.set('model.image', image);
if (this.get('model.isNew')) {
return;
}
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
clearCoverImage: function () {
var self = this;
clearCoverImage() {
this.set('model.image', '');
if (this.get('model.isNew')) {
return;
}
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
resetUploader: function () {
var uploader = this.get('uploaderReference');
resetUploader() {
let uploader = this.get('uploaderReference');
if (uploader && uploader[0]) {
uploader[0].uploaderUi.reset();
}
},
resetPubDate: function () {
resetPubDate() {
this.set('publishedAtValue', '');
},
closeNavMenu: function () {
closeNavMenu() {
this.get('application').send('closeNavMenu');
},
changeAuthor: function (newAuthor) {
var author = this.get('model.author'),
model = this.get('model'),
self = this;
changeAuthor(newAuthor) {
let author = this.get('model.author');
let model = this.get('model');
// return if nothing changed
if (newAuthor.get('id') === author.get('id')) {
@ -465,19 +450,20 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return;
}
model.save().catch(function (errors) {
self.showErrors(errors);
self.set('selectedAuthor', author);
model.save().catch((errors) => {
this.showErrors(errors);
this.set('selectedAuthor', author);
model.rollbackAttributes();
});
},
addTag: function (tagName, index) {
var self = this,
currentTags = this.get('model.tags'),
currentTagNames = currentTags.map(function (tag) { return tag.get('name').toLowerCase(); }),
availableTagNames = null,
tagToAdd = null;
addTag(tagName, index) {
let currentTags = this.get('model.tags');
let currentTagNames = currentTags.map((tag) => {
return tag.get('name').toLowerCase();
});
let availableTagNames,
tagToAdd;
tagName = tagName.trim();
@ -486,30 +472,34 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return;
}
this.get('availableTags').then(function (availableTags) {
availableTagNames = availableTags.map(function (tag) { return tag.get('name').toLowerCase(); });
this.get('availableTags').then((availableTags) => {
availableTagNames = availableTags.map((tag) => {
return tag.get('name').toLowerCase();
});
// find existing tag or create new
if (availableTagNames.contains(tagName.toLowerCase())) {
tagToAdd = availableTags.find(function (tag) {
tagToAdd = availableTags.find((tag) => {
return tag.get('name').toLowerCase() === tagName.toLowerCase();
});
} else {
tagToAdd = self.get('store').createRecord('tag', {
tagToAdd = this.get('store').createRecord('tag', {
name: tagName
});
// we need to set a UUID so that selectize has a unique value
// it will be ignored when sent to the server
tagToAdd.set('uuid', Ember.guidFor(tagToAdd));
tagToAdd.set('uuid', guidFor(tagToAdd));
}
// push tag onto post relationship
if (tagToAdd) { self.get('model.tags').insertAt(index, tagToAdd); }
if (tagToAdd) {
this.get('model.tags').insertAt(index, tagToAdd);
}
});
},
removeTag: function (tag) {
removeTag(tag) {
this.get('model.tags').removeObject(tag);
if (tag.get('isNew')) {

View File

@ -1,17 +1,20 @@
import Ember from 'ember';
const {Controller, compare, computed} = Ember;
const {equal} = computed;
// a custom sort function is needed in order to sort the posts list the same way the server would:
// status: ASC
// published_at: DESC
// updated_at: DESC
// id: DESC
function comparator(item1, item2) {
var updated1 = item1.get('updated_at'),
updated2 = item2.get('updated_at'),
idResult,
let updated1 = item1.get('updated_at');
let updated2 = item2.get('updated_at');
let idResult,
publishedAtResult,
statusResult,
updatedAtResult,
publishedAtResult;
updatedAtResult;
// when `updated_at` is undefined, the model is still
// being written to with the results from the server
@ -23,9 +26,9 @@ function comparator(item1, item2) {
return 1;
}
idResult = Ember.compare(parseInt(item1.get('id')), parseInt(item2.get('id')));
statusResult = Ember.compare(item1.get('status'), item2.get('status'));
updatedAtResult = Ember.compare(updated1.valueOf(), updated2.valueOf());
idResult = compare(parseInt(item1.get('id')), parseInt(item2.get('id')));
statusResult = compare(item1.get('status'), item2.get('status'));
updatedAtResult = compare(updated1.valueOf(), updated2.valueOf());
publishedAtResult = publishedAtCompare(item1, item2);
if (statusResult === 0) {
@ -45,8 +48,8 @@ function comparator(item1, item2) {
}
function publishedAtCompare(item1, item2) {
var published1 = item1.get('published_at'),
published2 = item2.get('published_at');
let published1 = item1.get('published_at');
let published2 = item2.get('published_at');
if (!published1 && !published2) {
return 0;
@ -60,23 +63,23 @@ function publishedAtCompare(item1, item2) {
return 1;
}
return Ember.compare(published1.valueOf(), published2.valueOf());
return compare(published1.valueOf(), published2.valueOf());
}
export default Ember.Controller.extend({
export default Controller.extend({
// See PostsRoute's shortcuts
postListFocused: Ember.computed.equal('keyboardFocus', 'postList'),
postContentFocused: Ember.computed.equal('keyboardFocus', 'postContent'),
postListFocused: equal('keyboardFocus', 'postList'),
postContentFocused: equal('keyboardFocus', 'postContent'),
sortedPosts: Ember.computed('model.@each.status', 'model.@each.published_at', 'model.@each.isNew', 'model.@each.updated_at', function () {
var postsArray = this.get('model').toArray();
sortedPosts: computed('model.@each.status', 'model.@each.published_at', 'model.@each.isNew', 'model.@each.updated_at', function () {
let postsArray = this.get('model').toArray();
return postsArray.sort(comparator);
}),
actions: {
showPostContent: function (post) {
showPostContent(post) {
if (!post) {
return;
}

View File

@ -2,7 +2,9 @@ import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, computed, inject} = Ember;
export default Controller.extend(ValidationEngine, {
newPassword: '',
ne2Password: '',
token: '',
@ -11,18 +13,18 @@ export default Ember.Controller.extend(ValidationEngine, {
validationType: 'reset',
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
session: inject.service(),
email: Ember.computed('token', function () {
email: computed('token', function () {
// The token base64 encodes the email (and some other stuff),
// each section is divided by a '|'. Email comes second.
return atob(this.get('token')).split('|')[1];
}),
// Used to clear sensitive information
clearData: function () {
clearData() {
this.setProperties({
newPassword: '',
ne2Password: '',
@ -31,34 +33,34 @@ export default Ember.Controller.extend(ValidationEngine, {
},
actions: {
submit: function () {
var credentials = this.getProperties('newPassword', 'ne2Password', 'token'),
self = this;
submit() {
let credentials = this.getProperties('newPassword', 'ne2Password', 'token');
this.set('flowErrors', '');
this.get('hasValidated').addObjects((['newPassword', 'ne2Password']));
this.validate().then(function () {
self.toggleProperty('submitting');
this.validate().then(() => {
this.toggleProperty('submitting');
ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
url: this.get('ghostPaths.url').api('authentication', 'passwordreset'),
type: 'PUT',
data: {
passwordreset: [credentials]
}
}).then(function (resp) {
self.toggleProperty('submitting');
self.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true, key: 'password.reset'});
self.get('session').authenticate('authenticator:oauth2', self.get('email'), credentials.newPassword);
}).catch(function (response) {
self.get('notifications').showAPIError(response, {key: 'password.reset'});
self.toggleProperty('submitting');
}).then((resp) => {
this.toggleProperty('submitting');
this.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true, key: 'password.reset'});
this.get('session').authenticate('authenticator:oauth2', this.get('email'), credentials.newPassword);
}).catch((response) => {
this.get('notifications').showAPIError(response, {key: 'password.reset'});
this.toggleProperty('submitting');
});
}).catch(function () {
if (self.get('errors.newPassword')) {
self.set('flowErrors', self.get('errors.newPassword')[0].message);
}).catch(() => {
if (this.get('errors.newPassword')) {
this.set('flowErrors', this.get('errors.newPassword')[0].message);
}
if (self.get('errors.ne2Password')) {
self.set('flowErrors', self.get('errors.ne2Password')[0].message);
if (this.get('errors.ne2Password')) {
this.set('flowErrors', this.get('errors.ne2Password')[0].message);
}
});
}

View File

@ -1,13 +1,15 @@
import Ember from 'ember';
import SettingsSaveMixin from 'ghost/mixins/settings-save';
export default Ember.Controller.extend(SettingsSaveMixin, {
notifications: Ember.inject.service(),
const {Controller, inject} = Ember;
save: function () {
var notifications = this.get('notifications');
export default Controller.extend(SettingsSaveMixin, {
notifications: inject.service(),
return this.get('model').save().catch(function (error) {
save() {
let notifications = this.get('notifications');
return this.get('model').save().catch((error) => {
notifications.showAPIError(error, {key: 'code-injection.save'});
});
}

View File

@ -2,16 +2,18 @@ import Ember from 'ember';
import SettingsSaveMixin from 'ghost/mixins/settings-save';
import randomPassword from 'ghost/utils/random-password';
export default Ember.Controller.extend(SettingsSaveMixin, {
notifications: Ember.inject.service(),
config: Ember.inject.service(),
const {Controller, computed, inject, observer} = Ember;
selectedTheme: Ember.computed('model.activeTheme', 'themes', function () {
var activeTheme = this.get('model.activeTheme'),
themes = this.get('themes'),
selectedTheme;
export default Controller.extend(SettingsSaveMixin, {
notifications: inject.service(),
config: inject.service(),
themes.forEach(function (theme) {
selectedTheme: computed('model.activeTheme', 'themes', function () {
let activeTheme = this.get('model.activeTheme');
let themes = this.get('themes');
let selectedTheme;
themes.forEach((theme) => {
if (theme.name === activeTheme) {
selectedTheme = theme;
}
@ -20,34 +22,35 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
return selectedTheme;
}),
logoImageSource: Ember.computed('model.logo', function () {
logoImageSource: computed('model.logo', function () {
return this.get('model.logo') || '';
}),
coverImageSource: Ember.computed('model.cover', function () {
coverImageSource: computed('model.cover', function () {
return this.get('model.cover') || '';
}),
isDatedPermalinks: Ember.computed('model.permalinks', {
set: function (key, value) {
isDatedPermalinks: computed('model.permalinks', {
set(key, value) {
this.set('model.permalinks', value ? '/:year/:month/:day/:slug/' : '/:slug/');
var slugForm = this.get('model.permalinks');
let slugForm = this.get('model.permalinks');
return slugForm !== '/:slug/';
},
get: function () {
var slugForm = this.get('model.permalinks');
get() {
let slugForm = this.get('model.permalinks');
return slugForm !== '/:slug/';
}
}),
themes: Ember.computed(function () {
themes: computed(function () {
return this.get('model.availableThemes').reduce(function (themes, t) {
var theme = {};
let theme = {};
theme.name = t.name;
theme.label = t.package ? t.package.name + ' - ' + t.package.version : t.name;
theme.label = t.package ? `${t.package.name} - ${t.package.version}` : t.name;
theme.package = t.package;
theme.active = !!t.active;
@ -57,22 +60,22 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
}, []);
}).readOnly(),
generatePassword: Ember.observer('model.isPrivate', function () {
generatePassword: observer('model.isPrivate', function () {
this.get('model.errors').remove('password');
if (this.get('model.isPrivate') && this.get('model.hasDirtyAttributes')) {
this.get('model').set('password', randomPassword());
}
}),
save: function () {
var notifications = this.get('notifications'),
config = this.get('config');
save() {
let notifications = this.get('notifications');
let config = this.get('config');
return this.get('model').save().then(function (model) {
return this.get('model').save().then((model) => {
config.set('blogTitle', model.get('title'));
return model;
}).catch(function (error) {
}).catch((error) => {
if (error) {
notifications.showAPIError(error, {key: 'settings.save'});
}
@ -80,19 +83,19 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
},
actions: {
validate: function (property) {
this.get('model').validate({property: property});
validate(property) {
this.get('model').validate({property});
},
checkPostsPerPage: function () {
var postsPerPage = this.get('model.postsPerPage');
checkPostsPerPage() {
let postsPerPage = this.get('model.postsPerPage');
if (postsPerPage < 1 || postsPerPage > 1000 || isNaN(postsPerPage)) {
this.set('model.postsPerPage', 5);
}
},
setTheme: function (theme) {
setTheme(theme) {
this.set('model.activeTheme', theme.name);
}
}

View File

@ -1,51 +1,51 @@
import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend({
const {Controller, computed, inject} = Ember;
export default Controller.extend({
uploadButtonText: 'Import',
importErrors: '',
submitting: false,
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
feature: Ember.inject.controller(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
session: inject.service(),
feature: inject.controller(),
labsJSON: Ember.computed('model.labs', function () {
labsJSON: computed('model.labs', function () {
return JSON.parse(this.get('model.labs') || {});
}),
saveLabs: function (optionName, optionValue) {
var self = this,
labsJSON = this.get('labsJSON');
saveLabs(optionName, optionValue) {
let labsJSON = this.get('labsJSON');
// Set new value in the JSON object
labsJSON[optionName] = optionValue;
this.set('model.labs', JSON.stringify(labsJSON));
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollbackAttributes();
this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},
usePublicAPI: Ember.computed('feature.publicAPI', {
get: function () {
usePublicAPI: computed('feature.publicAPI', {
get() {
return this.get('feature.publicAPI');
},
set: function (key, value) {
set(key, value) {
this.saveLabs('publicAPI', value);
return value;
}
}),
actions: {
onUpload: function (file) {
var self = this,
formData = new FormData(),
notifications = this.get('notifications'),
currentUserId = this.get('session.user.id');
onUpload(file) {
let formData = new FormData();
let notifications = this.get('notifications');
let currentUserId = this.get('session.user.id');
this.set('uploadButtonText', 'Importing');
this.set('importErrors', '');
@ -59,29 +59,30 @@ export default Ember.Controller.extend({
cache: false,
contentType: false,
processData: false
}).then(function () {
}).then(() => {
// Clear the store, so that all the new data gets fetched correctly.
self.store.unloadAll();
this.store.unloadAll();
// Reload currentUser and set session
self.set('session.user', self.store.findRecord('user', currentUserId));
this.set('session.user', this.store.findRecord('user', currentUserId));
// TODO: keep as notification, add link to view content
notifications.showNotification('Import successful.');
notifications.closeAlerts('import.upload');
}).catch(function (response) {
}).catch((response) => {
if (response && response.jqXHR && response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) {
self.set('importErrors', response.jqXHR.responseJSON.errors);
this.set('importErrors', response.jqXHR.responseJSON.errors);
}
notifications.showAlert('Import Failed', {type: 'error', key: 'import.upload.failed'});
}).finally(function () {
self.set('uploadButtonText', 'Import');
}).finally(() => {
this.set('uploadButtonText', 'Import');
});
},
exportData: function () {
var iframe = $('#iframeDownload'),
downloadURL = this.get('ghostPaths.url').api('db') +
'?access_token=' + this.get('session.data.authenticated.access_token');
exportData() {
let dbUrl = this.get('ghostPaths.url').api('db');
let accessToken = this.get('session.data.authenticated.access_token');
let downloadURL = `${dbUrl}?access_token=${accessToken}`;
let iframe = $('#iframeDownload');
if (iframe.length === 0) {
iframe = $('<iframe>', {id: 'iframeDownload'}).hide().appendTo('body');
@ -90,24 +91,23 @@ export default Ember.Controller.extend({
iframe.attr('src', downloadURL);
},
sendTestEmail: function () {
var notifications = this.get('notifications'),
self = this;
sendTestEmail() {
let notifications = this.get('notifications');
this.toggleProperty('submitting');
ajax(this.get('ghostPaths.url').api('mail', 'test'), {
type: 'POST'
}).then(function () {
}).then(() => {
notifications.showAlert('Check your email for the test message.', {type: 'info', key: 'test-email.send.success'});
self.toggleProperty('submitting');
}).catch(function (error) {
this.toggleProperty('submitting');
}).catch((error) => {
if (typeof error.jqXHR !== 'undefined') {
notifications.showAPIError(error, {key: 'test-email.send'});
} else {
notifications.showErrors(error, {key: 'test-email.send'});
}
self.toggleProperty('submitting');
this.toggleProperty('submitting');
});
}
}

View File

@ -3,6 +3,10 @@ import DS from 'ember-data';
import SettingsSaveMixin from 'ghost/mixins/settings-save';
import ValidationEngine from 'ghost/mixins/validation-engine';
const {Controller, RSVP, computed, inject, isBlank, observer} = Ember;
const {Errors} = DS;
const emberA = Ember.A;
export const NavItem = Ember.Object.extend(ValidationEngine, {
label: '',
url: '',
@ -10,30 +14,30 @@ export const NavItem = Ember.Object.extend(ValidationEngine, {
validationType: 'navItem',
isComplete: Ember.computed('label', 'url', function () {
return !(Ember.isBlank(this.get('label').trim()) || Ember.isBlank(this.get('url')));
isComplete: computed('label', 'url', function () {
return !(isBlank(this.get('label').trim()) || isBlank(this.get('url')));
}),
init: function () {
init() {
this._super(...arguments);
this.set('errors', DS.Errors.create());
this.set('hasValidated', Ember.A());
this.set('errors', Errors.create());
this.set('hasValidated', emberA());
}
});
export default Ember.Controller.extend(SettingsSaveMixin, {
config: Ember.inject.service(),
notifications: Ember.inject.service(),
export default Controller.extend(SettingsSaveMixin, {
config: inject.service(),
notifications: inject.service(),
blogUrl: Ember.computed('config.blogUrl', function () {
var url = this.get('config.blogUrl');
blogUrl: computed('config.blogUrl', function () {
let url = this.get('config.blogUrl');
return url.slice(-1) !== '/' ? url + '/' : url;
return url.slice(-1) !== '/' ? `${url}/` : url;
}),
navigationItems: Ember.computed('model.navigation', function () {
var navItems,
lastItem;
navigationItems: computed('model.navigation', function () {
let lastItem,
navItems;
try {
navItems = JSON.parse(this.get('model.navigation') || [{}]);
@ -41,7 +45,7 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
navItems = [{}];
}
navItems = navItems.map(function (item) {
navItems = navItems.map((item) => {
return NavItem.create(item);
});
@ -53,10 +57,10 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
return navItems;
}),
updateLastNavItem: Ember.observer('navigationItems.[]', function () {
var navItems = this.get('navigationItems');
updateLastNavItem: observer('navigationItems.[]', function () {
let navItems = this.get('navigationItems');
navItems.forEach(function (item, index, items) {
navItems.forEach((item, index, items) => {
if (index === (items.length - 1)) {
item.set('last', true);
} else {
@ -65,72 +69,72 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
});
}),
save: function () {
var navSetting,
navItems = this.get('navigationItems'),
notifications = this.get('notifications'),
validationPromises,
self = this;
save() {
let navItems = this.get('navigationItems');
let notifications = this.get('notifications');
let navSetting,
validationPromises;
validationPromises = navItems.map(function (item) {
validationPromises = navItems.map((item) => {
return item.validate();
});
return Ember.RSVP.all(validationPromises).then(function () {
navSetting = navItems.map(function (item) {
var label = item.get('label').trim(),
url = item.get('url').trim();
return RSVP.all(validationPromises).then(() => {
navSetting = navItems.map((item) => {
let label = item.get('label').trim();
let url = item.get('url').trim();
if (item.get('last') && !item.get('isComplete')) {
return null;
}
return {label: label, url: url};
return {label, url};
}).compact();
self.set('model.navigation', JSON.stringify(navSetting));
this.set('model.navigation', JSON.stringify(navSetting));
// trigger change event because even if the final JSON is unchanged
// we need to have navigationItems recomputed.
self.get('model').notifyPropertyChange('navigation');
this.get('model').notifyPropertyChange('navigation');
return self.get('model').save().catch(function (err) {
return this.get('model').save().catch((err) => {
notifications.showErrors(err);
});
}).catch(function () {
}).catch(() => {
// TODO: noop - needed to satisfy spinner button
});
},
actions: {
addItem: function () {
var navItems = this.get('navigationItems'),
lastItem = navItems.get('lastObject');
addItem() {
let navItems = this.get('navigationItems');
let lastItem = navItems.get('lastObject');
if (lastItem && lastItem.get('isComplete')) {
navItems.addObject(NavItem.create({last: true})); // Adds new blank navItem
// Add new blank navItem
navItems.addObject(NavItem.create({last: true}));
}
},
deleteItem: function (item) {
deleteItem(item) {
if (!item) {
return;
}
var navItems = this.get('navigationItems');
let navItems = this.get('navigationItems');
navItems.removeObject(item);
},
moveItem: function (index, newIndex) {
var navItems = this.get('navigationItems'),
item = navItems.objectAt(index);
moveItem(index, newIndex) {
let navItems = this.get('navigationItems');
let item = navItems.objectAt(index);
navItems.removeAt(index);
navItems.insertAt(newIndex, item);
},
updateUrl: function (url, navItem) {
updateUrl(url, navItem) {
if (!navItem) {
return;
}

View File

@ -1,7 +1,7 @@
import Ember from 'ember';
const {computed, inject} = Ember,
{alias, equal, sort} = computed;
const {computed, inject} = Ember;
const {alias, equal, sort} = computed;
export default Ember.Controller.extend({
@ -14,8 +14,8 @@ export default Ember.Controller.extend({
// TODO: replace with ordering by page count once supported by the API
tags: sort('model', function (a, b) {
const idA = +a.get('id'),
idB = +b.get('id');
let idA = +a.get('id');
let idB = +b.get('id');
if (idA > idB) {
return 1;
@ -27,7 +27,7 @@ export default Ember.Controller.extend({
}),
actions: {
leftMobile: function () {
leftMobile() {
let firstTag = this.get('tags.firstObject');
// redirect to first tag if possible so that you're not left with
// tag settings blank slate when switching from portrait to landscape

View File

@ -1,9 +1,9 @@
import Ember from 'ember';
const {computed, inject} = Ember,
{alias} = computed;
const {Controller, computed, inject} = Ember;
const {alias} = computed;
export default Ember.Controller.extend({
export default Controller.extend({
tag: alias('model'),
isMobile: alias('tagsController.isMobile'),
@ -11,9 +11,9 @@ export default Ember.Controller.extend({
tagsController: inject.controller('settings.tags'),
notifications: inject.service(),
saveTagProperty: function (propKey, newValue) {
const tag = this.get('tag'),
currentValue = tag.get(propKey);
saveTagProperty(propKey, newValue) {
let tag = this.get('tag');
let currentValue = tag.get(propKey);
newValue = newValue.trim();
@ -37,7 +37,7 @@ export default Ember.Controller.extend({
},
actions: {
setProperty: function (propKey, value) {
setProperty(propKey, value) {
this.saveTagProperty(propKey, value);
}
}

View File

@ -1,14 +1,17 @@
import Ember from 'ember';
export default Ember.Controller.extend({
appController: Ember.inject.controller('application'),
ghostPaths: Ember.inject.service('ghost-paths'),
const {Controller, computed, get, inject} = Ember;
const {match} = computed;
showBackLink: Ember.computed.match('appController.currentRouteName', /^setup\.(two|three)$/),
export default Controller.extend({
appController: inject.controller('application'),
ghostPaths: inject.service('ghost-paths'),
backRoute: Ember.computed('appController.currentRouteName', function () {
var appController = this.get('appController'),
currentRoute = Ember.get(appController, 'currentRouteName');
showBackLink: match('appController.currentRouteName', /^setup\.(two|three)$/),
backRoute: computed('appController.currentRouteName', function () {
let appController = this.get('appController');
let currentRoute = get(appController, 'currentRouteName');
return currentRoute === 'setup.two' ? 'setup.one' : 'setup.two';
})

View File

@ -1,19 +1,24 @@
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
two: Ember.inject.controller('setup/two'),
const {Controller, RSVP, computed, inject} = Ember;
const {Errors} = DS;
const {alias} = computed;
const emberA = Ember.A;
errors: DS.Errors.create(),
hasValidated: Ember.A(),
export default Controller.extend({
notifications: inject.service(),
two: inject.controller('setup/two'),
errors: Errors.create(),
hasValidated: emberA(),
users: '',
ownerEmail: Ember.computed.alias('two.email'),
ownerEmail: alias('two.email'),
submitting: false,
usersArray: Ember.computed('users', function () {
var errors = this.get('errors'),
users = this.get('users').split('\n').filter(function (email) {
usersArray: computed('users', function () {
let errors = this.get('errors');
let users = this.get('users').split('\n').filter(function (email) {
return email.trim().length > 0;
});
@ -27,28 +32,28 @@ export default Ember.Controller.extend({
return users.uniq();
}),
validUsersArray: Ember.computed('usersArray', 'ownerEmail', function () {
var ownerEmail = this.get('ownerEmail');
validUsersArray: computed('usersArray', 'ownerEmail', function () {
let ownerEmail = this.get('ownerEmail');
return this.get('usersArray').filter(function (user) {
return validator.isEmail(user) && user !== ownerEmail;
});
}),
invalidUsersArray: Ember.computed('usersArray', 'ownerEmail', function () {
var ownerEmail = this.get('ownerEmail');
invalidUsersArray: computed('usersArray', 'ownerEmail', function () {
let ownerEmail = this.get('ownerEmail');
return this.get('usersArray').reject(function (user) {
return this.get('usersArray').reject((user) => {
return validator.isEmail(user) || user === ownerEmail;
});
}),
validationResult: Ember.computed('invalidUsersArray', function () {
var errors = [];
validationResult: computed('invalidUsersArray', function () {
let errors = [];
this.get('invalidUsersArray').forEach(function (user) {
this.get('invalidUsersArray').forEach((user) => {
errors.push({
user: user,
user,
error: 'email'
});
});
@ -62,34 +67,36 @@ export default Ember.Controller.extend({
}
}),
validate: function () {
var errors = this.get('errors'),
validationResult = this.get('validationResult'),
property = 'users';
validate() {
let errors = this.get('errors');
let validationResult = this.get('validationResult');
let property = 'users';
errors.clear();
// If property isn't in the `hasValidated` array, add it to mark that this field can show a validation result
this.get('hasValidated').addObject(property);
if (validationResult === true) { return true; }
if (validationResult === true) {
return true;
}
validationResult.forEach(function (error) {
validationResult.forEach((error) => {
// Only one error type here so far, but one day the errors might be more detailed
switch (error.error) {
case 'email':
errors.add(property, error.user + ' is not a valid email.');
errors.add(property, `${error.user} is not a valid email.`);
}
});
return false;
},
buttonText: Ember.computed('errors.users', 'validUsersArray', 'invalidUsersArray', function () {
var usersError = this.get('errors.users.firstObject.message'),
validNum = this.get('validUsersArray').length,
invalidNum = this.get('invalidUsersArray').length,
userCount;
buttonText: computed('errors.users', 'validUsersArray', 'invalidUsersArray', function () {
let usersError = this.get('errors.users.firstObject.message');
let validNum = this.get('validUsersArray').length;
let invalidNum = this.get('invalidUsersArray').length;
let userCount;
if (usersError && usersError.match(/no users/i)) {
return usersError;
@ -102,15 +109,15 @@ export default Ember.Controller.extend({
if (validNum > 0) {
userCount = validNum === 1 ? 'user' : 'users';
userCount = validNum + ' ' + userCount;
userCount = `${validNum} ${userCount}`;
} else {
userCount = 'some users';
}
return 'Invite ' + userCount;
return `Invite ${userCount}`;
}),
buttonClass: Ember.computed('validationResult', 'usersArray.length', function () {
buttonClass: computed('validationResult', 'usersArray.length', function () {
if (this.get('validationResult') === true && this.get('usersArray.length') > 0) {
return 'btn-green';
} else {
@ -118,52 +125,51 @@ export default Ember.Controller.extend({
}
}),
authorRole: Ember.computed(function () {
return this.store.findAll('role', {reload: true}).then(function (roles) {
authorRole: computed(function () {
return this.store.findAll('role', {reload: true}).then((roles) => {
return roles.findBy('name', 'Author');
});
}),
actions: {
validate: function () {
validate() {
this.validate();
},
invite: function () {
var self = this,
users = this.get('usersArray'),
notifications = this.get('notifications'),
invitationsString;
invite() {
let users = this.get('usersArray');
let notifications = this.get('notifications');
let invitationsString;
if (this.validate() && users.length > 0) {
this.toggleProperty('submitting');
this.get('authorRole').then(function (authorRole) {
Ember.RSVP.Promise.all(
users.map(function (user) {
var newUser = self.store.createRecord('user', {
this.get('authorRole').then((authorRole) => {
RSVP.Promise.all(
users.map((user) => {
let newUser = this.store.createRecord('user', {
email: user,
status: 'invited',
role: authorRole
});
return newUser.save().then(function () {
return newUser.save().then(() => {
return {
email: user,
success: newUser.get('status') === 'invited'
};
}).catch(function () {
}).catch(() => {
return {
email: user,
success: false
};
});
})
).then(function (invites) {
var successCount = 0,
erroredEmails = [],
message;
).then((invites) => {
let erroredEmails = [];
let successCount = 0;
let message;
invites.forEach(function (invite) {
invites.forEach((invite) => {
if (invite.success) {
successCount++;
} else {
@ -173,7 +179,7 @@ export default Ember.Controller.extend({
if (erroredEmails.length > 0) {
invitationsString = erroredEmails.length > 1 ? ' invitations: ' : ' invitation: ';
message = 'Failed to send ' + erroredEmails.length + invitationsString;
message = `Failed to send ${erroredEmails.length} ${invitationsString}`;
message += erroredEmails.join(', ');
notifications.showAlert(message, {type: 'error', delayed: successCount > 0, key: 'signup.send-invitations.failed'});
}
@ -181,11 +187,11 @@ export default Ember.Controller.extend({
if (successCount > 0) {
// pluralize
invitationsString = successCount > 1 ? 'invitations' : 'invitation';
notifications.showAlert(successCount + ' ' + invitationsString + ' sent!', {type: 'success', delayed: true, key: 'signup.send-invitations.success'});
notifications.showAlert(`${successCount} ${invitationsString} sent!`, {type: 'success', delayed: true, key: 'signup.send-invitations.success'});
}
self.send('loadServerNotifications');
self.toggleProperty('submitting');
self.transitionToRoute('posts.index');
this.send('loadServerNotifications');
this.toggleProperty('submitting');
this.transitionToRoute('posts.index');
});
});
} else if (users.length === 0) {
@ -193,7 +199,7 @@ export default Ember.Controller.extend({
}
},
skipInvite: function () {
skipInvite() {
this.send('loadServerNotifications');
this.transitionToRoute('posts.index');
}

View File

@ -2,7 +2,9 @@ import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, RSVP, inject} = Ember;
export default Controller.extend(ValidationEngine, {
size: 90,
blogTitle: null,
name: null,
@ -13,11 +15,11 @@ export default Ember.Controller.extend(ValidationEngine, {
submitting: false,
flowErrors: '',
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
application: Ember.inject.controller(),
config: Ember.inject.service(),
session: Ember.inject.service(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
application: inject.controller(),
config: inject.service(),
session: inject.service(),
// ValidationEngine settings
validationType: 'setup',
@ -27,17 +29,16 @@ export default Ember.Controller.extend(ValidationEngine, {
* @param {Object} user User object, returned from the 'setup' api call
* @return {Ember.RSVP.Promise} A promise that takes care of both calls
*/
sendImage: function (user) {
var self = this,
image = this.get('image');
sendImage(user) {
let image = this.get('image');
return new Ember.RSVP.Promise(function (resolve, reject) {
return new RSVP.Promise((resolve, reject) => {
image.formData = {};
image.submit()
.success(function (response) {
user.image = response;
ajax({
url: self.get('ghostPaths.url').api('users', user.id.toString()),
url: this.get('ghostPaths.url').api('users', user.id.toString()),
type: 'PUT',
data: {
users: [user]
@ -48,7 +49,7 @@ export default Ember.Controller.extend(ValidationEngine, {
});
},
_handleSaveError: function (resp) {
_handleSaveError(resp) {
this.toggleProperty('submitting');
if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) {
this.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message);
@ -57,7 +58,7 @@ export default Ember.Controller.extend(ValidationEngine, {
}
},
_handleAuthenticationError: function (error) {
_handleAuthenticationError(error) {
this.toggleProperty('submitting');
if (error && error.errors) {
this.set('flowErrors', error.errors[0].message);
@ -68,28 +69,27 @@ export default Ember.Controller.extend(ValidationEngine, {
},
actions: {
preValidate: function (model) {
preValidate(model) {
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
if (this.get(model)) {
this.validate({property: model});
}
},
setup: function () {
var self = this,
setupProperties = ['blogTitle', 'name', 'email', 'password', 'image'],
data = self.getProperties(setupProperties),
notifications = this.get('notifications'),
config = this.get('config'),
method = this.get('blogCreated') ? 'PUT' : 'POST';
setup() {
let setupProperties = ['blogTitle', 'name', 'email', 'password', 'image'];
let data = this.getProperties(setupProperties);
let notifications = this.get('notifications');
let config = this.get('config');
let method = this.get('blogCreated') ? 'PUT' : 'POST';
this.toggleProperty('submitting');
this.set('flowErrors', '');
this.get('hasValidated').addObjects(setupProperties);
this.validate().then(function () {
this.validate().then(() => {
ajax({
url: self.get('ghostPaths.url').api('authentication', 'setup'),
url: this.get('ghostPaths.url').api('authentication', 'setup'),
type: method,
data: {
setup: [{
@ -99,37 +99,38 @@ export default Ember.Controller.extend(ValidationEngine, {
blogTitle: data.blogTitle
}]
}
}).then(function (result) {
}).then((result) => {
config.set('blogTitle', data.blogTitle);
// Don't call the success handler, otherwise we will be redirected to admin
self.get('application').set('skipAuthSuccessHandler', true);
self.get('session').authenticate('authenticator:oauth2', self.get('email'), self.get('password')).then(function () {
self.set('blogCreated', true);
this.get('application').set('skipAuthSuccessHandler', true);
this.get('session').authenticate('authenticator:oauth2', this.get('email'), this.get('password')).then(() => {
this.set('blogCreated', true);
if (data.image) {
self.sendImage(result.users[0])
.then(function () {
self.toggleProperty('submitting');
self.transitionToRoute('setup.three');
}).catch(function (resp) {
self.toggleProperty('submitting');
this.sendImage(result.users[0])
.then(() => {
this.toggleProperty('submitting');
this.transitionToRoute('setup.three');
}).catch((resp) => {
this.toggleProperty('submitting');
notifications.showAPIError(resp, {key: 'setup.blog-details'});
});
} else {
self.toggleProperty('submitting');
self.transitionToRoute('setup.three');
this.toggleProperty('submitting');
this.transitionToRoute('setup.three');
}
}).catch(function (error) {
self._handleAuthenticationError(error);
}).catch((error) => {
this._handleAuthenticationError(error);
});
}).catch(function (error) {
self._handleSaveError(error);
}).catch((error) => {
this._handleSaveError(error);
});
}).catch(function () {
self.toggleProperty('submitting');
self.set('flowErrors', 'Please fill out the form to setup your blog.');
}).catch(() => {
this.toggleProperty('submitting');
this.set('flowErrors', 'Please fill out the form to setup your blog.');
});
},
setImage: function (image) {
setImage(image) {
this.set('image', image);
}
}

View File

@ -2,53 +2,53 @@ import Ember from 'ember';
import ValidationEngine from 'ghost/mixins/validation-engine';
import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, inject} = Ember;
export default Controller.extend(ValidationEngine, {
submitting: false,
loggingIn: false,
authProperties: ['identification', 'password'],
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
application: Ember.inject.controller(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
session: inject.service(),
application: inject.controller(),
flowErrors: '',
// ValidationEngine settings
validationType: 'signin',
actions: {
authenticate: function () {
var self = this,
model = this.get('model'),
authStrategy = 'authenticator:oauth2';
authenticate() {
let model = this.get('model');
let authStrategy = 'authenticator:oauth2';
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
this.get('session').authenticate(authStrategy, model.get('identification'), model.get('password')).catch(function (error) {
self.toggleProperty('loggingIn');
this.get('session').authenticate(authStrategy, model.get('identification'), model.get('password')).catch((error) => {
this.toggleProperty('loggingIn');
if (error && error.errors) {
error.errors.forEach(function (err) {
error.errors.forEach((err) => {
err.message = err.message.htmlSafe();
});
self.set('flowErrors', error.errors[0].message.string);
this.set('flowErrors', error.errors[0].message.string);
if (error.errors[0].message.string.match(/user with that email/)) {
self.get('model.errors').add('identification', '');
this.get('model.errors').add('identification', '');
}
if (error.errors[0].message.string.match(/password is incorrect/)) {
self.get('model.errors').add('password', '');
this.get('model.errors').add('password', '');
}
} else {
// Connection errors don't return proper status message, only req.body
self.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
}
});
},
validateAndAuthenticate: function () {
var self = this;
validateAndAuthenticate() {
this.set('flowErrors', '');
// Manually trigger events for input fields, ensuring legacy compatibility with
// browsers and password managers that don't send proper events on autofill
@ -56,56 +56,54 @@ export default Ember.Controller.extend(ValidationEngine, {
// This is a bit dirty, but there's no other way to ensure the properties are set as well as 'signin'
this.get('hasValidated').addObjects(this.authProperties);
this.validate({property: 'signin'}).then(function () {
self.toggleProperty('loggingIn');
self.send('authenticate');
}).catch(function (error) {
this.validate({property: 'signin'}).then(() => {
this.toggleProperty('loggingIn');
this.send('authenticate');
}).catch((error) => {
if (error) {
self.get('notifications').showAPIError(error, {key: 'signin.authenticate'});
this.get('notifications').showAPIError(error, {key: 'signin.authenticate'});
} else {
self.set('flowErrors', 'Please fill out the form to sign in.');
this.set('flowErrors', 'Please fill out the form to sign in.');
}
});
},
forgotten: function () {
var email = this.get('model.identification'),
notifications = this.get('notifications'),
self = this;
forgotten() {
let email = this.get('model.identification');
let notifications = this.get('notifications');
this.set('flowErrors', '');
// This is a bit dirty, but there's no other way to ensure the properties are set as well as 'forgotPassword'
this.get('hasValidated').addObject('identification');
this.validate({property: 'forgotPassword'}).then(function () {
self.toggleProperty('submitting');
this.validate({property: 'forgotPassword'}).then(() => {
this.toggleProperty('submitting');
ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
url: this.get('ghostPaths.url').api('authentication', 'passwordreset'),
type: 'POST',
data: {
passwordreset: [{
email: email
}]
passwordreset: [{email}]
}
}).then(function () {
self.toggleProperty('submitting');
}).then(() => {
this.toggleProperty('submitting');
notifications.showAlert('Please check your email for instructions.', {type: 'info', key: 'forgot-password.send.success'});
}).catch(function (resp) {
self.toggleProperty('submitting');
}).catch((resp) => {
this.toggleProperty('submitting');
if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) {
var message = resp.jqXHR.responseJSON.errors[0].message;
let [error] = resp.jqXHR.responseJSON.errors;
let {message} = error;
self.set('flowErrors', message);
this.set('flowErrors', message);
if (message.match(/no user with that email/)) {
self.get('model.errors').add('identification', '');
this.get('model.errors').add('identification', '');
}
} else {
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.', key: 'forgot-password.send'});
}
});
}).catch(function () {
self.set('flowErrors', 'We need your email address to reset your password!');
}).catch(() => {
this.set('flowErrors', 'We need your email address to reset your password!');
});
}
}

View File

@ -2,7 +2,9 @@ import Ember from 'ember';
import {request as ajax} from 'ic-ajax';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, RSVP, inject} = Ember;
export default Controller.extend(ValidationEngine, {
// ValidationEngine settings
validationType: 'signup',
@ -10,23 +12,22 @@ export default Ember.Controller.extend(ValidationEngine, {
flowErrors: '',
image: null,
ghostPaths: Ember.inject.service('ghost-paths'),
config: Ember.inject.service(),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
ghostPaths: inject.service('ghost-paths'),
config: inject.service(),
notifications: inject.service(),
session: inject.service(),
sendImage: function () {
var self = this,
image = this.get('image');
sendImage() {
let image = this.get('image');
this.get('session.user').then(function (user) {
return new Ember.RSVP.Promise(function (resolve, reject) {
this.get('session.user').then((user) => {
return new RSVP.Promise((resolve, reject) => {
image.formData = {};
image.submit()
.success(function (response) {
.success((response) => {
user.image = response;
ajax({
url: self.get('ghostPaths.url').api('users', user.id.toString()),
url: this.get('ghostPaths.url').api('users', user.id.toString()),
type: 'PUT',
data: {
users: [user]
@ -39,22 +40,20 @@ export default Ember.Controller.extend(ValidationEngine, {
},
actions: {
signup: function () {
var self = this,
model = this.get('model'),
setupProperties = ['name', 'email', 'password', 'token'],
data = model.getProperties(setupProperties),
image = this.get('image'),
notifications = this.get('notifications');
signup() {
let model = this.get('model');
let setupProperties = ['name', 'email', 'password', 'token'];
let data = model.getProperties(setupProperties);
let image = this.get('image');
let notifications = this.get('notifications');
this.set('flowErrors', '');
this.get('hasValidated').addObjects(setupProperties);
this.validate().then(function () {
self.toggleProperty('submitting');
this.validate().then(() => {
this.toggleProperty('submitting');
ajax({
url: self.get('ghostPaths.url').api('authentication', 'invitation'),
url: this.get('ghostPaths.url').api('authentication', 'invitation'),
type: 'POST',
dataType: 'json',
data: {
@ -65,27 +64,28 @@ export default Ember.Controller.extend(ValidationEngine, {
token: data.token
}]
}
}).then(function () {
self.get('session').authenticate('authenticator:oauth2', self.get('model.email'), self.get('model.password')).then(function () {
}).then(() => {
this.get('session').authenticate('authenticator:oauth2', this.get('model.email'), this.get('model.password')).then(() => {
if (image) {
self.sendImage();
this.sendImage();
}
}).catch(function (resp) {
}).catch((resp) => {
notifications.showAPIError(resp, {key: 'signup.complete'});
});
}).catch(function (resp) {
self.toggleProperty('submitting');
}).catch((resp) => {
this.toggleProperty('submitting');
if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) {
self.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message);
this.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message);
} else {
notifications.showAPIError(resp, {key: 'signup.complete'});
}
});
}).catch(function () {
self.set('flowErrors', 'Please fill out the form to complete your sign-up');
}).catch(() => {
this.set('flowErrors', 'Please fill out the form to complete your sign-up');
});
},
setImage: function (image) {
setImage(image) {
this.set('image', image);
}
}

View File

@ -1,17 +1,20 @@
import Ember from 'ember';
export default Ember.Controller.extend({
const {Controller, computed, inject} = Ember;
const {alias, filter} = computed;
session: Ember.inject.service(),
export default Controller.extend({
users: Ember.computed.alias('model'),
session: inject.service(),
activeUsers: Ember.computed.filter('users', function (user) {
users: alias('model'),
activeUsers: filter('users', function (user) {
return /^active|warn-[1-4]|locked$/.test(user.get('status'));
}),
invitedUsers: Ember.computed.filter('users', function (user) {
var status = user.get('status');
invitedUsers: filter('users', function (user) {
let status = user.get('status');
return status === 'invited' || status === 'invited-pending';
})

View File

@ -4,32 +4,40 @@ import isNumber from 'ghost/utils/isNumber';
import boundOneWay from 'ghost/utils/bound-one-way';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
const {Controller, RSVP, computed, inject} = Ember;
const {alias, and, not, or, readOnly} = computed;
export default Controller.extend(ValidationEngine, {
// ValidationEngine settings
validationType: 'user',
submitting: false,
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
session: Ember.inject.service(),
ghostPaths: inject.service('ghost-paths'),
notifications: inject.service(),
session: inject.service(),
currentUser: Ember.computed.alias('session.user'),
lastPromise: null,
isNotOwnProfile: Ember.computed('user.id', 'currentUser.id', function () {
currentUser: alias('session.user'),
user: alias('model'),
email: readOnly('user.email'),
slugValue: boundOneWay('user.slug'),
isNotOwnProfile: computed('user.id', 'currentUser.id', function () {
return this.get('user.id') !== this.get('currentUser.id');
}),
isNotOwnersProfile: Ember.computed.not('user.isOwner'),
isNotOwnersProfile: not('user.isOwner'),
isAdminUserOnOwnerProfile: Ember.computed.and('currentUser.isAdmin', 'user.isOwner'),
isAdminUserOnOwnerProfile: and('currentUser.isAdmin', 'user.isOwner'),
canAssignRoles: Ember.computed.or('currentUser.isAdmin', 'currentUser.isOwner'),
canAssignRoles: or('currentUser.isAdmin', 'currentUser.isOwner'),
canMakeOwner: Ember.computed.and('currentUser.isOwner', 'isNotOwnProfile', 'user.isAdmin'),
canMakeOwner: and('currentUser.isOwner', 'isNotOwnProfile', 'user.isAdmin'),
rolesDropdownIsVisible: Ember.computed.and('isNotOwnProfile', 'canAssignRoles', 'isNotOwnersProfile'),
rolesDropdownIsVisible: and('isNotOwnProfile', 'canAssignRoles', 'isNotOwnersProfile'),
deleteUserActionIsVisible: Ember.computed('currentUser', 'canAssignRoles', 'user', function () {
deleteUserActionIsVisible: computed('currentUser', 'canAssignRoles', 'user', function () {
if ((this.get('canAssignRoles') && this.get('isNotOwnProfile') && !this.get('user.isOwner')) ||
(this.get('currentUser.isEditor') && (this.get('isNotOwnProfile') ||
this.get('user.isAuthor')))) {
@ -37,68 +45,57 @@ export default Ember.Controller.extend(ValidationEngine, {
}
}),
userActionsAreVisible: Ember.computed.or('deleteUserActionIsVisible', 'canMakeOwner'),
user: Ember.computed.alias('model'),
email: Ember.computed.readOnly('model.email'),
slugValue: boundOneWay('model.slug'),
lastPromise: null,
userActionsAreVisible: or('deleteUserActionIsVisible', 'canMakeOwner'),
// duplicated in gh-user-active -- find a better home and consolidate?
userDefault: Ember.computed('ghostPaths', function () {
userDefault: computed('ghostPaths', function () {
return this.get('ghostPaths.url').asset('/shared/img/user-image.png');
}),
userImageBackground: Ember.computed('user.image', 'userDefault', function () {
var url = this.get('user.image') || this.get('userDefault');
userImageBackground: computed('user.image', 'userDefault', function () {
let url = this.get('user.image') || this.get('userDefault');
return Ember.String.htmlSafe(`background-image: url(${url})`);
}),
// end duplicated
coverDefault: Ember.computed('ghostPaths', function () {
coverDefault: computed('ghostPaths', function () {
return this.get('ghostPaths.url').asset('/shared/img/user-cover.png');
}),
coverImageBackground: Ember.computed('user.cover', 'coverDefault', function () {
var url = this.get('user.cover') || this.get('coverDefault');
coverImageBackground: computed('user.cover', 'coverDefault', function () {
let url = this.get('user.cover') || this.get('coverDefault');
return Ember.String.htmlSafe(`background-image: url(${url})`);
}),
coverTitle: Ember.computed('user.name', function () {
return this.get('user.name') + '\'s Cover Image';
coverTitle: computed('user.name', function () {
return `${this.get('user.name')}'s Cover Image`;
}),
// Lazy load the slug generator for slugPlaceholder
slugGenerator: Ember.computed(function () {
slugGenerator: computed(function () {
return SlugGenerator.create({
ghostPaths: this.get('ghostPaths'),
slugType: 'user'
});
}),
roles: Ember.computed(function () {
roles: computed(function () {
return this.store.query('role', {permissions: 'assign'});
}),
actions: {
changeRole: function (newRole) {
changeRole(newRole) {
this.set('model.role', newRole);
},
save: function () {
var user = this.get('user'),
slugValue = this.get('slugValue'),
afterUpdateSlug = this.get('lastPromise'),
promise,
slugChanged,
self = this;
save() {
let user = this.get('user');
let slugValue = this.get('slugValue');
let afterUpdateSlug = this.get('lastPromise');
let promise,
slugChanged;
if (user.get('slug') !== slugValue) {
slugChanged = true;
@ -107,10 +104,10 @@ export default Ember.Controller.extend(ValidationEngine, {
this.toggleProperty('submitting');
promise = Ember.RSVP.resolve(afterUpdateSlug).then(function () {
promise = RSVP.resolve(afterUpdateSlug).then(() => {
return user.save({format: false});
}).then(function (model) {
var currentPath,
}).then((model) => {
let currentPath,
newPath;
// If the user's slug has changed, change the URL and replace
@ -125,27 +122,26 @@ export default Ember.Controller.extend(ValidationEngine, {
window.history.replaceState({path: newPath}, '', newPath);
}
self.toggleProperty('submitting');
self.get('notifications').closeAlerts('user.update');
this.toggleProperty('submitting');
this.get('notifications').closeAlerts('user.update');
return model;
}).catch(function (errors) {
}).catch((errors) => {
if (errors) {
self.get('notifications').showErrors(errors, {key: 'user.update'});
this.get('notifications').showErrors(errors, {key: 'user.update'});
}
self.toggleProperty('submitting');
this.toggleProperty('submitting');
});
this.set('lastPromise', promise);
},
password: function () {
var user = this.get('user'),
self = this;
password() {
let user = this.get('user');
if (user.get('isPasswordValid')) {
user.saveNewPassword().then(function (model) {
user.saveNewPassword().then((model) => {
// Clear properties from view
user.setProperties({
password: '',
@ -153,38 +149,36 @@ export default Ember.Controller.extend(ValidationEngine, {
ne2Password: ''
});
self.get('notifications').showAlert('Password updated.', {type: 'success', key: 'user.change-password.success'});
this.get('notifications').showAlert('Password updated.', {type: 'success', key: 'user.change-password.success'});
return model;
}).catch(function (errors) {
self.get('notifications').showAPIError(errors, {key: 'user.change-password'});
}).catch((errors) => {
this.get('notifications').showAPIError(errors, {key: 'user.change-password'});
});
} else {
// TODO: switch to in-line validation
self.get('notifications').showErrors(user.get('passwordValidationErrors'), {key: 'user.change-password'});
this.get('notifications').showErrors(user.get('passwordValidationErrors'), {key: 'user.change-password'});
}
},
updateSlug: function (newSlug) {
var self = this,
afterSave = this.get('lastPromise'),
promise;
updateSlug(newSlug) {
let afterSave = this.get('lastPromise');
let promise;
promise = Ember.RSVP.resolve(afterSave).then(function () {
var slug = self.get('model.slug');
promise = RSVP.resolve(afterSave).then(() => {
let slug = this.get('model.slug');
newSlug = newSlug || slug;
newSlug = newSlug.trim();
// Ignore unchanged slugs or candidate slugs that are empty
if (!newSlug || slug === newSlug) {
self.set('slugValue', slug);
this.set('slugValue', slug);
return;
}
return self.get('slugGenerator').generateSlug(newSlug).then(function (serverSlug) {
return this.get('slugGenerator').generateSlug(newSlug).then((serverSlug) => {
// If after getting the sanitized and unique slug back from the API
// we end up with a slug that matches the existing slug, abort the change
if (serverSlug === slug) {
@ -198,20 +192,20 @@ export default Ember.Controller.extend(ValidationEngine, {
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
// get the last token out of the slug candidate and see if it's a number
var slugTokens = serverSlug.split('-'),
check = Number(slugTokens.pop());
let slugTokens = serverSlug.split('-');
let check = Number(slugTokens.pop());
// if the candidate slug is the same as the existing slug except
// for the incrementor then the existing slug should be used
if (isNumber(check) && check > 0) {
if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
self.set('slugValue', slug);
this.set('slugValue', slug);
return;
}
}
self.set('slugValue', serverSlug);
this.set('slugValue', serverSlug);
});
});

View File

@ -1,16 +1,15 @@
import Ember from 'ember';
export default Ember.Helper.helper(function (params) {
var el = document.createElement('span'),
length,
content;
const {Helper} = Ember;
export default Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
content = params[0] || '';
length = content.length;
let el = document.createElement('span');
let content = params[0] || '';
let {length} = content;
el.className = 'word-count';

View File

@ -1,17 +1,17 @@
import Ember from 'ember';
export default Ember.Helper.helper(function (params) {
var el = document.createElement('span'),
content,
maxCharacters,
length;
const {Helper} = Ember;
export default Helper.helper(function (params) {
if (!params || params.length < 2) {
return;
}
content = params[0] || '';
maxCharacters = params[1];
let el = document.createElement('span');
let [content, maxCharacters] = params;
let length;
content = content || '';
length = content.length;
el.className = 'word-count';

View File

@ -1,21 +1,20 @@
import Ember from 'ember';
import counter from 'ghost/utils/word-count';
export default Ember.Helper.helper(function (params) {
const {Helper} = Ember;
export default Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var markdown,
count;
markdown = params[0] || '';
let markdown = params[0] || '';
if (/^\s*$/.test(markdown)) {
return '0 words';
}
count = counter(markdown);
let count = counter(markdown);
return count + (count === 1 ? ' word' : ' words');
});

View File

@ -1,13 +1,15 @@
import Ember from 'ember';
/* global html_sanitize*/
import Ember from 'ember';
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
export default Ember.Helper.helper(function (params) {
const {Helper} = Ember;
export default Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var escapedhtml = params[0] || '';
let escapedhtml = params[0] || '';
// replace script and iFrame
escapedhtml = escapedhtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,

View File

@ -1,16 +1,18 @@
import Ember from 'ember';
/* global Showdown, html_sanitize*/
import Ember from 'ember';
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
var showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
const {Helper} = Ember;
export default Ember.Helper.helper(function (params) {
let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
export default Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var escapedhtml = '',
markdown = params[0] || '';
let markdown = params[0] || '';
let escapedhtml = '';
// convert markdown to HTML
escapedhtml = showdown.makeHtml(markdown);

View File

@ -1,11 +1,13 @@
import Ember from 'ember';
export default Ember.Helper.helper(function (params) {
const {Helper} = Ember;
export default Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var timeago = params[0];
let [timeago] = params;
return moment(timeago).fromNow();
// stefanpenner says cool for small number of timeagos.

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