mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Remove split Ghost-Admin code
This commit is contained in:
parent
72c6cf7f93
commit
1b85d67e0e
514 changed files with 0 additions and 33625 deletions
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": true
|
||||
}
|
20
core/client/.gitignore
vendored
20
core/client/.gitignore
vendored
|
@ -1,20 +0,0 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# misc
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
|
||||
# built by grunt
|
||||
public/assets/img/contributors/
|
||||
app/templates/-contributors.hbs
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"preset": "ember-suave",
|
||||
"validateIndentation": 4,
|
||||
"disallowSpacesInFunction": null,
|
||||
"disallowSpacesInNamedFunctionExpression": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"disallowSpacesInFunctionDeclaration": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"disallowSpacesInsideObjectBrackets": "all",
|
||||
"requireCommentsToIncludeAccess": null,
|
||||
"requireSpacesInsideObjectBrackets": null
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"predef": [
|
||||
"server",
|
||||
"document",
|
||||
"window",
|
||||
"-Promise",
|
||||
"-Notification",
|
||||
"validator",
|
||||
"moment"
|
||||
],
|
||||
"browser": true,
|
||||
"boss": true,
|
||||
"curly": true,
|
||||
"debug": false,
|
||||
"devel": true,
|
||||
"eqeqeq": true,
|
||||
"evil": true,
|
||||
"forin": false,
|
||||
"immed": false,
|
||||
"laxbreak": false,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": false,
|
||||
"nonew": false,
|
||||
"nomen": false,
|
||||
"onevar": false,
|
||||
"plusplus": false,
|
||||
"regexp": false,
|
||||
"undef": true,
|
||||
"sub": true,
|
||||
"strict": false,
|
||||
"white": false,
|
||||
"eqnull": true,
|
||||
"esnext": true,
|
||||
"unused": true
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
# Ghost Admin Client
|
||||
|
||||
Ember.js application used as a client-side admin for the [Ghost](http://ghost.org) blogging platform. This readme is a work in progress guide aimed at explaining the specific nuances of the Ghost Ember app to contributors whose main focus is on this side of things.
|
||||
|
||||
|
||||
## CSS
|
||||
|
||||
We use pure CSS, which is pre-processed for backwards compatibility by [Myth](http://myth.io). We do not follow any strict CSS framework, however our general style is pretty similar to BEM.
|
||||
|
||||
Styles are primarily broken up into 4 main categories:
|
||||
|
||||
* **Patterns** - are base level visual styles for HTML elements (eg. Buttons)
|
||||
* **Components** - are groups of patterns used to create a UI component (eg. Modals)
|
||||
* **Layouts** - are groups of components used to create application screens (eg. Settings)
|
||||
|
||||
All of these separate files are subsequently imported and compiled in `app.css`.
|
||||
|
||||
|
||||
## Front End Standards
|
||||
|
||||
* 4 spaces for HTML & CSS indentation. Never tabs.
|
||||
* Double quotes only, never single quotes.
|
||||
* Use tags and elements appropriate for an HTML5 doctype (including self-closing tags)
|
||||
* Adhere to the [Recess CSS](http://markdotto.com/2011/11/29/css-property-order/) property order.
|
||||
* Always a space after a property's colon (.e.g, display: block; and not display:block;).
|
||||
* End all lines with a semi-colon.
|
||||
* For multiple, comma-separated selectors, place each selector on its own line.
|
||||
* Use js- prefixed classes for JavaScript hooks into the DOM, and never use these in CSS as per [Slightly Obtrusive JavaSript](http://ozmm.org/posts/slightly_obtrusive_javascript.html)
|
||||
* Avoid over-nesting CSS. Never nest more than 3 levels deep.
|
||||
* Use comments to explain "why" not "what" (Good: This requires a z-index in order to appear above mobile navigation. Bad: This is a thing which is always on top!)
|
|
@ -1,15 +0,0 @@
|
|||
# Dependencies
|
||||
markdown: kramdown
|
||||
highlighter: highlighter
|
||||
|
||||
# Permalinks
|
||||
permalink: pretty
|
||||
|
||||
# Server
|
||||
source: docs
|
||||
destination: _gh_pages
|
||||
host: 0.0.0.0
|
||||
port: 9001
|
||||
baseurl:
|
||||
url: http://localhost:9001
|
||||
encoding: UTF-8
|
|
@ -1,9 +0,0 @@
|
|||
import EmbeddedRelationAdapter from 'ghost/adapters/embedded-relation-adapter';
|
||||
|
||||
export default EmbeddedRelationAdapter.extend({
|
||||
|
||||
shouldBackgroundReloadRecord() {
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import RESTAdapter from 'ember-data/adapters/rest';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
|
||||
|
||||
const {
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default RESTAdapter.extend(DataAdapterMixin, {
|
||||
authorizer: 'authorizer:oauth2',
|
||||
|
||||
host: window.location.origin,
|
||||
namespace: ghostPaths().apiRoot.slice(1),
|
||||
|
||||
session: service(),
|
||||
|
||||
shouldBackgroundReloadRecord() {
|
||||
return false;
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
let id;
|
||||
|
||||
if (query.id) {
|
||||
id = query.id;
|
||||
delete query.id;
|
||||
}
|
||||
|
||||
return this.ajax(this.buildURL(type.modelName, id), 'GET', {data: query});
|
||||
},
|
||||
|
||||
buildURL() {
|
||||
// Ensure trailing slashes
|
||||
let url = this._super(...arguments);
|
||||
|
||||
if (url.slice(-1) !== '/') {
|
||||
url += '/';
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
handleResponse(status) {
|
||||
if (status === 401) {
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
this.get('session').invalidate();
|
||||
return; // prevent error from bubbling because invalidate is async
|
||||
}
|
||||
}
|
||||
|
||||
return this._super(...arguments);
|
||||
}
|
||||
});
|
|
@ -1,132 +0,0 @@
|
|||
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.
|
||||
// In cases where there is no query object (DS.Model#save, or simple finds) the URL
|
||||
// that is built will be augmented with ?include=... where appropriate.
|
||||
//
|
||||
// Example:
|
||||
// If a model has an embedded hasMany relation, the related type will be included:
|
||||
// roles: DS.hasMany('role', { embedded: 'always' }) => ?include=roles
|
||||
|
||||
export default BaseAdapter.extend({
|
||||
find(store, type, id, snapshot) {
|
||||
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'find'), 'GET');
|
||||
},
|
||||
|
||||
findRecord(store, type, id, snapshot) {
|
||||
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord'), 'GET');
|
||||
},
|
||||
|
||||
findAll(store, type, sinceToken) {
|
||||
let query, url;
|
||||
|
||||
if (sinceToken) {
|
||||
query = {since: sinceToken};
|
||||
}
|
||||
|
||||
url = this.buildIncludeURL(store, type.modelName, null, null, 'findAll');
|
||||
|
||||
return this.ajax(url, 'GET', {data: query});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this._super(store, type, this.buildQuery(store, type.modelName, query));
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this._super(store, type, this.buildQuery(store, type.modelName, query));
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
return this.saveRecord(store, type, snapshot, {method: 'POST'}, 'createRecord');
|
||||
},
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
let options = {
|
||||
method: 'PUT',
|
||||
id: get(snapshot, 'id')
|
||||
};
|
||||
|
||||
return this.saveRecord(store, type, snapshot, options, 'updateRecord');
|
||||
},
|
||||
|
||||
saveRecord(store, type, snapshot, options, requestType) {
|
||||
let _options = options || {};
|
||||
let url = this.buildIncludeURL(store, type.modelName, _options.id, snapshot, requestType);
|
||||
let payload = this.preparePayload(store, type, snapshot);
|
||||
|
||||
return this.ajax(url, _options.method, payload);
|
||||
},
|
||||
|
||||
preparePayload(store, type, snapshot) {
|
||||
let serializer = store.serializerFor(type.modelName);
|
||||
let payload = {};
|
||||
|
||||
serializer.serializeIntoHash(payload, type, snapshot);
|
||||
|
||||
return {data: payload};
|
||||
},
|
||||
|
||||
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(',')}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
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
|
||||
if (typeof options === 'string' || typeof options === 'number') {
|
||||
query = {};
|
||||
query.id = options;
|
||||
query.include = toInclude.join(',');
|
||||
} 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
|
||||
// into the existing object. Existing properties and includes are preserved.
|
||||
query = query || {};
|
||||
toInclude = toInclude.concat(query.include ? query.include.split(',') : []);
|
||||
|
||||
toInclude.forEach((include) => {
|
||||
deDupe[include] = true;
|
||||
});
|
||||
|
||||
query.include = Object.keys(deDupe).join(',');
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
},
|
||||
|
||||
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
|
||||
model.eachRelationship(function (name, meta) {
|
||||
if (meta.kind === 'hasMany' &&
|
||||
Object.prototype.hasOwnProperty.call(meta.options, 'embedded') &&
|
||||
meta.options.embedded === 'always') {
|
||||
ret.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
import ApplicationAdapter from 'ghost/adapters/application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
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;
|
||||
|
||||
// use the SettingSerializer to transform the model back into
|
||||
// an array of settings objects like the API expects
|
||||
serializer.serializeIntoHash(data, type, record);
|
||||
|
||||
// use the ApplicationAdapter's buildURL method but do not
|
||||
// pass in an id.
|
||||
return this.ajax(this.buildURL(type.modelName), 'PUT', {data});
|
||||
}
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
import ApplicationAdapter from 'ghost/adapters/application';
|
||||
import SlugUrl from 'ghost/mixins/slug-url';
|
||||
|
||||
export default ApplicationAdapter.extend(SlugUrl);
|
|
@ -1,23 +0,0 @@
|
|||
import ApplicationAdapter from 'ghost/adapters/application';
|
||||
import SlugUrl from 'ghost/mixins/slug-url';
|
||||
|
||||
export default ApplicationAdapter.extend(SlugUrl, {
|
||||
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(store, type, id, snapshot) {
|
||||
let url = this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord');
|
||||
|
||||
url += '&status=all';
|
||||
|
||||
return this.ajax(url, 'GET');
|
||||
},
|
||||
|
||||
findAll(store, type, id) {
|
||||
return this.query(store, type, {id, status: 'all'});
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import Resolver from './resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import 'ghost/utils/link-component';
|
||||
import 'ghost/utils/text-field';
|
||||
import config from './config/environment';
|
||||
|
||||
const {Application} = Ember;
|
||||
|
||||
Ember.MODEL_FACTORY_INJECTIONS = true;
|
||||
|
||||
let App = Application.extend({
|
||||
Resolver,
|
||||
modulePrefix: config.modulePrefix,
|
||||
podModulePrefix: config.podModulePrefix
|
||||
});
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
|
@ -1,28 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import Authenticator from 'ember-simple-auth/authenticators/oauth2-password-grant';
|
||||
|
||||
const {
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Authenticator.extend({
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
|
||||
serverTokenEndpoint: computed('ghostPaths.apiRoot', function () {
|
||||
return `${this.get('ghostPaths.apiRoot')}/authentication/token`;
|
||||
}),
|
||||
|
||||
serverTokenRevocationEndpoint: computed('ghostPaths.apiRoot', function () {
|
||||
return `${this.get('ghostPaths.apiRoot')}/authentication/revoke`;
|
||||
}),
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
import Oauth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';
|
||||
|
||||
export default Oauth2Bearer;
|
|
@ -1,22 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, run} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active'],
|
||||
active: false,
|
||||
linkClasses: null,
|
||||
|
||||
click() {
|
||||
this.$('a').blur();
|
||||
},
|
||||
|
||||
actions: {
|
||||
setActive(value) {
|
||||
run.schedule('afterRender', this, function () {
|
||||
this.set('active', value);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'article',
|
||||
classNames: ['gh-alert'],
|
||||
classNameBindings: ['typeClass'],
|
||||
|
||||
notifications: service(),
|
||||
|
||||
typeClass: computed('message.type', function () {
|
||||
let type = this.get('message.type');
|
||||
let classes = '';
|
||||
let typeMapping;
|
||||
|
||||
typeMapping = {
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'blue'
|
||||
};
|
||||
|
||||
if (typeMapping[type] !== undefined) {
|
||||
classes += `gh-alert-${typeMapping[type]}`;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
closeNotification() {
|
||||
this.get('notifications').closeNotification(this.get('message'));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
observer
|
||||
} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'aside',
|
||||
classNames: 'gh-alerts',
|
||||
|
||||
notifications: service(),
|
||||
|
||||
messages: alias('notifications.alerts'),
|
||||
|
||||
messageCountObserver: observer('messages.[]', function () {
|
||||
this.sendAction('notify', this.get('messages').length);
|
||||
})
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, observer} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['gh-app'],
|
||||
|
||||
showSettingsMenu: false,
|
||||
|
||||
toggleSettingsMenuBodyClass: observer('showSettingsMenu', function () {
|
||||
let showSettingsMenu = this.get('showSettingsMenu');
|
||||
|
||||
Ember.$('body').toggleClass('settings-menu-expanded', showSettingsMenu);
|
||||
})
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
config: service()
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/* global CodeMirror */
|
||||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: ['isFocused:focused'],
|
||||
|
||||
value: '', // make sure a value exists
|
||||
isFocused: false,
|
||||
|
||||
// options for the editor
|
||||
lineNumbers: true,
|
||||
indentUnit: 4,
|
||||
mode: 'htmlmixed',
|
||||
theme: 'xq-light',
|
||||
|
||||
_editor: null, // reference to CodeMirror editor
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let options = this.getProperties('lineNumbers', 'indentUnit', 'mode', 'theme');
|
||||
let editor = new CodeMirror(this.get('element'), options);
|
||||
|
||||
editor.getDoc().setValue(this.get('value'));
|
||||
|
||||
// events
|
||||
editor.on('focus', Ember.run.bind(this, 'set', 'isFocused', true));
|
||||
editor.on('blur', Ember.run.bind(this, 'set', 'isFocused', false));
|
||||
editor.on('change', () => {
|
||||
Ember.run(this, function () {
|
||||
this.set('value', editor.getDoc().getValue());
|
||||
});
|
||||
});
|
||||
|
||||
this._editor = editor;
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let editor = this._editor.getWrapperElement();
|
||||
editor.parentNode.removeChild(editor);
|
||||
this._editor = null;
|
||||
}
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
|
||||
Implements a div for covering the page content
|
||||
when in a menu context that, for example,
|
||||
should be closed when the user clicks elsewhere.
|
||||
|
||||
Example:
|
||||
```
|
||||
{{gh-content-cover onClick="closeMenus" onMouseEnter="closeAutoNav"}}
|
||||
```
|
||||
**/
|
||||
|
||||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['content-cover'],
|
||||
|
||||
onClick: null,
|
||||
onMouseEnter: null,
|
||||
|
||||
click() {
|
||||
this.sendAction('onClick');
|
||||
},
|
||||
|
||||
mouseEnter() {
|
||||
this.sendAction('onMouseEnter');
|
||||
}
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['content-preview-content'],
|
||||
|
||||
content: null,
|
||||
|
||||
didReceiveAttrs(options) {
|
||||
this._super(...arguments);
|
||||
|
||||
// adjust when didReceiveAttrs gets both newAttrs and oldAttrs
|
||||
if (options.newAttrs.content && this.get('content') !== options.newAttrs.content.value) {
|
||||
let el = this.$();
|
||||
|
||||
if (el) {
|
||||
el.closest('.content-preview').scrollTop(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['gh-view', 'content-view-container'],
|
||||
|
||||
mediaQueries: service(),
|
||||
previewIsHidden: computed.reads('mediaQueries.maxWidth900')
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import TextInputMixin from 'ghost/mixins/text-input';
|
||||
import boundOneWay from 'ghost/utils/bound-one-way';
|
||||
import {formatDate} from 'ghost/utils/date-formatting';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend(TextInputMixin, {
|
||||
tagName: 'span',
|
||||
classNames: 'input-icon icon-calendar',
|
||||
|
||||
datetime: boundOneWay('value'),
|
||||
inputClass: null,
|
||||
inputId: null,
|
||||
inputName: null,
|
||||
|
||||
didReceiveAttrs() {
|
||||
let datetime = this.get('datetime') || moment();
|
||||
|
||||
if (!this.get('update')) {
|
||||
throw new Error(`You must provide an \`update\` action to \`{{${this.templateName}}}\`.`);
|
||||
}
|
||||
|
||||
this.set('datetime', formatDate(datetime));
|
||||
},
|
||||
|
||||
focusOut() {
|
||||
let datetime = this.get('datetime');
|
||||
|
||||
invokeAction(this, 'update', datetime);
|
||||
}
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import DropdownMixin from 'ghost/mixins/dropdown-mixin';
|
||||
|
||||
const {
|
||||
Component,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend(DropdownMixin, {
|
||||
tagName: 'button',
|
||||
attributeBindings: 'role',
|
||||
role: 'button',
|
||||
|
||||
// matches with the dropdown this button toggles
|
||||
dropdownName: null,
|
||||
|
||||
dropdown: service(),
|
||||
|
||||
// Notify dropdown service this dropdown should be toggled
|
||||
click(event) {
|
||||
this._super(event);
|
||||
this.get('dropdown').toggleDropdown(this.get('dropdownName'), this);
|
||||
}
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import DropdownMixin from 'ghost/mixins/dropdown-mixin';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend(DropdownMixin, {
|
||||
classNames: 'dropdown',
|
||||
classNameBindings: ['fadeIn:fade-in-scale:fade-out', 'isOpen:open:closed'],
|
||||
|
||||
name: null,
|
||||
closeOnClick: false,
|
||||
|
||||
// Helps track the user re-opening the menu while it's fading out.
|
||||
closing: false,
|
||||
|
||||
// Helps track whether the dropdown is open or closes, or in a transition to either
|
||||
isOpen: false,
|
||||
|
||||
// Managed the toggle between the fade-in and fade-out classes
|
||||
fadeIn: computed('isOpen', 'closing', function () {
|
||||
return this.get('isOpen') && !this.get('closing');
|
||||
}),
|
||||
|
||||
dropdown: service(),
|
||||
|
||||
open() {
|
||||
this.set('isOpen', true);
|
||||
this.set('closing', false);
|
||||
this.set('button.isOpen', true);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.set('closing', true);
|
||||
|
||||
if (this.get('button')) {
|
||||
this.set('button.isOpen', false);
|
||||
}
|
||||
|
||||
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => {
|
||||
if (event.originalEvent.animationName === 'fade-out') {
|
||||
Ember.run(this, function () {
|
||||
if (this.get('closing')) {
|
||||
this.set('isOpen', false);
|
||||
this.set('closing', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Called by the dropdown service when any dropdown button is clicked.
|
||||
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) {
|
||||
button = options.button;
|
||||
this.set('button', button);
|
||||
}
|
||||
this.open();
|
||||
} else if (isOpen) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
click(event) {
|
||||
this._super(event);
|
||||
|
||||
if (this.get('closeOnClick')) {
|
||||
return this.close();
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
let dropdownService = this.get('dropdown');
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
dropdownService.on('close', this, this.close);
|
||||
dropdownService.on('toggle', this, this.toggle);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
let dropdownService = this.get('dropdown');
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
dropdownService.off('close', this, this.close);
|
||||
dropdownService.off('toggle', this, this.toggle);
|
||||
}
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import EditorAPI from 'ghost/mixins/ed-editor-api';
|
||||
import EditorShortcuts from 'ghost/mixins/ed-editor-shortcuts';
|
||||
import EditorScroll from 'ghost/mixins/ed-editor-scroll';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
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() {
|
||||
this.sendAction('onFocusIn');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the focus of the textarea if needed
|
||||
*/
|
||||
setFocus() {
|
||||
if (this.get('focus')) {
|
||||
this.$().val(this.$().val()).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up properties at render time
|
||||
*/
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.setFocus();
|
||||
|
||||
invokeAction(this, 'setEditor', this);
|
||||
|
||||
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
||||
},
|
||||
|
||||
afterRenderEvent() {
|
||||
if (this.get('focus') && this.get('focusCursorAtEnd')) {
|
||||
this.setSelection('end');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleCopyHTMLModal(generatedHTML) {
|
||||
invokeAction(this, 'toggleCopyHTMLModal', generatedHTML);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,110 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import {formatMarkdown} from 'ghost/helpers/gh-format-markdown';
|
||||
|
||||
const {
|
||||
Component,
|
||||
run,
|
||||
uuid
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
_scrollWrapper: null,
|
||||
|
||||
previewHTML: '',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('imageUploadComponents', Ember.A([]));
|
||||
this.buildPreviewHTML();
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
||||
this.adjustScrollPosition(this.get('scrollPosition'));
|
||||
},
|
||||
|
||||
didReceiveAttrs(attrs) {
|
||||
this._super(...arguments);
|
||||
|
||||
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) {
|
||||
run.throttle(this, this.buildPreviewHTML, 30, false);
|
||||
}
|
||||
},
|
||||
|
||||
adjustScrollPosition(scrollPosition) {
|
||||
let scrollWrapper = this._scrollWrapper;
|
||||
|
||||
if (scrollWrapper) {
|
||||
scrollWrapper.scrollTop(scrollPosition);
|
||||
}
|
||||
},
|
||||
|
||||
buildPreviewHTML() {
|
||||
let markdown = this.get('markdown');
|
||||
let html = formatMarkdown([markdown]).string;
|
||||
let template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
let fragment = template.content;
|
||||
let dropzones = fragment.querySelectorAll('.js-drop-zone');
|
||||
let components = this.get('imageUploadComponents');
|
||||
|
||||
if (dropzones.length !== components.length) {
|
||||
components = Ember.A([]);
|
||||
this.set('imageUploadComponents', components);
|
||||
}
|
||||
|
||||
[...dropzones].forEach((oldEl, i) => {
|
||||
let el = oldEl.cloneNode(true);
|
||||
let component = components[i];
|
||||
let uploadTarget = el.querySelector('.js-upload-target');
|
||||
let id = uuid();
|
||||
let destinationElementId = `image-uploader-${id}`;
|
||||
let src;
|
||||
|
||||
if (uploadTarget) {
|
||||
src = uploadTarget.getAttribute('src');
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component.set('destinationElementId', destinationElementId);
|
||||
component.set('src', src);
|
||||
} else {
|
||||
let imageUpload = Ember.Object.create({
|
||||
destinationElementId,
|
||||
id,
|
||||
src,
|
||||
index: i
|
||||
});
|
||||
|
||||
this.get('imageUploadComponents').pushObject(imageUpload);
|
||||
}
|
||||
|
||||
el.id = destinationElementId;
|
||||
el.innerHTML = '';
|
||||
el.classList.remove('image-uploader');
|
||||
|
||||
oldEl.parentNode.replaceChild(el, oldEl);
|
||||
});
|
||||
|
||||
this.set('previewHTML', fragment);
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateImageSrc(index, url) {
|
||||
this.attrs.updateImageSrc(index, url);
|
||||
},
|
||||
|
||||
updateHeight() {
|
||||
this.attrs.updateHeight(this.$().height());
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, computed} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['splitbtn', 'js-publish-splitbutton'],
|
||||
classNameBindings: ['isNew:unsaved'],
|
||||
|
||||
isNew: null,
|
||||
isPublished: null,
|
||||
willPublish: null,
|
||||
postOrPage: null,
|
||||
submitting: false,
|
||||
|
||||
// Tracks whether we're going to change the state of the post on save
|
||||
isDangerous: computed('isPublished', 'willPublish', function () {
|
||||
return this.get('isPublished') !== this.get('willPublish');
|
||||
}),
|
||||
|
||||
publishText: computed('isPublished', 'postOrPage', function () {
|
||||
return this.get('isPublished') ? `Update ${this.get('postOrPage')}` : 'Publish Now';
|
||||
}),
|
||||
|
||||
draftText: computed('isPublished', function () {
|
||||
return this.get('isPublished') ? 'Unpublish' : 'Save Draft';
|
||||
}),
|
||||
|
||||
deleteText: computed('postOrPage', function () {
|
||||
return `Delete ${this.get('postOrPage')}`;
|
||||
}),
|
||||
|
||||
saveText: computed('willPublish', 'publishText', 'draftText', function () {
|
||||
return this.get('willPublish') ? this.get('publishText') : this.get('draftText');
|
||||
}),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.sendAction('save');
|
||||
},
|
||||
|
||||
setSaveType(saveType) {
|
||||
this.sendAction('setSaveType', saveType);
|
||||
},
|
||||
|
||||
delete() {
|
||||
this.sendAction('delete');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,123 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ShortcutsMixin from 'ghost/mixins/shortcuts';
|
||||
import imageManager from 'ghost/utils/ed-image-manager';
|
||||
import editorShortcuts from 'ghost/utils/editor-shortcuts';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {Component, computed, run} = Ember;
|
||||
const {equal} = computed;
|
||||
|
||||
export default Component.extend(ShortcutsMixin, {
|
||||
tagName: 'section',
|
||||
classNames: ['view-container', 'view-editor'],
|
||||
|
||||
activeTab: 'markdown',
|
||||
editor: null,
|
||||
editorDisabled: undefined,
|
||||
editorScrollInfo: null, // updated when gh-ed-editor component scrolls
|
||||
height: null, // updated when markdown is rendered
|
||||
shouldFocusEditor: false,
|
||||
showCopyHTMLModal: false,
|
||||
copyHTMLModalContent: null,
|
||||
|
||||
shortcuts: editorShortcuts,
|
||||
|
||||
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: computed('editorScrollInfo', 'height', function () {
|
||||
let scrollInfo = this.get('editorScrollInfo');
|
||||
let {$previewContent, $previewViewPort} = this;
|
||||
|
||||
if (!scrollInfo || !$previewContent || !$previewViewPort) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let previewHeight = $previewContent.height() - $previewViewPort.height();
|
||||
let previewPosition, ratio;
|
||||
|
||||
ratio = previewHeight / scrollInfo.diff;
|
||||
previewPosition = scrollInfo.top * ratio;
|
||||
|
||||
return previewPosition;
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.registerShortcuts();
|
||||
run.scheduleOnce('afterRender', this, this._cacheElements);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
invokeAction(this, 'onTeardown');
|
||||
|
||||
this.removeShortcuts();
|
||||
},
|
||||
|
||||
_cacheElements() {
|
||||
// cache these elements for use in other methods
|
||||
this.$previewViewPort = this.$('.js-entry-preview-content');
|
||||
this.$previewContent = this.$('.js-rendered-markdown');
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectTab(tab) {
|
||||
this.set('activeTab', tab);
|
||||
},
|
||||
|
||||
updateScrollInfo(scrollInfo) {
|
||||
this.set('editorScrollInfo', scrollInfo);
|
||||
},
|
||||
|
||||
updateHeight(height) {
|
||||
this.set('height', height);
|
||||
},
|
||||
|
||||
// set from a `sendAction` on the gh-ed-editor component,
|
||||
// so that we get a reference for handling uploads.
|
||||
setEditor(editor) {
|
||||
this.set('editor', editor);
|
||||
},
|
||||
|
||||
disableEditor() {
|
||||
this.set('editorDisabled', true);
|
||||
},
|
||||
|
||||
enableEditor() {
|
||||
this.set('editorDisabled', undefined);
|
||||
},
|
||||
|
||||
// The actual functionality is implemented in utils/ed-editor-shortcuts
|
||||
editorShortcut(options) {
|
||||
if (this.editor.$().is(':focus')) {
|
||||
this.editor.shortcut(options.type);
|
||||
}
|
||||
},
|
||||
|
||||
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
||||
// ensuring that everything ends up in the correct place and format.
|
||||
handleImgUpload(imageIndex, newSrc) {
|
||||
let editor = this.get('editor');
|
||||
let editorValue = editor.getValue();
|
||||
let replacement = imageManager.getSrcRange(editorValue, imageIndex);
|
||||
let cursorPosition;
|
||||
|
||||
if (replacement) {
|
||||
cursorPosition = replacement.start + newSrc.length + 1;
|
||||
if (replacement.needsParens) {
|
||||
newSrc = `(${newSrc})`;
|
||||
}
|
||||
editor.replaceSelection(newSrc, replacement.start, replacement.end, cursorPosition);
|
||||
}
|
||||
},
|
||||
|
||||
toggleCopyHTMLModal(generatedHTML) {
|
||||
this.set('copyHTMLModalContent', generatedHTML);
|
||||
this.toggleProperty('showCopyHTMLModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
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
|
||||
* that specific property. If there are no errors associated with the property,
|
||||
* nothing will be rendered.
|
||||
* @param {DS.Errors} errors The DS.Errors object
|
||||
* @param {string} property The property name
|
||||
*/
|
||||
export default Component.extend({
|
||||
tagName: 'p',
|
||||
classNames: ['response'],
|
||||
|
||||
errors: null,
|
||||
property: '',
|
||||
|
||||
isVisible: computed.notEmpty('errors'),
|
||||
|
||||
message: computed('errors.[]', 'property', function () {
|
||||
let property = this.get('property');
|
||||
let errors = this.get('errors');
|
||||
let messages = [];
|
||||
let index;
|
||||
|
||||
if (!isEmpty(errors) && errors.get(property)) {
|
||||
errors.get(property).forEach((error) => {
|
||||
messages.push(error);
|
||||
});
|
||||
index = Math.floor(Math.random() * messages.length);
|
||||
return messages[index].message;
|
||||
}
|
||||
})
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
computed,
|
||||
inject: {service},
|
||||
Component
|
||||
} = Ember;
|
||||
|
||||
const FeatureFlagComponent = Component.extend({
|
||||
tagName: 'label',
|
||||
classNames: 'checkbox',
|
||||
attributeBindings: ['for'],
|
||||
_flagValue: null,
|
||||
|
||||
feature: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set('_flagValue', this.get(`feature.${this.get('flag')}`));
|
||||
},
|
||||
|
||||
value: computed('_flagValue', {
|
||||
get() {
|
||||
return this.get('_flagValue');
|
||||
},
|
||||
set(key, value) {
|
||||
return this.set(`feature.${this.get('flag')}`, value);
|
||||
}
|
||||
}),
|
||||
|
||||
for: computed('flag', function () {
|
||||
return `labs-${this.get('flag')}`;
|
||||
}),
|
||||
|
||||
name: computed('flag', function () {
|
||||
return `labs[${this.get('flag')}]`;
|
||||
})
|
||||
});
|
||||
|
||||
FeatureFlagComponent.reopenClass({
|
||||
positionalParams: ['flag']
|
||||
});
|
||||
|
||||
export default FeatureFlagComponent;
|
|
@ -1,37 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
_file: null,
|
||||
|
||||
uploadButtonText: 'Text',
|
||||
uploadButtonDisabled: true,
|
||||
|
||||
onUpload: null,
|
||||
onAdd: null,
|
||||
|
||||
shouldResetForm: true,
|
||||
|
||||
change(event) {
|
||||
this.set('uploadButtonDisabled', false);
|
||||
this.sendAction('onAdd');
|
||||
this._file = event.target.files[0];
|
||||
},
|
||||
|
||||
actions: {
|
||||
upload() {
|
||||
if (!this.get('uploadButtonDisabled') && this._file) {
|
||||
this.sendAction('onUpload', this._file);
|
||||
}
|
||||
|
||||
// Prevent double post by disabling the button.
|
||||
this.set('uploadButtonDisabled', true);
|
||||
|
||||
// Reset form
|
||||
if (this.get('shouldResetForm')) {
|
||||
this.$().closest('form')[0].reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,161 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import { invoke, invokeAction } from 'ember-invoke-action';
|
||||
import {
|
||||
RequestEntityTooLargeError,
|
||||
UnsupportedMediaTypeError
|
||||
} from 'ghost/services/ajax';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
run
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
classNameBindings: ['dragClass'],
|
||||
|
||||
labelText: 'Select or drag-and-drop a file',
|
||||
url: null,
|
||||
paramName: 'file',
|
||||
|
||||
file: null,
|
||||
response: null,
|
||||
|
||||
dragClass: null,
|
||||
failureMessage: null,
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: service(),
|
||||
|
||||
formData: computed('file', function () {
|
||||
let paramName = this.get('paramName');
|
||||
let file = this.get('file');
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(paramName, file);
|
||||
|
||||
return formData;
|
||||
}),
|
||||
|
||||
progressStyle: computed('uploadPercentage', function () {
|
||||
let percentage = this.get('uploadPercentage');
|
||||
let width = '';
|
||||
|
||||
if (percentage > 0) {
|
||||
width = `${percentage}%`;
|
||||
} else {
|
||||
width = '0';
|
||||
}
|
||||
|
||||
return Ember.String.htmlSafe(`width: ${width}`);
|
||||
}),
|
||||
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is needed to work around inconsistencies with dropping files
|
||||
// from Chrome's downloads bar
|
||||
let eA = event.dataTransfer.effectAllowed;
|
||||
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
this.set('dragClass', '--drag-over');
|
||||
},
|
||||
|
||||
dragLeave(event) {
|
||||
event.preventDefault();
|
||||
this.set('dragClass', null);
|
||||
},
|
||||
|
||||
drop(event) {
|
||||
event.preventDefault();
|
||||
this.set('dragClass', null);
|
||||
if (event.dataTransfer.files) {
|
||||
invoke(this, 'fileSelected', event.dataTransfer.files);
|
||||
}
|
||||
},
|
||||
|
||||
generateRequest() {
|
||||
let ajax = this.get('ajax');
|
||||
let formData = this.get('formData');
|
||||
let url = this.get('url');
|
||||
|
||||
invokeAction(this, 'uploadStarted');
|
||||
|
||||
ajax.post(url, {
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'text',
|
||||
xhr: () => {
|
||||
let xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
this._uploadProgress(event);
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
}).then((response) => {
|
||||
this._uploadSuccess(JSON.parse(response));
|
||||
}).catch((error) => {
|
||||
this._uploadFailed(error);
|
||||
}).finally(() => {
|
||||
invokeAction(this, 'uploadFinished');
|
||||
});
|
||||
},
|
||||
|
||||
_uploadProgress(event) {
|
||||
if (event.lengthComputable) {
|
||||
run(() => {
|
||||
let percentage = Math.round((event.loaded / event.total) * 100);
|
||||
this.set('uploadPercentage', percentage);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_uploadSuccess(response) {
|
||||
invokeAction(this, 'uploadSuccess', response);
|
||||
invoke(this, 'reset');
|
||||
},
|
||||
|
||||
_uploadFailed(error) {
|
||||
let message;
|
||||
|
||||
if (error instanceof UnsupportedMediaTypeError) {
|
||||
message = 'The file type you uploaded is not supported.';
|
||||
} else if (error instanceof RequestEntityTooLargeError) {
|
||||
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
||||
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||
message = error.errors[0].message;
|
||||
} else {
|
||||
message = 'Something went wrong :(';
|
||||
}
|
||||
|
||||
this.set('failureMessage', message);
|
||||
invokeAction(this, 'uploadFailed', error);
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
this.set('file', fileList[0]);
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
this.set('failureMessage', null);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
import ValidationStatusContainer from 'ghost/components/gh-validation-status-container';
|
||||
|
||||
export default ValidationStatusContainer.extend({
|
||||
classNames: 'form-group'
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import LiquidTether from 'liquid-tether/components/liquid-tether';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {
|
||||
RSVP: {Promise},
|
||||
inject: {service},
|
||||
isBlank,
|
||||
on,
|
||||
run
|
||||
} = Ember;
|
||||
const emberA = Ember.A;
|
||||
|
||||
const FullScreenModalComponent = LiquidTether.extend({
|
||||
to: 'fullscreen-modal',
|
||||
target: 'document.body',
|
||||
targetModifier: 'visible',
|
||||
targetAttachment: 'top center',
|
||||
attachment: 'top center',
|
||||
tetherClass: 'fullscreen-modal',
|
||||
overlayClass: 'fullscreen-modal-background',
|
||||
modalPath: 'unknown',
|
||||
|
||||
dropdown: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.modalPath = `modals/${this.get('modal')}`;
|
||||
},
|
||||
|
||||
setTetherClass: on('init', function () {
|
||||
let tetherClass = this.get('tetherClass');
|
||||
let modifiers = (this.get('modifier') || '').split(' ');
|
||||
let tetherClasses = emberA([tetherClass]);
|
||||
|
||||
modifiers.forEach((modifier) => {
|
||||
if (!isBlank(modifier)) {
|
||||
let className = `${tetherClass}-${modifier}`;
|
||||
tetherClasses.push(className);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('tetherClass', tetherClasses.join(' '));
|
||||
}),
|
||||
|
||||
closeDropdowns: on('didInsertElement', function () {
|
||||
run.schedule('afterRender', this, function () {
|
||||
this.get('dropdown').closeDropdowns();
|
||||
});
|
||||
}),
|
||||
|
||||
actions: {
|
||||
close() {
|
||||
// Because we return the promise from invokeAction, we have
|
||||
// to check if "close" exists first
|
||||
if (this.get('close')) {
|
||||
return invokeAction(this, 'close');
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
confirm() {
|
||||
if (this.get('confirm')) {
|
||||
return invokeAction(this, 'confirm');
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
clickOverlay() {
|
||||
this.send('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FullScreenModalComponent.reopenClass({
|
||||
positionalParams: ['modal']
|
||||
});
|
||||
|
||||
export default FullScreenModalComponent;
|
|
@ -1,39 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
actions: {
|
||||
update() {
|
||||
if (typeof this.attrs.update === 'function') {
|
||||
this.attrs.update(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
onInput() {
|
||||
if (typeof this.attrs.onInput === 'function') {
|
||||
this.attrs.onInput(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
uploadStarted() {
|
||||
if (typeof this.attrs.uploadStarted === 'function') {
|
||||
this.attrs.uploadStarted(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
if (typeof this.attrs.uploadFinished === 'function') {
|
||||
this.attrs.uploadFinished(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
formChanged() {
|
||||
if (typeof this.attrs.formChanged === 'function') {
|
||||
this.attrs.formChanged(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,226 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
import {RequestEntityTooLargeError, UnsupportedMediaTypeError} from 'ghost/services/ajax';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
run
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
classNameBindings: ['dragClass'],
|
||||
|
||||
image: null,
|
||||
text: 'Upload an image',
|
||||
saveButton: true,
|
||||
|
||||
dragClass: null,
|
||||
failureMessage: null,
|
||||
file: null,
|
||||
formType: 'upload',
|
||||
url: null,
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
|
||||
// TODO: this wouldn't be necessary if the server could accept direct
|
||||
// file uploads
|
||||
formData: computed('file', function () {
|
||||
let file = this.get('file');
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append('uploadimage', file);
|
||||
|
||||
return formData;
|
||||
}),
|
||||
|
||||
progressStyle: computed('uploadPercentage', function () {
|
||||
let percentage = this.get('uploadPercentage');
|
||||
let width = '';
|
||||
|
||||
if (percentage > 0) {
|
||||
width = `${percentage}%`;
|
||||
} else {
|
||||
width = '0';
|
||||
}
|
||||
|
||||
return Ember.String.htmlSafe(`width: ${width}`);
|
||||
}),
|
||||
|
||||
canShowUploadForm: computed('config.fileStorage', function () {
|
||||
return this.get('config.fileStorage') !== false;
|
||||
}),
|
||||
|
||||
showUploadForm: computed('formType', function () {
|
||||
let canShowUploadForm = this.get('canShowUploadForm');
|
||||
let formType = this.get('formType');
|
||||
|
||||
return formType === 'upload' && canShowUploadForm;
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
let image = this.get('image');
|
||||
this.set('url', image);
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
let showUploadForm = this.get('showUploadForm');
|
||||
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is needed to work around inconsistencies with dropping files
|
||||
// from Chrome's downloads bar
|
||||
let eA = event.dataTransfer.effectAllowed;
|
||||
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (showUploadForm) {
|
||||
this.set('dragClass', '--drag-over');
|
||||
}
|
||||
},
|
||||
|
||||
dragLeave(event) {
|
||||
let showUploadForm = this.get('showUploadForm');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (showUploadForm) {
|
||||
this.set('dragClass', null);
|
||||
}
|
||||
},
|
||||
|
||||
drop(event) {
|
||||
let showUploadForm = this.get('showUploadForm');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.set('dragClass', null);
|
||||
|
||||
if (showUploadForm) {
|
||||
if (event.dataTransfer.files) {
|
||||
this.send('fileSelected', event.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
uploadStarted() {
|
||||
if (typeof this.attrs.uploadStarted === 'function') {
|
||||
this.attrs.uploadStarted();
|
||||
}
|
||||
},
|
||||
|
||||
uploadProgress(event) {
|
||||
if (event.lengthComputable) {
|
||||
run(() => {
|
||||
let percentage = Math.round((event.loaded / event.total) * 100);
|
||||
this.set('uploadPercentage', percentage);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
if (typeof this.attrs.uploadFinished === 'function') {
|
||||
this.attrs.uploadFinished();
|
||||
}
|
||||
},
|
||||
|
||||
uploadSuccess(response) {
|
||||
this.set('url', response);
|
||||
this.send('saveUrl');
|
||||
this.send('reset');
|
||||
},
|
||||
|
||||
uploadFailed(error) {
|
||||
let message;
|
||||
|
||||
if (error instanceof UnsupportedMediaTypeError) {
|
||||
message = 'The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.';
|
||||
} else if (error instanceof RequestEntityTooLargeError) {
|
||||
message = 'The image you uploaded was larger than the maximum file size your server allows.';
|
||||
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||
message = error.errors[0].message;
|
||||
} else {
|
||||
message = 'Something went wrong :(';
|
||||
}
|
||||
|
||||
this.set('failureMessage', message);
|
||||
},
|
||||
|
||||
generateRequest() {
|
||||
let ajax = this.get('ajax');
|
||||
let formData = this.get('formData');
|
||||
let url = `${ghostPaths().apiRoot}/uploads/`;
|
||||
|
||||
this.uploadStarted();
|
||||
|
||||
ajax.post(url, {
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'text',
|
||||
xhr: () => {
|
||||
let xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
this.uploadProgress(event);
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
}).then((response) => {
|
||||
let url = JSON.parse(response);
|
||||
this.uploadSuccess(url);
|
||||
}).catch((error) => {
|
||||
this.uploadFailed(error);
|
||||
}).finally(() => {
|
||||
this.uploadFinished();
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
this.set('file', fileList[0]);
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
},
|
||||
|
||||
onInput(url) {
|
||||
this.set('url', url);
|
||||
|
||||
if (typeof this.attrs.onInput === 'function') {
|
||||
this.attrs.onInput(url);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
switchForm(formType) {
|
||||
this.set('formType', formType);
|
||||
|
||||
if (typeof this.attrs.formChanged === 'function') {
|
||||
run.scheduleOnce('afterRender', this, function () {
|
||||
this.attrs.formChanged(formType);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
this.attrs.update(url);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import InfiniteScrollMixin from 'ghost/mixins/infinite-scroll';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend(InfiniteScrollMixin, {
|
||||
actions: {
|
||||
checkScroll() {
|
||||
this._checkScroll();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import TextInputMixin from 'ghost/mixins/text-input';
|
||||
|
||||
const {TextField} = Ember;
|
||||
|
||||
export default TextField.extend(TextInputMixin, {
|
||||
classNames: 'gh-input'
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import LightTable from 'ember-light-table/components/light-table';
|
||||
|
||||
const {$, run} = Ember;
|
||||
|
||||
export default LightTable.extend({
|
||||
|
||||
// HACK: infinite pagination was not triggering when scrolling very fast
|
||||
// as the throttle triggers before scrolling into the buffer area but
|
||||
// the scroll finishes before the throttle timeout. Adding a debounce that
|
||||
// does the same thing means that we are guaranteed a final trigger when
|
||||
// scrolling stops
|
||||
//
|
||||
// An issue has been opened upstream, this can be removed if it gets fixed
|
||||
// https://github.com/offirgolan/ember-light-table/issues/15
|
||||
|
||||
_setupScrollEvents() {
|
||||
$(this.get('touchMoveContainer')).on('touchmove.light-table', run.bind(this, this._scrollHandler, '_touchmoveTimer'));
|
||||
$(this.get('scrollContainer')).on('scroll.light-table', run.bind(this, this._scrollHandler, '_scrollTimer'));
|
||||
$(this.get('scrollContainer')).on('scroll.light-table', run.bind(this, this._scrollHandler, '_scrollDebounce'));
|
||||
},
|
||||
|
||||
_scrollHandler(timer) {
|
||||
this.set(timer, run.debounce(this, this._onScroll, 100));
|
||||
this.set(timer, run.throttle(this, this._onScroll, 100));
|
||||
}
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'main',
|
||||
classNames: ['gh-main'],
|
||||
ariaRole: 'main',
|
||||
|
||||
mouseEnter() {
|
||||
this.sendAction('onMouseEnter');
|
||||
}
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = 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: service(),
|
||||
isMobile: computed.reads('mediaQueries.isMobile'),
|
||||
maximise: false,
|
||||
|
||||
iconClass: computed('maximise', 'isMobile', function () {
|
||||
if (this.get('maximise') && !this.get('isMobile')) {
|
||||
return 'icon-maximise';
|
||||
} else {
|
||||
return 'icon-minimise';
|
||||
}
|
||||
}),
|
||||
|
||||
click() {
|
||||
if (this.get('isMobile')) {
|
||||
this.sendAction('mobileAction');
|
||||
} else {
|
||||
this.toggleProperty('maximise');
|
||||
this.sendAction('desktopAction');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
inject: {service},
|
||||
computed
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'nav',
|
||||
classNames: ['gh-nav'],
|
||||
classNameBindings: ['open'],
|
||||
|
||||
open: false,
|
||||
|
||||
navMenuIcon: computed('ghostPaths.subdir', function () {
|
||||
let url = `${this.get('ghostPaths.subdir')}/ghost/img/ghosticon.jpg`;
|
||||
|
||||
return Ember.String.htmlSafe(`background-image: url(${url})`);
|
||||
}),
|
||||
|
||||
config: service(),
|
||||
session: service(),
|
||||
ghostPaths: service(),
|
||||
feature: service(),
|
||||
|
||||
mouseEnter() {
|
||||
this.sendAction('onMouseEnter');
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleAutoNav() {
|
||||
this.sendAction('toggleMaximise');
|
||||
},
|
||||
|
||||
showMarkdownHelp() {
|
||||
this.sendAction('showMarkdownHelp');
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
this.sendAction('closeMobileMenu');
|
||||
},
|
||||
|
||||
openAutoNav() {
|
||||
this.sendAction('openAutoNav');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, run} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: 'gh-view',
|
||||
|
||||
didInsertElement() {
|
||||
let navContainer = this.$('.js-gh-blognav');
|
||||
let navElements = '.gh-blognav-item:not(.gh-blognav-item:last-child)';
|
||||
// needed because jqueryui sortable doesn't trigger babel's autoscoping
|
||||
let _this = this;
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
navContainer.sortable({
|
||||
handle: '.gh-blognav-grab',
|
||||
items: navElements,
|
||||
|
||||
start(event, ui) {
|
||||
run(() => {
|
||||
ui.item.data('start-index', ui.item.index());
|
||||
});
|
||||
},
|
||||
|
||||
update(event, ui) {
|
||||
run(() => {
|
||||
_this.sendAction('moveItem', ui.item.data('start-index'), ui.item.index());
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.$('.ui-sortable').sortable('destroy');
|
||||
}
|
||||
});
|
|
@ -1,145 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {TextField, computed, run} = Ember;
|
||||
|
||||
let joinUrlParts = function (url, path) {
|
||||
if (path[0] !== '/' && url.slice(-1) !== '/') {
|
||||
path = `/${path}`;
|
||||
} else if (path[0] === '/' && url.slice(-1) === '/') {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
};
|
||||
|
||||
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 TextField.extend({
|
||||
classNames: 'gh-input',
|
||||
|
||||
isBaseUrl: computed('baseUrl', 'value', function () {
|
||||
return this.get('baseUrl') === this.get('value');
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
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)) {
|
||||
url = joinUrlParts(baseUrl, url);
|
||||
}
|
||||
|
||||
this.set('value', url);
|
||||
},
|
||||
|
||||
focusIn(event) {
|
||||
this.set('hasFocus', true);
|
||||
|
||||
if (this.get('isBaseUrl')) {
|
||||
// position the cursor at the end of the input
|
||||
run.next(function (el) {
|
||||
let {length} = el.value;
|
||||
|
||||
el.setSelectionRange(length, length);
|
||||
}, event.target);
|
||||
}
|
||||
},
|
||||
|
||||
keyDown(event) {
|
||||
// delete the "placeholder" value all at once
|
||||
if (this.get('isBaseUrl') && (event.keyCode === 8 || event.keyCode === 46)) {
|
||||
this.set('value', '');
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// CMD-S
|
||||
if (event.keyCode === 83 && event.metaKey) {
|
||||
this.notifyUrlChanged();
|
||||
}
|
||||
},
|
||||
|
||||
keyPress(event) {
|
||||
invokeAction(this, 'clearErrors');
|
||||
|
||||
// enter key
|
||||
if (event.keyCode === 13) {
|
||||
this.notifyUrlChanged();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
focusOut() {
|
||||
this.set('hasFocus', false);
|
||||
|
||||
this.notifyUrlChanged();
|
||||
},
|
||||
|
||||
notifyUrlChanged() {
|
||||
let url = this.get('value').trim();
|
||||
let urlParts = document.createElement('a');
|
||||
let baseUrl = this.get('baseUrl');
|
||||
let baseUrlParts = document.createElement('a');
|
||||
|
||||
// ensure value property is trimmed
|
||||
this.set('value', url);
|
||||
|
||||
// leverage the browser's native URI parsing
|
||||
urlParts.href = url;
|
||||
baseUrlParts.href = baseUrl;
|
||||
|
||||
// if we have an email address, add the mailto:
|
||||
if (validator.isEmail(url)) {
|
||||
url = `mailto:${url}`;
|
||||
this.set('value', url);
|
||||
}
|
||||
|
||||
// if we have a relative url, create the absolute url to be displayed in the input
|
||||
if (isRelative(url)) {
|
||||
url = joinUrlParts(baseUrl, url);
|
||||
this.set('value', url);
|
||||
}
|
||||
|
||||
// get our baseUrl relativity checks in order
|
||||
let isOnSameHost = urlParts.host === baseUrlParts.host;
|
||||
let isAnchorLink = url.match(/^#/);
|
||||
let isRelativeToBasePath = urlParts.pathname.indexOf(baseUrlParts.pathname) === 0;
|
||||
|
||||
// if our pathname is only missing a trailing / mark it as relative
|
||||
if (`${urlParts.pathname}/` === baseUrlParts.pathname) {
|
||||
isRelativeToBasePath = true;
|
||||
}
|
||||
|
||||
// if relative to baseUrl, remove the base url before sending to action
|
||||
if (!isAnchorLink && isOnSameHost && isRelativeToBasePath) {
|
||||
url = url.replace(/^[a-zA-Z0-9\-]+:/, '');
|
||||
url = url.replace(/^\/\//, '');
|
||||
url = url.replace(baseUrlParts.host, '');
|
||||
url = url.replace(baseUrlParts.pathname, '');
|
||||
|
||||
// handle case where url path is same as baseUrl path but missing trailing slash
|
||||
if (urlParts.pathname.slice(-1) !== '/') {
|
||||
url = url.replace(baseUrlParts.pathname.slice(0, -1), '');
|
||||
}
|
||||
|
||||
if (!url.match(/^\//)) {
|
||||
url = `/${url}`;
|
||||
}
|
||||
|
||||
if (!url.match(/\/$/) && !url.match(/[\.#\?]/)) {
|
||||
url = `${url}/`;
|
||||
}
|
||||
}
|
||||
|
||||
this.sendAction('change', url);
|
||||
}
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ValidationState from 'ghost/mixins/validation-state';
|
||||
import SortableItem from 'ember-sortable/mixins/sortable-item';
|
||||
|
||||
const {Component, computed, run} = Ember;
|
||||
const {alias, readOnly} = computed;
|
||||
|
||||
export default Component.extend(ValidationState, SortableItem, {
|
||||
classNames: 'gh-blognav-item',
|
||||
classNameBindings: ['errorClass', 'navItem.isNew::gh-blognav-item--sortable'],
|
||||
|
||||
new: false,
|
||||
handle: '.gh-blognav-grab',
|
||||
|
||||
model: alias('navItem'),
|
||||
errors: readOnly('navItem.errors'),
|
||||
|
||||
errorClass: computed('hasError', function () {
|
||||
if (this.get('hasError')) {
|
||||
return 'gh-blognav-item--error';
|
||||
}
|
||||
}),
|
||||
|
||||
keyPress(event) {
|
||||
// enter key
|
||||
if (event.keyCode === 13 && this.get('navItem.isNew')) {
|
||||
event.preventDefault();
|
||||
run.scheduleOnce('actions', this, function () {
|
||||
this.send('addItem');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addItem() {
|
||||
this.sendAction('addItem');
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.sendAction('deleteItem', item);
|
||||
},
|
||||
|
||||
updateUrl(value) {
|
||||
this.sendAction('updateUrl', value, this.get('navItem'));
|
||||
},
|
||||
|
||||
clearLabelErrors() {
|
||||
this.get('navItem.errors').remove('label');
|
||||
},
|
||||
|
||||
clearUrlErrors() {
|
||||
this.get('navItem.errors').remove('url');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'article',
|
||||
classNames: ['gh-notification', 'gh-notification-passive'],
|
||||
classNameBindings: ['typeClass'],
|
||||
|
||||
message: null,
|
||||
|
||||
notifications: service(),
|
||||
|
||||
typeClass: computed('message.type', function () {
|
||||
let type = this.get('message.type');
|
||||
let classes = '';
|
||||
let typeMapping;
|
||||
|
||||
typeMapping = {
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
warn: 'yellow'
|
||||
};
|
||||
|
||||
if (typeMapping[type] !== undefined) {
|
||||
classes += `gh-notification-${typeMapping[type]}`;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => {
|
||||
if (event.originalEvent.animationName === 'fade-out') {
|
||||
this.get('notifications').closeNotification(this.get('message'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.$().off('animationend webkitAnimationEnd oanimationend MSAnimationEnd');
|
||||
},
|
||||
|
||||
actions: {
|
||||
closeNotification() {
|
||||
this.get('notifications').closeNotification(this.get('message'));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'aside',
|
||||
classNames: 'gh-notifications',
|
||||
|
||||
notifications: service(),
|
||||
|
||||
messages: alias('notifications.notifications')
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import DropdownButton from 'ghost/components/gh-dropdown-button';
|
||||
|
||||
const {
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
function K() {
|
||||
return this;
|
||||
}
|
||||
|
||||
export default DropdownButton.extend({
|
||||
dropdown: service(),
|
||||
|
||||
click: K,
|
||||
|
||||
mouseEnter() {
|
||||
this._super(...arguments);
|
||||
this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
|
||||
},
|
||||
|
||||
mouseLeave() {
|
||||
this._super(...arguments);
|
||||
this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
|
||||
}
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import GhostDropdown from 'ghost/components/gh-dropdown';
|
||||
|
||||
const {
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default GhostDropdown.extend({
|
||||
classNames: 'ghost-popover',
|
||||
dropdown: service()
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ActiveLinkWrapper from 'ghost/mixins/active-link-wrapper';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {
|
||||
$,
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
const {alias, equal} = computed;
|
||||
|
||||
export default Component.extend(ActiveLinkWrapper, {
|
||||
tagName: 'li',
|
||||
classNameBindings: ['isFeatured:featured', 'isPage:page'],
|
||||
|
||||
post: null,
|
||||
previewIsHidden: false,
|
||||
|
||||
isFeatured: alias('post.featured'),
|
||||
isPage: alias('post.page'),
|
||||
isPublished: equal('post.status', 'published'),
|
||||
|
||||
ghostPaths: service(),
|
||||
|
||||
authorName: computed('post.author.name', 'post.author.email', function () {
|
||||
return this.get('post.author.name') || this.get('post.author.email');
|
||||
}),
|
||||
|
||||
authorAvatar: computed('post.author.image', function () {
|
||||
return this.get('post.author.image') || `${this.get('ghostPaths.subdir')}/ghost/img/user-image.png`;
|
||||
}),
|
||||
|
||||
authorAvatarBackground: computed('authorAvatar', function () {
|
||||
return Ember.String.htmlSafe(`background-image: url(${this.get('authorAvatar')})`);
|
||||
}),
|
||||
|
||||
click() {
|
||||
this.sendAction('onClick', this.get('post'));
|
||||
},
|
||||
|
||||
doubleClick() {
|
||||
this.sendAction('onDoubleClick', this.get('post'));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.addObserver('active', this, this.scrollIntoView);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.removeObserver('active', this, this.scrollIntoView);
|
||||
if (this.get('post.isDeleted') && this.get('onDelete')) {
|
||||
invokeAction(this, 'onDelete');
|
||||
}
|
||||
},
|
||||
|
||||
scrollIntoView() {
|
||||
if (!this.get('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
isOnScreen = isBelowTop && isAboveBottom;
|
||||
|
||||
if (!isOnScreen) {
|
||||
// Scroll so that element is centered in container
|
||||
// 40 is the amount of padding on the container
|
||||
container.clearQueue().animate({
|
||||
scrollTop: currentScroll + offset - 40 - containerHeight / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,142 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import AjaxService from 'ember-ajax/services/ajax';
|
||||
import {NotFoundError} from 'ghost/services/ajax';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
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
|
||||
*
|
||||
* Example: {{gh-profile-image email=controllerEmailProperty setImage="controllerActionName" debounce=500}}
|
||||
*
|
||||
* @param {int} size The size of the image to render
|
||||
* @param {String} email Reference to a bound email object if gravatar image behavior is desired.
|
||||
* @param {String|action} setImage The string name of the action on the controller to be called when an image is added.
|
||||
* @param {int} debounce Period to wait after changes to email before attempting to load gravatar
|
||||
* @property {Boolean} hasUploadedImage Whether or not the user has uploaded an image (whether or not to show the default image/gravatar image)
|
||||
* @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 Component.extend({
|
||||
email: '',
|
||||
size: 90,
|
||||
debounce: 300,
|
||||
|
||||
validEmail: '',
|
||||
hasUploadedImage: false,
|
||||
fileStorage: true,
|
||||
ajax: AjaxService.create(),
|
||||
config: service(),
|
||||
|
||||
ghostPaths: service(),
|
||||
displayGravatar: notEmpty('validEmail'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
// Fire this immediately in case we're initialized with a valid email
|
||||
this.trySetValidEmail();
|
||||
},
|
||||
|
||||
defaultImage: computed('ghostPaths', function () {
|
||||
let url = `${this.get('ghostPaths.subdir')}/ghost/img/user-image.png`;
|
||||
return Ember.String.htmlSafe(`background-image: url(${url})`);
|
||||
}),
|
||||
|
||||
trySetValidEmail() {
|
||||
if (!this.get('isDestroyed')) {
|
||||
let email = this.get('email');
|
||||
this.set('validEmail', validator.isEmail(email) ? email : '');
|
||||
}
|
||||
},
|
||||
|
||||
didReceiveAttrs(attrs) {
|
||||
this._super(...arguments);
|
||||
let timeout = parseInt(attrs.newAttrs.throttle || this.get('debounce'));
|
||||
run.debounce(this, 'trySetValidEmail', timeout);
|
||||
},
|
||||
|
||||
imageBackground: computed('validEmail', 'size', function () {
|
||||
let email = this.get('validEmail');
|
||||
let size = this.get('size');
|
||||
let style = '';
|
||||
|
||||
if (!isBlank(email)) {
|
||||
let gravatarUrl = `//www.gravatar.com/avatar/${window.md5(email)}?s=${size}&d=404`;
|
||||
|
||||
this.get('ajax').request(gravatarUrl)
|
||||
.catch((error) => {
|
||||
let defaultImageUrl = `url("${this.get('ghostPaths.subdir')}/ghost/img/user-image.png")`;
|
||||
|
||||
if (error instanceof NotFoundError) {
|
||||
this.$('.placeholder-img')[0].style.backgroundImage = Ember.String.htmlSafe(defaultImageUrl);
|
||||
} else {
|
||||
this.$('.placeholder-img')[0].style.backgroundImage = 'url()';
|
||||
}
|
||||
});
|
||||
|
||||
style = `background-image: url(${gravatarUrl})`;
|
||||
}
|
||||
return Ember.String.htmlSafe(style);
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
let size = this.get('size');
|
||||
let uploadElement = this.$('.js-file-input');
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
// while theoretically the 'add' and 'processalways' functions could be
|
||||
// added as properties of the hash passed to fileupload(), for some reason
|
||||
// they needed to be placed in an on() call for the add method to work correctly
|
||||
uploadElement.fileupload({
|
||||
url: this.get('ghostPaths.url').api('uploads'),
|
||||
dropZone: this.$('.js-img-dropzone'),
|
||||
previewMaxHeight: size,
|
||||
previewMaxWidth: size,
|
||||
previewCrop: true,
|
||||
maxNumberOfFiles: 1,
|
||||
autoUpload: false
|
||||
})
|
||||
.on('fileuploadadd', run.bind(this, this.queueFile))
|
||||
.on('fileuploadprocessalways', run.bind(this, this.triggerPreview));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
let $input = this.$('.js-file-input');
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
if ($input.length && $input.data()['blueimp-fileupload']) {
|
||||
$input.fileupload('destroy');
|
||||
}
|
||||
},
|
||||
|
||||
queueFile(e, data) {
|
||||
let fileName = data.files[0].name;
|
||||
|
||||
if ((/\.(gif|jpe?g|png|svg?z)$/i).test(fileName)) {
|
||||
this.sendAction('setImage', data);
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
// potential todo: rename 'gravatar-img' class in the CSS to be something
|
||||
// that both the gravatar and the image preview can use that's not so confusing
|
||||
this.$('.js-img-preview').empty().append(this.$(file.preview).addClass('gravatar-img'));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,206 +0,0 @@
|
|||
/* global key */
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
RSVP,
|
||||
computed,
|
||||
run,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
isEmpty
|
||||
} = Ember;
|
||||
|
||||
export function computedGroup(category) {
|
||||
return computed('content', 'currentSearch', function () {
|
||||
if (!this.get('currentSearch') || !this.get('content')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.get('content').filter((item) => {
|
||||
let search = new RegExp(this.get('currentSearch'), 'ig');
|
||||
|
||||
return (item.category === category) &&
|
||||
item.title.match(search);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
selection: null,
|
||||
content: [],
|
||||
isLoading: false,
|
||||
contentExpiry: 10 * 1000,
|
||||
contentExpiresAt: false,
|
||||
currentSearch: '',
|
||||
|
||||
posts: computedGroup('Posts'),
|
||||
pages: computedGroup('Pages'),
|
||||
users: computedGroup('Users'),
|
||||
tags: computedGroup('Tags'),
|
||||
|
||||
_store: service('store'),
|
||||
_routing: service('-routing'),
|
||||
ajax: service(),
|
||||
|
||||
refreshContent() {
|
||||
let promises = [];
|
||||
let now = new Date();
|
||||
let contentExpiry = this.get('contentExpiry');
|
||||
let contentExpiresAt = this.get('contentExpiresAt');
|
||||
|
||||
if (this.get('isLoading') || contentExpiresAt > now) {
|
||||
return RSVP.resolve();
|
||||
}
|
||||
|
||||
this.set('isLoading', true);
|
||||
this.set('content', []);
|
||||
promises.pushObject(this._loadPosts());
|
||||
promises.pushObject(this._loadUsers());
|
||||
promises.pushObject(this._loadTags());
|
||||
|
||||
return RSVP.all(promises).then(() => { }).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
this.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
|
||||
});
|
||||
},
|
||||
|
||||
groupedContent: computed('posts', 'pages', 'users', 'tags', function () {
|
||||
let groups = [];
|
||||
|
||||
if (!isEmpty(this.get('posts'))) {
|
||||
groups.pushObject({groupName: 'Posts', options: this.get('posts')});
|
||||
}
|
||||
|
||||
if (!isEmpty(this.get('pages'))) {
|
||||
groups.pushObject({groupName: 'Pages', options: this.get('pages')});
|
||||
}
|
||||
|
||||
if (!isEmpty(this.get('users'))) {
|
||||
groups.pushObject({groupName: 'Users', options: this.get('users')});
|
||||
}
|
||||
|
||||
if (!isEmpty(this.get('tags'))) {
|
||||
groups.pushObject({groupName: 'Tags', options: this.get('tags')});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}),
|
||||
|
||||
_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 this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
|
||||
|
||||
content.pushObjects(posts.posts.map((post) => {
|
||||
return {
|
||||
id: `post.${post.id}`,
|
||||
title: post.title,
|
||||
category: post.page ? 'Pages' : 'Posts'
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
_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 this.get('ajax').request(usersUrl, {data: usersQuery}).then((users) => {
|
||||
content.pushObjects(users.users.map((user) => {
|
||||
return {
|
||||
id: `user.${user.slug}`,
|
||||
title: user.name,
|
||||
category: 'Users'
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
_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 this.get('ajax').request(tagsUrl, {data: tagsQuery}).then((tags) => {
|
||||
content.pushObjects(tags.tags.map((tag) => {
|
||||
return {
|
||||
id: `tag.${tag.slug}`,
|
||||
title: tag.name,
|
||||
category: 'Tags'
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
_performSearch(term, resolve, reject) {
|
||||
if (isBlank(term)) {
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
this.refreshContent().then(() => {
|
||||
this.set('currentSearch', term);
|
||||
|
||||
return resolve(this.get('groupedContent'));
|
||||
}).catch(reject);
|
||||
},
|
||||
|
||||
_setKeymasterScope() {
|
||||
key.setScope('search-input');
|
||||
},
|
||||
|
||||
_resetKeymasterScope() {
|
||||
key.setScope('default');
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
this._super(...arguments);
|
||||
this._resetKeymasterScope();
|
||||
},
|
||||
|
||||
actions: {
|
||||
openSelected(selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.category === 'Posts' || selected.category === 'Pages') {
|
||||
let id = selected.id.replace('post.', '');
|
||||
this.get('_routing.router').transitionTo('editor.edit', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Users') {
|
||||
let id = selected.id.replace('user.', '');
|
||||
this.get('_routing.router').transitionTo('team.user', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Tags') {
|
||||
let id = selected.id.replace('tag.', '');
|
||||
this.get('_routing.router').transitionTo('settings.tags.tag', id);
|
||||
}
|
||||
},
|
||||
|
||||
onFocus() {
|
||||
this._setKeymasterScope();
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
this._resetKeymasterScope();
|
||||
},
|
||||
|
||||
search(term) {
|
||||
return new RSVP.Promise((resolve, reject) => {
|
||||
run.debounce(this, this._performSearch, term, resolve, reject, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {run, isBlank, Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
open() {
|
||||
this.get('select.actions').open();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.get('select.actions').close();
|
||||
},
|
||||
|
||||
actions: {
|
||||
captureMouseDown(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
search(term) {
|
||||
if (isBlank(term) === this.get('select.isOpen')) {
|
||||
run.scheduleOnce('afterRender', this, isBlank(term) ? this.close : this.open);
|
||||
}
|
||||
|
||||
invokeAction(this, 'select.actions.search', term);
|
||||
},
|
||||
|
||||
focusInput() {
|
||||
this.$('input')[0].focus();
|
||||
},
|
||||
|
||||
resetInput() {
|
||||
this.$('input').val('');
|
||||
},
|
||||
|
||||
handleKeydown(e) {
|
||||
let select = this.get('select');
|
||||
if (!select.isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
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: K, // action to fire on change
|
||||
|
||||
// shadow the passed-in `selection` to avoid
|
||||
// leaking changes to it via a 2-way binding
|
||||
_selection: reads('selection'),
|
||||
|
||||
actions: {
|
||||
change() {
|
||||
// jscs:disable requireArrayDestructuring
|
||||
let selectEl = this.$('select')[0];
|
||||
// jscs:enable requireArrayDestructuring
|
||||
let {selectedIndex} = selectEl;
|
||||
|
||||
// decrement index by 1 if we have a prompt
|
||||
let hasPrompt = !!this.get('prompt');
|
||||
let contentIndex = hasPrompt ? selectedIndex - 1 : selectedIndex;
|
||||
|
||||
let selection = this.get('content').objectAt(contentIndex);
|
||||
|
||||
// set the local, shadowed selection to avoid leaking
|
||||
// changes to `selection` out via 2-way binding
|
||||
this.set('_selection', selection);
|
||||
|
||||
this.sendAction('action', selection);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,124 +0,0 @@
|
|||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
import Ember from 'ember';
|
||||
import EmberSelectizeComponent from 'ember-cli-selectize/components/ember-selectize';
|
||||
|
||||
const {computed, isArray, isBlank, get, run} = Ember;
|
||||
const emberA = Ember.A;
|
||||
|
||||
export default EmberSelectizeComponent.extend({
|
||||
|
||||
selectizeOptions: computed(function () {
|
||||
let options = this._super(...arguments);
|
||||
|
||||
options.onChange = run.bind(this, '_onChange');
|
||||
|
||||
return options;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Event callback that is triggered when user creates a tag
|
||||
* - modified to pass the caret position to the action
|
||||
*/
|
||||
_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
|
||||
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
|
||||
// in the content and selection property
|
||||
callback(null);
|
||||
},
|
||||
|
||||
_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
|
||||
// to the right
|
||||
caret = caret - 1;
|
||||
|
||||
this.get('selection').insertAt(caret, obj);
|
||||
|
||||
run.schedule('actions', this, function () {
|
||||
this.sendAction('add-item', obj);
|
||||
this.sendAction('add-value', val);
|
||||
});
|
||||
},
|
||||
|
||||
_onChange(args) {
|
||||
let selection = Ember.get(this, 'selection');
|
||||
let valuePath = Ember.get(this, '_valuePath');
|
||||
let reorderedSelection = emberA([]);
|
||||
|
||||
if (!args || !selection || !isArray(selection) || args.length !== get(selection, 'length')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// exit if we're not dealing with the same objects as the selection
|
||||
let objectsHaveChanged = selection.any(function (obj) {
|
||||
return args.indexOf(get(obj, valuePath)) === -1;
|
||||
});
|
||||
|
||||
if (objectsHaveChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// exit if the order is still the same
|
||||
let orderIsSame = selection.every(function (obj, idx) {
|
||||
return get(obj, valuePath) === args[idx];
|
||||
});
|
||||
|
||||
if (orderIsSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have a re-order, update the selection
|
||||
args.forEach((value) => {
|
||||
let obj = selection.find(function (item) {
|
||||
return `${get(item, valuePath)}` === value;
|
||||
});
|
||||
|
||||
if (obj) {
|
||||
reorderedSelection.addObject(obj);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('selection', reorderedSelection);
|
||||
},
|
||||
|
||||
_preventOpeningWhenBlank() {
|
||||
let openOnFocus = this.get('openOnFocus');
|
||||
|
||||
if (!openOnFocus) {
|
||||
run.schedule('afterRender', this, function () {
|
||||
let selectize = this._selectize;
|
||||
if (selectize) {
|
||||
selectize.on('dropdown_open', function () {
|
||||
if (isBlank(selectize.$control_input.val())) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
selectize.on('type', function (filter) {
|
||||
if (isBlank(filter)) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._preventOpeningWhenBlank();
|
||||
}
|
||||
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
/*jshint scripturl:true*/
|
||||
import Ember from 'ember';
|
||||
|
||||
const {$, Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'a',
|
||||
anchor: '',
|
||||
classNames: ['sr-only', 'sr-only-focusable'],
|
||||
// Add attributes to component for href
|
||||
// href should be set to retain anchor properties
|
||||
// such as pointer cursor and text underline
|
||||
attributeBindings: ['href'],
|
||||
// Used so that upon clicking on the link
|
||||
// anchor behaviors or ignored
|
||||
href: Ember.String.htmlSafe('javascript:;'),
|
||||
|
||||
click() {
|
||||
let anchor = this.get('anchor');
|
||||
let $el = Ember.$(anchor);
|
||||
|
||||
if ($el) {
|
||||
// Scrolls to the top of main content or whatever
|
||||
// is passed to the anchor attribute
|
||||
Ember.$('body').scrollTop($el.offset().top);
|
||||
|
||||
// This sets focus on the content which was skipped to
|
||||
// upon losing focus, the tabindex should be removed
|
||||
// so that normal keyboard navigation picks up from focused
|
||||
// element
|
||||
Ember.$($el).attr('tabindex', -1).on('blur focusout', function () {
|
||||
$(this).removeAttr('tabindex');
|
||||
}).focus();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, computed, observer, run} = Ember;
|
||||
const {equal} = computed;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'button',
|
||||
buttonText: '',
|
||||
submitting: false,
|
||||
showSpinner: false,
|
||||
showSpinnerTimeout: null,
|
||||
autoWidth: true,
|
||||
|
||||
// Disable Button when isLoading equals true
|
||||
attributeBindings: ['disabled', 'type', 'tabindex'],
|
||||
|
||||
// Must be set on the controller
|
||||
disabled: equal('showSpinner', true),
|
||||
|
||||
click() {
|
||||
if (this.get('action')) {
|
||||
this.sendAction('action');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
toggleSpinner: observer('submitting', function () {
|
||||
let submitting = this.get('submitting');
|
||||
let timeout = this.get('showSpinnerTimeout');
|
||||
|
||||
if (submitting) {
|
||||
this.set('showSpinner', true);
|
||||
this.set('showSpinnerTimeout', run.later(this, function () {
|
||||
if (!this.get('submitting')) {
|
||||
this.set('showSpinner', false);
|
||||
}
|
||||
this.set('showSpinnerTimeout', null);
|
||||
}, 1000));
|
||||
} else if (!submitting && timeout === null) {
|
||||
this.set('showSpinner', false);
|
||||
}
|
||||
}),
|
||||
|
||||
setSize: observer('showSpinner', function () {
|
||||
if (this.get('showSpinner') && this.get('autoWidth')) {
|
||||
this.$().width(this.$().width());
|
||||
this.$().height(this.$().height());
|
||||
} else {
|
||||
this.$().width('');
|
||||
this.$().height('');
|
||||
}
|
||||
}),
|
||||
|
||||
willDestroy() {
|
||||
this._super(...arguments);
|
||||
run.cancel(this.get('showSpinnerTimeout'));
|
||||
}
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['subscribers-table'],
|
||||
|
||||
table: null,
|
||||
|
||||
actions: {
|
||||
onScrolledToBottom() {
|
||||
let loadNextPage = this.get('loadNextPage');
|
||||
|
||||
if (!this.get('isLoading')) {
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
// See gh-tabs-manager.js for use
|
||||
export default Component.extend({
|
||||
classNameBindings: ['active'],
|
||||
|
||||
tabsManager: computed(function () {
|
||||
return this.nearestWithProperty('isTabsManager');
|
||||
}),
|
||||
|
||||
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: alias('tab.active'),
|
||||
|
||||
willRender() {
|
||||
this._super(...arguments);
|
||||
// Register with the tabs manager
|
||||
this.get('tabsManager').registerTabPane(this);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
// Deregister with the tabs manager
|
||||
this.get('tabsManager').unregisterTabPane(this);
|
||||
}
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component, computed} = Ember;
|
||||
|
||||
// See gh-tabs-manager.js for use
|
||||
export default Component.extend({
|
||||
tabsManager: computed(function () {
|
||||
return this.nearestWithProperty('isTabsManager');
|
||||
}),
|
||||
|
||||
active: computed('tabsManager.activeTab', function () {
|
||||
return this.get('tabsManager.activeTab') === this;
|
||||
}),
|
||||
|
||||
index: computed('tabsManager.tabs.[]', function () {
|
||||
return this.get('tabsManager.tabs').indexOf(this);
|
||||
}),
|
||||
|
||||
// Select on click
|
||||
click() {
|
||||
this.get('tabsManager').select(this);
|
||||
},
|
||||
|
||||
willRender() {
|
||||
this._super(...arguments);
|
||||
// register the tabs with the tab manager
|
||||
this.get('tabsManager').registerTab(this);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
// unregister the tabs with the tab manager
|
||||
this.get('tabsManager').unregisterTab(this);
|
||||
}
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
/**
|
||||
Heavily inspired by ic-tabs (https://github.com/instructure/ic-tabs)
|
||||
|
||||
Three components work together for smooth tabbing.
|
||||
1. tabs-manager (gh-tabs)
|
||||
2. tab (gh-tab)
|
||||
3. tab-pane (gh-tab-pane)
|
||||
|
||||
## Usage:
|
||||
The tabs-manager must wrap all tab and tab-pane components,
|
||||
but they can be nested at any level.
|
||||
|
||||
A tab and its pane are tied together via their order.
|
||||
So, the second tab within a tab manager will activate
|
||||
the second pane within that manager.
|
||||
|
||||
```hbs
|
||||
{{#gh-tabs-manager}}
|
||||
{{#gh-tab}}
|
||||
First tab
|
||||
{{/gh-tab}}
|
||||
{{#gh-tab}}
|
||||
Second tab
|
||||
{{/gh-tab}}
|
||||
|
||||
....
|
||||
{{#gh-tab-pane}}
|
||||
First pane
|
||||
{{/gh-tab-pane}}
|
||||
{{#gh-tab-pane}}
|
||||
Second pane
|
||||
{{/gh-tab-pane}}
|
||||
{{/gh-tabs-manager}}
|
||||
```
|
||||
## Options:
|
||||
|
||||
the tabs-manager will send a "selected" action whenever one of its
|
||||
tabs is clicked.
|
||||
```hbs
|
||||
{{#gh-tabs-manager selected="myAction"}}
|
||||
....
|
||||
{{/gh-tabs-manager}}
|
||||
```
|
||||
|
||||
## Styling:
|
||||
Both tab and tab-pane elements have an "active"
|
||||
class applied when they are active.
|
||||
|
||||
*/
|
||||
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(tab) {
|
||||
this.set('activeTab', tab);
|
||||
this.sendAction('selected');
|
||||
},
|
||||
|
||||
// Register tabs and their panes to allow for
|
||||
// interaction between components.
|
||||
registerTab(tab) {
|
||||
this.get('tabs').addObject(tab);
|
||||
},
|
||||
|
||||
unregisterTab(tab) {
|
||||
this.get('tabs').removeObject(tab);
|
||||
},
|
||||
|
||||
registerTabPane(tabPane) {
|
||||
this.get('tabPanes').addObject(tabPane);
|
||||
},
|
||||
|
||||
unregisterTabPane(tabPane) {
|
||||
this.get('tabPanes').removeObject(tabPane);
|
||||
}
|
||||
});
|
|
@ -1,136 +0,0 @@
|
|||
/* global key */
|
||||
import Ember from 'ember';
|
||||
import boundOneWay from 'ghost/utils/bound-one-way';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {
|
||||
Component,
|
||||
Handlebars,
|
||||
computed,
|
||||
get,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
const {reads} = computed;
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
tag: null,
|
||||
|
||||
scratchName: boundOneWay('tag.name'),
|
||||
scratchSlug: boundOneWay('tag.slug'),
|
||||
scratchDescription: boundOneWay('tag.description'),
|
||||
scratchMetaTitle: boundOneWay('tag.metaTitle'),
|
||||
scratchMetaDescription: boundOneWay('tag.metaDescription'),
|
||||
|
||||
isViewingSubview: false,
|
||||
|
||||
config: service(),
|
||||
mediaQueries: service(),
|
||||
|
||||
isMobile: reads('mediaQueries.maxWidth600'),
|
||||
|
||||
title: computed('tag.isNew', function () {
|
||||
if (this.get('tag.isNew')) {
|
||||
return 'New Tag';
|
||||
} else {
|
||||
return 'Tag Settings';
|
||||
}
|
||||
}),
|
||||
|
||||
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 = Handlebars.Utils.escapeExpression(metaTitle);
|
||||
metaTitle = Ember.String.htmlSafe(`${metaTitle}…`);
|
||||
}
|
||||
|
||||
return metaTitle;
|
||||
}),
|
||||
|
||||
seoURL: computed('scratchSlug', function () {
|
||||
let blogUrl = this.get('config.blogUrl');
|
||||
let seoSlug = this.get('scratchSlug') || '';
|
||||
|
||||
let seoURL = `${blogUrl}/tag/${seoSlug}`;
|
||||
|
||||
// only append a slash to the URL if the slug exists
|
||||
if (seoSlug) {
|
||||
seoURL += '/';
|
||||
}
|
||||
|
||||
if (seoURL.length > 70) {
|
||||
seoURL = seoURL.substring(0, 70).trim();
|
||||
seoURL = Ember.String.htmlSafe(`${seoURL}…`);
|
||||
}
|
||||
|
||||
return seoURL;
|
||||
}),
|
||||
|
||||
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 = Handlebars.Utils.escapeExpression(metaDescription);
|
||||
metaDescription = Ember.String.htmlSafe(`${metaDescription}…`);
|
||||
}
|
||||
|
||||
return metaDescription;
|
||||
}),
|
||||
|
||||
didReceiveAttrs(attrs) {
|
||||
this._super(...arguments);
|
||||
|
||||
if (get(attrs, 'newAttrs.tag.value.id') !== get(attrs, 'oldAttrs.tag.value.id')) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('isViewingSubview', false);
|
||||
if (this.$()) {
|
||||
this.$('.settings-menu-pane').scrollTop(0);
|
||||
}
|
||||
},
|
||||
|
||||
focusIn() {
|
||||
key.setScope('tag-settings-form');
|
||||
},
|
||||
|
||||
focusOut() {
|
||||
key.setScope('default');
|
||||
},
|
||||
|
||||
actions: {
|
||||
setProperty(property, value) {
|
||||
invokeAction(this, 'setProperty', property, value);
|
||||
},
|
||||
|
||||
setCoverImage(image) {
|
||||
invokeAction(this, 'setProperty', 'image', image);
|
||||
},
|
||||
|
||||
clearCoverImage() {
|
||||
invokeAction(this, 'setProperty', 'image', '');
|
||||
},
|
||||
|
||||
openMeta() {
|
||||
this.set('isViewingSubview', true);
|
||||
},
|
||||
|
||||
closeMeta() {
|
||||
this.set('isViewingSubview', false);
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
invokeAction(this, 'showDeleteTagModal');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.get('tag.isDeleted') && this.get('onDelete')) {
|
||||
invokeAction(this, 'onDelete');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
observer,
|
||||
run
|
||||
} = Ember;
|
||||
const {equal, reads} = computed;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['view-container'],
|
||||
classNameBindings: ['isMobile'],
|
||||
|
||||
mediaQueries: service(),
|
||||
|
||||
tags: null,
|
||||
selectedTag: null,
|
||||
|
||||
isMobile: reads('mediaQueries.maxWidth600'),
|
||||
isEmpty: equal('tags.length', 0),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
run.schedule('actions', this, this.fireMobileChangeActions);
|
||||
},
|
||||
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// display list if no tag is selected on mobile
|
||||
if (isMobile && isBlank(selectedTag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// default to displaying settings pane
|
||||
return true;
|
||||
}),
|
||||
|
||||
fireMobileChangeActions: observer('isMobile', function () {
|
||||
if (!this.get('isMobile')) {
|
||||
this.sendAction('leftMobile');
|
||||
}
|
||||
})
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import TextInputMixin from 'ghost/mixins/text-input';
|
||||
|
||||
const {TextArea} = Ember;
|
||||
|
||||
export default TextArea.extend(TextInputMixin, {
|
||||
classNames: 'gh-input'
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
/*global device*/
|
||||
import Ember from 'ember';
|
||||
|
||||
const {TextField, computed} = Ember;
|
||||
|
||||
export default TextField.extend({
|
||||
focus: true,
|
||||
classNames: 'gh-input',
|
||||
attributeBindings: ['autofocus'],
|
||||
|
||||
autofocus: computed(function () {
|
||||
if (this.get('focus')) {
|
||||
return (device.ios()) ? false : 'autofocus';
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
_focusField() {
|
||||
// This fix is required until Mobile Safari has reliable
|
||||
// autofocus, select() or focus() support
|
||||
if (this.get('focus') && !device.ios()) {
|
||||
this.$().val(this.$().val()).focus();
|
||||
}
|
||||
},
|
||||
|
||||
_trimValue() {
|
||||
let text = this.$().val();
|
||||
this.$().val(text.trim());
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._focusField();
|
||||
},
|
||||
|
||||
focusOut() {
|
||||
this._super(...arguments);
|
||||
this._trimValue();
|
||||
}
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
/*
|
||||
Example usage:
|
||||
{{gh-url-preview prefix="tag" slug=theSlugValue tagName="p" classNames="description"}}
|
||||
*/
|
||||
export default Component.extend({
|
||||
classNames: 'ghost-url-preview',
|
||||
prefix: null,
|
||||
slug: null,
|
||||
|
||||
config: service(),
|
||||
|
||||
url: computed('slug', function () {
|
||||
// Get the blog URL and strip the scheme
|
||||
let blogUrl = this.get('config.blogUrl');
|
||||
// Remove `http[s]://`
|
||||
let noSchemeBlogUrl = blogUrl.substr(blogUrl.indexOf('://') + 3);
|
||||
|
||||
// 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
|
||||
let theUrl = `${noSchemeBlogUrl}/${prefix}${slug}`;
|
||||
|
||||
return theUrl;
|
||||
})
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
user: null,
|
||||
|
||||
ghostPaths: service(),
|
||||
|
||||
userDefault: computed('ghostPaths', function () {
|
||||
return `${this.get('ghostPaths.subdir')}/ghost/img/user-image.png`;
|
||||
}),
|
||||
|
||||
userImageBackground: computed('user.image', 'userDefault', function () {
|
||||
let url = this.get('user.image') || this.get('userDefault');
|
||||
|
||||
return Ember.String.htmlSafe(`background-image: url(${url})`);
|
||||
}),
|
||||
|
||||
lastLogin: computed('user.lastLogin', function () {
|
||||
let lastLogin = this.get('user.lastLogin');
|
||||
|
||||
return lastLogin ? lastLogin.fromNow() : '(Never)';
|
||||
})
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
user: null,
|
||||
isSending: false,
|
||||
|
||||
notifications: service(),
|
||||
|
||||
createdAt: computed('user.createdAt', function () {
|
||||
let createdAt = this.get('user.createdAt');
|
||||
|
||||
return createdAt ? createdAt.fromNow() : '';
|
||||
}),
|
||||
|
||||
actions: {
|
||||
resend() {
|
||||
let user = this.get('user');
|
||||
let notifications = this.get('notifications');
|
||||
|
||||
this.set('isSending', true);
|
||||
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'.
|
||||
if (result.users[0].status === 'invited-pending') {
|
||||
notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.resend.not-sent'});
|
||||
} else {
|
||||
user.set('status', result.users[0].status);
|
||||
notifications.showNotification(notificationText, {key: 'invite.resend.success'});
|
||||
}
|
||||
}).catch((error) => {
|
||||
notifications.showAPIError(error, {key: 'invite.resend'});
|
||||
}).finally(() => {
|
||||
this.set('isSending', false);
|
||||
});
|
||||
},
|
||||
|
||||
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(() => {
|
||||
if (user.get('invited')) {
|
||||
user.destroyRecord().then(() => {
|
||||
let notificationText = `Invitation revoked. (${email})`;
|
||||
notifications.showNotification(notificationText, {key: 'invite.revoke.success'});
|
||||
}).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
|
||||
this.sendAction('reload');
|
||||
notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true, key: 'invite.revoke.already-accepted'});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
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
|
||||
* the specified property, it will change the CSS to reflect the error state
|
||||
* @param {DS.Errors} errors The DS.Errors object
|
||||
* @param {string} property Name of the property
|
||||
*/
|
||||
export default Component.extend(ValidationStateMixin, {
|
||||
classNameBindings: ['errorClass'],
|
||||
|
||||
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';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Component} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'h2',
|
||||
classNames: ['view-title'],
|
||||
|
||||
actions: {
|
||||
openMobileMenu() {
|
||||
this.sendAction('openMobileMenu');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
/* global key */
|
||||
import Ember from 'ember';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {Component, run} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: 'modal-content',
|
||||
|
||||
_previousKeymasterScope: null,
|
||||
|
||||
_setupShortcuts() {
|
||||
run(function () {
|
||||
document.activeElement.blur();
|
||||
});
|
||||
this._previousKeymasterScope = key.getScope();
|
||||
|
||||
key('enter', 'modal', () => {
|
||||
this.send('confirm');
|
||||
});
|
||||
|
||||
key('escape', 'modal', () => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
||||
key.setScope('modal');
|
||||
},
|
||||
|
||||
_removeShortcuts() {
|
||||
key.unbind('enter', 'modal');
|
||||
key.unbind('escape', 'modal');
|
||||
|
||||
key.setScope(this._previousKeymasterScope);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._setupShortcuts();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this._removeShortcuts();
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
throw new Error('You must override the "confirm" action in your modal component');
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
invokeAction(this, 'closeModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
const {computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
generatedHtml: alias('model')
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
const {
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
ajax: service(),
|
||||
|
||||
_deleteAll() {
|
||||
let deleteUrl = this.get('ghostPaths.url').api('db');
|
||||
return this.get('ajax').del(deleteUrl);
|
||||
},
|
||||
|
||||
_unloadData() {
|
||||
this.get('store').unloadAll('post');
|
||||
this.get('store').unloadAll('tag');
|
||||
},
|
||||
|
||||
_showSuccess() {
|
||||
this.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'});
|
||||
},
|
||||
|
||||
_showFailure(error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'all-content.delete'});
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
this._deleteAll().then(() => {
|
||||
this._unloadData();
|
||||
this._showSuccess();
|
||||
}).catch((error) => {
|
||||
this._showFailure(error);
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
const {
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
const {alias} = computed;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
post: alias('model'),
|
||||
|
||||
notifications: service(),
|
||||
routing: service('-routing'),
|
||||
|
||||
_deletePost() {
|
||||
let post = this.get('post');
|
||||
|
||||
// definitely want to clear the data store and post of any unsaved,
|
||||
// client-generated tags
|
||||
post.updateTags();
|
||||
|
||||
return post.destroyRecord();
|
||||
},
|
||||
|
||||
_success() {
|
||||
// clear any previous error messages
|
||||
this.get('notifications').closeAlerts('post.delete');
|
||||
|
||||
// redirect to content screen
|
||||
this.get('routing').transitionTo('posts');
|
||||
},
|
||||
|
||||
_failure() {
|
||||
this.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'});
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
this._deletePost().then(() => {
|
||||
this._success();
|
||||
}, () => {
|
||||
this._failure();
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
subscriber: alias('model'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.set('submitting', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
tag: alias('model'),
|
||||
|
||||
postInflection: computed('tag.count.posts', function () {
|
||||
return this.get('tag.count.posts') > 1 ? 'posts' : 'post';
|
||||
}),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
user: null,
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import { invokeAction } from 'ember-invoke-action';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
|
||||
const {computed} = Ember;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
labelText: 'Select or drag-and-drop a CSV File',
|
||||
|
||||
response: null,
|
||||
closeDisabled: false,
|
||||
|
||||
uploadUrl: computed(function () {
|
||||
return `${ghostPaths().apiRoot}/subscribers/csv/`;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
uploadStarted() {
|
||||
this.set('closeDisabled', true);
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
this.set('closeDisabled', false);
|
||||
},
|
||||
|
||||
uploadSuccess(response) {
|
||||
this.set('response', response.meta.stats);
|
||||
// invoke the passed in confirm action
|
||||
invokeAction(this, 'confirm');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
// noop - we don't want the enter key doing anything
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
if (!this.get('closeDisabled')) {
|
||||
this._super(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,125 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
const {
|
||||
RSVP: {Promise},
|
||||
inject: {service},
|
||||
run
|
||||
} = Ember;
|
||||
const emberA = Ember.A;
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
classNames: 'modal-content invite-new-user',
|
||||
|
||||
role: null,
|
||||
roles: null,
|
||||
authorRole: null,
|
||||
submitting: false,
|
||||
|
||||
validationType: 'inviteUser',
|
||||
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
// populate roles and set initial value for the dropdown
|
||||
run.schedule('afterRender', this, function () {
|
||||
this.get('store').query('role', {permissions: 'assign'}).then((roles) => {
|
||||
let authorRole = roles.findBy('name', 'Author');
|
||||
|
||||
this.set('roles', roles);
|
||||
this.set('authorRole', authorRole);
|
||||
|
||||
if (!this.get('role')) {
|
||||
this.set('role', authorRole);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
// TODO: this should not be needed, ValidationEngine acts as a
|
||||
// singleton and so it's errors and hasValidated state stick around
|
||||
this.get('errors').clear();
|
||||
this.set('hasValidated', emberA());
|
||||
},
|
||||
|
||||
validate() {
|
||||
let email = this.get('email');
|
||||
|
||||
// TODO: either the validator should check the email's existence or
|
||||
// the API should return an appropriate error when attempting to save
|
||||
return new Promise((resolve, reject) => {
|
||||
return this._super().then(() => {
|
||||
this.get('store').findAll('user', {reload: true}).then((result) => {
|
||||
let invitedUser = result.findBy('email', email);
|
||||
|
||||
if (invitedUser) {
|
||||
this.get('errors').clear('email');
|
||||
if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') {
|
||||
this.get('errors').add('email', 'A user with that email address was already invited.');
|
||||
} else {
|
||||
this.get('errors').add('email', 'A user with that email address already exists.');
|
||||
}
|
||||
|
||||
// TODO: this shouldn't be needed, ValidationEngine doesn't mark
|
||||
// properties as validated when validating an entire object
|
||||
this.get('hasValidated').addObject('email');
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
// TODO: this shouldn't be needed, ValidationEngine doesn't mark
|
||||
// properties as validated when validating an entire object
|
||||
this.get('hasValidated').addObject('email');
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
setRole(role) {
|
||||
this.set('role', role);
|
||||
},
|
||||
|
||||
confirm() {
|
||||
let email = this.get('email');
|
||||
let role = this.get('role');
|
||||
let notifications = this.get('notifications');
|
||||
let newUser;
|
||||
|
||||
this.validate().then(() => {
|
||||
this.set('submitting', true);
|
||||
|
||||
newUser = this.get('store').createRecord('user', {
|
||||
email,
|
||||
role,
|
||||
status: 'invited'
|
||||
});
|
||||
|
||||
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') {
|
||||
notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'});
|
||||
} else {
|
||||
notifications.showNotification(notificationText, {key: 'invite.send.success'});
|
||||
}
|
||||
}).catch((errors) => {
|
||||
newUser.deleteRecord();
|
||||
notifications.showErrors(errors, {key: 'invite.send'});
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
actions: {
|
||||
confirm() {
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
actions: {
|
||||
updateEmail(newEmail) {
|
||||
this.set('model.email', newEmail);
|
||||
this.set('model.hasValidated', Ember.A());
|
||||
this.get('model.errors').clear();
|
||||
},
|
||||
|
||||
confirm() {
|
||||
let confirmAction = this.get('confirm');
|
||||
|
||||
this.set('submitting', true);
|
||||
|
||||
confirmAction().then(() => {
|
||||
this.send('closeModal');
|
||||
}).catch((errors) => {
|
||||
let [error] = errors;
|
||||
if (error && error.match(/email/i)) {
|
||||
this.get('model.errors').add('email', error);
|
||||
this.get('model.hasValidated').pushObject('email');
|
||||
}
|
||||
}).finally(() => {
|
||||
if (!this.get('isDestroying') && !this.get('isDestroyed')) {
|
||||
this.set('submitting', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
const {
|
||||
$,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
validationType: 'signin',
|
||||
|
||||
submitting: false,
|
||||
authenticationError: null,
|
||||
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
|
||||
identification: computed('session.user.email', function () {
|
||||
return this.get('session.user.email');
|
||||
}),
|
||||
|
||||
_authenticate() {
|
||||
let session = this.get('session');
|
||||
let authStrategy = 'authenticator:oauth2';
|
||||
let identification = this.get('identification');
|
||||
let password = this.get('password');
|
||||
|
||||
session.set('skipAuthSuccessHandler', true);
|
||||
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
return session.authenticate(authStrategy, identification, password).finally(() => {
|
||||
this.toggleProperty('submitting');
|
||||
session.set('skipAuthSuccessHandler', undefined);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
// 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.set('authenticationError', null);
|
||||
|
||||
this.validate({property: 'signin'}).then(() => {
|
||||
this._authenticate().then(() => {
|
||||
this.get('notifications').closeAlerts('post.save');
|
||||
this.send('closeModal');
|
||||
}).catch((error) => {
|
||||
if (error && error.errors) {
|
||||
error.errors.forEach((err) => {
|
||||
err.message = Ember.String.htmlSafe(err.message);
|
||||
});
|
||||
|
||||
this.get('errors').add('password', 'Incorrect password');
|
||||
this.get('hasValidated').pushObject('password');
|
||||
this.set('authenticationError', error.errors[0].message);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
this.get('hasValidated').pushObject('password');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
user: null,
|
||||
submitting: false,
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,101 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
|
||||
|
||||
const {
|
||||
computed,
|
||||
inject: {service},
|
||||
isEmpty
|
||||
} = Ember;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
model: null,
|
||||
submitting: false,
|
||||
|
||||
url: '',
|
||||
newUrl: '',
|
||||
|
||||
config: service(),
|
||||
notifications: service(),
|
||||
|
||||
image: computed('model.model', 'model.imageProperty', {
|
||||
get() {
|
||||
let imageProperty = this.get('model.imageProperty');
|
||||
|
||||
return this.get(`model.model.${imageProperty}`);
|
||||
},
|
||||
|
||||
set(key, value) {
|
||||
let model = this.get('model.model');
|
||||
let imageProperty = this.get('model.imageProperty');
|
||||
|
||||
return model.set(imageProperty, value);
|
||||
}
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
let image = this.get('image');
|
||||
this.set('url', image);
|
||||
this.set('newUrl', image);
|
||||
},
|
||||
|
||||
// TODO: should validation be handled in the gh-image-uploader component?
|
||||
// pro - consistency everywhere, simplification here
|
||||
// con - difficult if the "save" is happening externally as it does here
|
||||
//
|
||||
// maybe it should be handled at the model level?
|
||||
// - automatically present everywhere
|
||||
// - file uploads should always result in valid urls so it should only
|
||||
// affect the url input form
|
||||
keyDown() {
|
||||
this._setErrorState(false);
|
||||
},
|
||||
|
||||
_setErrorState(state) {
|
||||
if (state) {
|
||||
this.$('.url').addClass('error');
|
||||
} else {
|
||||
this.$('.url').removeClass('error');
|
||||
}
|
||||
},
|
||||
|
||||
_validateUrl(url) {
|
||||
if (!isEmpty(url) && !cajaSanitizers.url(url)) {
|
||||
this._setErrorState(true);
|
||||
return {message: 'Image URI is not valid'};
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
// end validation
|
||||
|
||||
actions: {
|
||||
fileUploaded(url) {
|
||||
this.set('url', url);
|
||||
this.set('newUrl', url);
|
||||
},
|
||||
|
||||
removeImage() {
|
||||
this.set('url', '');
|
||||
this.set('newUrl', '');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
let model = this.get('model.model');
|
||||
let newUrl = this.get('newUrl');
|
||||
let result = this._validateUrl(newUrl);
|
||||
let notifications = this.get('notifications');
|
||||
|
||||
if (result === true) {
|
||||
this.set('submitting', true);
|
||||
this.set('image', newUrl);
|
||||
|
||||
model.save().catch((err) => {
|
||||
notifications.showAPIError(err, {key: 'image.upload'});
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Controller,
|
||||
computed
|
||||
} = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
updateNotificationCount: 0,
|
||||
|
||||
actions: {
|
||||
updateNotificationChange(count) {
|
||||
this.set('updateNotificationCount', count);
|
||||
}
|
||||
},
|
||||
|
||||
copyrightYear: computed(function () {
|
||||
let date = new Date();
|
||||
return date.getFullYear();
|
||||
})
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Controller,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
dropdown: service(),
|
||||
session: service(),
|
||||
|
||||
showNavMenu: computed('currentPath', 'session.isAuthenticated', function () {
|
||||
return (this.get('currentPath') !== 'error404' || this.get('session.isAuthenticated')) &&
|
||||
!this.get('currentPath').match(/(signin|signup|setup|reset)/);
|
||||
}),
|
||||
|
||||
topNotificationCount: 0,
|
||||
showMobileMenu: false,
|
||||
showSettingsMenu: false,
|
||||
showMarkdownHelpModal: false,
|
||||
|
||||
autoNav: false,
|
||||
autoNavOpen: computed('autoNav', {
|
||||
get() {
|
||||
return false;
|
||||
},
|
||||
set(key, value) {
|
||||
if (this.get('autoNav')) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
topNotificationChange(count) {
|
||||
this.set('topNotificationCount', count);
|
||||
},
|
||||
|
||||
toggleAutoNav() {
|
||||
this.toggleProperty('autoNav');
|
||||
},
|
||||
|
||||
openAutoNav() {
|
||||
this.set('autoNavOpen', true);
|
||||
},
|
||||
|
||||
closeAutoNav() {
|
||||
this.set('autoNavOpen', false);
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
this.set('showMobileMenu', false);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
||||
|
||||
const {Controller} = Ember;
|
||||
|
||||
export default Controller.extend(EditorControllerMixin, {
|
||||
showDeletePostModal: false,
|
||||
|
||||
actions: {
|
||||
toggleDeletePostModal() {
|
||||
this.toggleProperty('showDeletePostModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
||||
|
||||
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: K,
|
||||
actions: {
|
||||
/**
|
||||
* Redirect to editor after the first save
|
||||
*/
|
||||
save(options) {
|
||||
return this._super(options).then((model) => {
|
||||
if (model.get('id')) {
|
||||
this.replaceRoute('editor.edit', model);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
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: 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';
|
||||
})
|
||||
});
|
|
@ -1,491 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import {parseDateString} from 'ghost/utils/date-formatting';
|
||||
import SettingsMenuMixin from 'ghost/mixins/settings-menu-controller';
|
||||
import boundOneWay from 'ghost/utils/bound-one-way';
|
||||
import isNumber from 'ghost/utils/isNumber';
|
||||
|
||||
const {
|
||||
$,
|
||||
ArrayProxy,
|
||||
Controller,
|
||||
Handlebars,
|
||||
PromiseProxyMixin,
|
||||
RSVP,
|
||||
computed,
|
||||
guidFor,
|
||||
inject: {service, controller},
|
||||
isArray,
|
||||
isBlank,
|
||||
observer,
|
||||
run
|
||||
} = Ember;
|
||||
|
||||
export default Controller.extend(SettingsMenuMixin, {
|
||||
debounceId: null,
|
||||
lastPromise: null,
|
||||
selectedAuthor: null,
|
||||
|
||||
application: controller(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
slugGenerator: service(),
|
||||
|
||||
initializeSelectedAuthor: observer('model', function () {
|
||||
return this.get('model.author').then((author) => {
|
||||
this.set('selectedAuthor', author);
|
||||
return author;
|
||||
});
|
||||
}),
|
||||
|
||||
authors: computed(function () {
|
||||
// Loaded asynchronously, so must use promise proxies.
|
||||
let deferred = {};
|
||||
|
||||
deferred.promise = this.store.query('user', {limit: 'all'}).then((users) => {
|
||||
return users.rejectBy('id', 'me').sortBy('name');
|
||||
}).then((users) => {
|
||||
return users.filter((user) => {
|
||||
return user.get('active');
|
||||
});
|
||||
});
|
||||
|
||||
return ArrayProxy
|
||||
.extend(PromiseProxyMixin)
|
||||
.create(deferred);
|
||||
}),
|
||||
|
||||
slugValue: boundOneWay('model.slug'),
|
||||
|
||||
// Requests slug from title
|
||||
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 = RSVP.resolve(afterSave).then(() => {
|
||||
return this.get('slugGenerator').generateSlug('post', title).then((slug) => {
|
||||
if (!isBlank(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.
|
||||
});
|
||||
});
|
||||
|
||||
this.set('lastPromise', promise);
|
||||
},
|
||||
|
||||
metaTitleScratch: boundOneWay('model.metaTitle'),
|
||||
metaDescriptionScratch: boundOneWay('model.metaDescription'),
|
||||
|
||||
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 = Handlebars.Utils.escapeExpression(metaTitle);
|
||||
metaTitle = Ember.String.htmlSafe(`${metaTitle}…`);
|
||||
}
|
||||
|
||||
return metaTitle;
|
||||
}),
|
||||
|
||||
seoDescription: computed('model.scratch', 'metaDescriptionScratch', function () {
|
||||
let metaDescription = this.get('metaDescriptionScratch') || '';
|
||||
let html = '';
|
||||
let el, placeholder;
|
||||
|
||||
if (metaDescription.length > 0) {
|
||||
placeholder = metaDescription;
|
||||
} else {
|
||||
el = $('.rendered-markdown');
|
||||
|
||||
// Get rendered markdown
|
||||
if (el !== undefined && el.length > 0) {
|
||||
html = el.clone();
|
||||
html.find('.js-drop-zone').remove();
|
||||
html = html[0].innerHTML;
|
||||
}
|
||||
|
||||
// Strip HTML
|
||||
placeholder = $('<div />', {html}).text();
|
||||
// Replace new lines and trim
|
||||
placeholder = placeholder.replace(/\n+/g, ' ').trim();
|
||||
}
|
||||
|
||||
if (placeholder.length > 156) {
|
||||
// Limit to 156 characters
|
||||
placeholder = placeholder.substring(0, 156).trim();
|
||||
placeholder = Handlebars.Utils.escapeExpression(placeholder);
|
||||
placeholder = Ember.String.htmlSafe(`${placeholder}…`);
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}),
|
||||
|
||||
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) {
|
||||
seoURL += '/';
|
||||
}
|
||||
|
||||
if (seoURL.length > 70) {
|
||||
seoURL = seoURL.substring(0, 70).trim();
|
||||
seoURL = Ember.String.htmlSafe(`${seoURL}…`);
|
||||
}
|
||||
|
||||
return seoURL;
|
||||
}),
|
||||
|
||||
// observe titleScratch, keeping the post's slug in sync
|
||||
// with it until saved for the first time.
|
||||
addTitleObserver: observer('model', function () {
|
||||
if (this.get('model.isNew') || this.get('model.title') === '(Untitled)') {
|
||||
this.addObserver('model.titleScratch', this, 'titleObserver');
|
||||
}
|
||||
}),
|
||||
|
||||
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 = run.debounce(this, 'generateAndSetSlug', 'model.slug', 700);
|
||||
}
|
||||
|
||||
this.set('debounceId', debounceId);
|
||||
},
|
||||
|
||||
// live-query of all tags for tag input autocomplete
|
||||
availableTags: computed(function () {
|
||||
return this.get('store').filter('tag', {limit: 'all'}, () => {
|
||||
return true;
|
||||
});
|
||||
}),
|
||||
|
||||
showErrors(errors) {
|
||||
errors = isArray(errors) ? errors : [errors];
|
||||
this.get('notifications').showErrors(errors);
|
||||
},
|
||||
|
||||
actions: {
|
||||
discardEnter() {
|
||||
return false;
|
||||
},
|
||||
|
||||
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((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
toggleFeatured() {
|
||||
this.toggleProperty('model.featured');
|
||||
|
||||
// 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(this.get('saveOptions')).catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* triggered by user manually changing slug
|
||||
*/
|
||||
updateSlug(newSlug) {
|
||||
let slug = this.get('model.slug');
|
||||
|
||||
newSlug = newSlug || slug;
|
||||
newSlug = newSlug && newSlug.trim();
|
||||
|
||||
// Ignore unchanged slugs or candidate slugs that are empty
|
||||
if (!newSlug || slug === newSlug) {
|
||||
// reset the input to its previous state
|
||||
this.set('slugValue', slug);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('slugGenerator').generateSlug('post', 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Because the server transforms the candidate slug by stripping
|
||||
// certain characters and appending a number onto the end of slugs
|
||||
// to enforce uniqueness, there are cases where we can get back a
|
||||
// candidate slug that is a duplicate of the original except for
|
||||
// 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
|
||||
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) {
|
||||
this.set('slugValue', slug);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('model.slug', serverSlug);
|
||||
|
||||
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 (this.get('model.isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.get('model').save();
|
||||
}).catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse user's set published date.
|
||||
* Action sent by post settings menu view.
|
||||
* (#1351)
|
||||
*/
|
||||
setPublishedAt(userInput) {
|
||||
if (!userInput) {
|
||||
// Clear out the publishedAt field for a draft
|
||||
if (this.get('model.isDraft')) {
|
||||
this.set('model.publishedAt', null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let newPublishedAt = parseDateString(userInput);
|
||||
let publishedAt = moment(this.get('model.publishedAt'));
|
||||
let errMessage = '';
|
||||
|
||||
// Clear previous errors
|
||||
this.get('model.errors').remove('post-setting-date');
|
||||
|
||||
// Validate new Published date
|
||||
if (!newPublishedAt.isValid()) {
|
||||
errMessage = 'Published Date must be a valid date with format: ' +
|
||||
'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
|
||||
} else if (newPublishedAt.diff(new Date(), 'h') > 0) {
|
||||
errMessage = 'Published Date cannot currently be in the future.';
|
||||
}
|
||||
|
||||
// If errors, notify and exit.
|
||||
if (errMessage) {
|
||||
this.get('model.errors').add('post-setting-date', errMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation complete, update the view
|
||||
this.set('model.publishedAt', newPublishedAt);
|
||||
|
||||
// Don't save the date if the user didn't actually changed the date
|
||||
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
setMetaTitle(metaTitle) {
|
||||
let property = 'metaTitle';
|
||||
let model = this.get('model');
|
||||
let currentTitle = model.get(property) || '';
|
||||
|
||||
// Only update if the title has changed
|
||||
if (currentTitle === metaTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.set(property, metaTitle);
|
||||
|
||||
// If this is a new post. Don't save the model. Defer the save
|
||||
// to the user pressing the save button
|
||||
if (model.get('isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.save();
|
||||
},
|
||||
|
||||
setMetaDescription(metaDescription) {
|
||||
let property = 'metaDescription';
|
||||
let model = this.get('model');
|
||||
let currentDescription = model.get(property) || '';
|
||||
|
||||
// Only update if the description has changed
|
||||
if (currentDescription === metaDescription) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.set(property, metaDescription);
|
||||
|
||||
// If this is a new post. Don't save the model. Defer the save
|
||||
// to the user pressing the save button
|
||||
if (model.get('isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.save();
|
||||
},
|
||||
|
||||
setCoverImage(image) {
|
||||
this.set('model.image', image);
|
||||
|
||||
if (this.get('model.isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('model').save().catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
clearCoverImage() {
|
||||
this.set('model.image', '');
|
||||
|
||||
if (this.get('model.isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('model').save().catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
resetPubDate() {
|
||||
this.set('publishedAtValue', '');
|
||||
},
|
||||
|
||||
closeNavMenu() {
|
||||
this.get('application').send('closeNavMenu');
|
||||
},
|
||||
|
||||
changeAuthor(newAuthor) {
|
||||
let author = this.get('model.author');
|
||||
let model = this.get('model');
|
||||
|
||||
// return if nothing changed
|
||||
if (newAuthor.get('id') === author.get('id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.set('author', newAuthor);
|
||||
|
||||
// if this is a new post (never been saved before), don't try to save it
|
||||
if (this.get('model.isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.save().catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.set('selectedAuthor', author);
|
||||
model.rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
||||
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();
|
||||
|
||||
// abort if tag is already selected
|
||||
if (currentTagNames.contains(tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
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((tag) => {
|
||||
return tag.get('name').toLowerCase() === tagName.toLowerCase();
|
||||
});
|
||||
} else {
|
||||
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', guidFor(tagToAdd));
|
||||
}
|
||||
|
||||
// push tag onto post relationship
|
||||
if (tagToAdd) {
|
||||
this.get('model.tags').insertAt(index, tagToAdd);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeTag(tag) {
|
||||
this.get('model.tags').removeObject(tag);
|
||||
|
||||
if (tag.get('isNew')) {
|
||||
tag.destroyRecord();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
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
|
||||
// publishedAt: DESC
|
||||
// updatedAt: DESC
|
||||
// id: DESC
|
||||
function comparator(item1, item2) {
|
||||
let updated1 = item1.get('updatedAt');
|
||||
let updated2 = item2.get('updatedAt');
|
||||
let idResult,
|
||||
publishedAtResult,
|
||||
statusResult,
|
||||
updatedAtResult;
|
||||
|
||||
// when `updatedAt` is undefined, the model is still
|
||||
// being written to with the results from the server
|
||||
if (item1.get('isNew') || !updated1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (item2.get('isNew') || !updated2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (publishedAtResult === 0) {
|
||||
if (updatedAtResult === 0) {
|
||||
// This should be DESC
|
||||
return idResult * -1;
|
||||
}
|
||||
// This should be DESC
|
||||
return updatedAtResult * -1;
|
||||
}
|
||||
// This should be DESC
|
||||
return publishedAtResult * -1;
|
||||
}
|
||||
|
||||
return statusResult;
|
||||
}
|
||||
|
||||
function publishedAtCompare(item1, item2) {
|
||||
let published1 = item1.get('publishedAt');
|
||||
let published2 = item2.get('publishedAt');
|
||||
|
||||
if (!published1 && !published2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!published1 && published2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!published2 && published1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return compare(published1.valueOf(), published2.valueOf());
|
||||
}
|
||||
|
||||
export default Controller.extend({
|
||||
|
||||
showDeletePostModal: false,
|
||||
|
||||
// See PostsRoute's shortcuts
|
||||
postListFocused: equal('keyboardFocus', 'postList'),
|
||||
postContentFocused: equal('keyboardFocus', 'postContent'),
|
||||
|
||||
sortedPosts: computed('model.@each.status', 'model.@each.publishedAt', 'model.@each.isNew', 'model.@each.updatedAt', function () {
|
||||
let postsArray = this.get('model').toArray();
|
||||
|
||||
return postsArray.sort(comparator);
|
||||
}),
|
||||
|
||||
actions: {
|
||||
toggleDeletePostModal() {
|
||||
this.toggleProperty('showDeletePostModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
const {
|
||||
Controller,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
newPassword: '',
|
||||
ne2Password: '',
|
||||
token: '',
|
||||
submitting: false,
|
||||
flowErrors: '',
|
||||
|
||||
validationType: 'reset',
|
||||
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
ajax: service(),
|
||||
|
||||
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() {
|
||||
this.setProperties({
|
||||
newPassword: '',
|
||||
ne2Password: '',
|
||||
token: ''
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
submit() {
|
||||
let credentials = this.getProperties('newPassword', 'ne2Password', 'token');
|
||||
|
||||
this.set('flowErrors', '');
|
||||
this.get('hasValidated').addObjects(['newPassword', 'ne2Password']);
|
||||
this.validate().then(() => {
|
||||
let authUrl = this.get('ghostPaths.url').api('authentication', 'passwordreset');
|
||||
this.toggleProperty('submitting');
|
||||
this.get('ajax').put(authUrl, {
|
||||
data: {
|
||||
passwordreset: [credentials]
|
||||
}
|
||||
}).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((error) => {
|
||||
this.get('notifications').showAPIError(error, {key: 'password.reset'});
|
||||
this.toggleProperty('submitting');
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (this.get('errors.newPassword')) {
|
||||
this.set('flowErrors', this.get('errors.newPassword')[0].message);
|
||||
}
|
||||
|
||||
if (this.get('errors.ne2Password')) {
|
||||
this.set('flowErrors', this.get('errors.ne2Password')[0].message);
|
||||
}
|
||||
|
||||
if (this.get('errors.length') === 0) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
computed,
|
||||
inject: {controller}
|
||||
} = Ember;
|
||||
|
||||
const {alias} = computed;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
appsController: controller('settings.apps'),
|
||||
|
||||
slack: alias('appsController.model.slack.firstObject')
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import { invoke } from 'ember-invoke-action';
|
||||
|
||||
const {
|
||||
Controller,
|
||||
computed: {empty},
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
ghostPaths: service(),
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
|
||||
// will be set by route
|
||||
settings: null,
|
||||
|
||||
isSaving: false,
|
||||
savePromise: null,
|
||||
isSendingTest: false,
|
||||
|
||||
testNotificationDisabled: empty('model.url'),
|
||||
|
||||
actions: {
|
||||
sendTestNotification() {
|
||||
let notifications = this.get('notifications');
|
||||
let slackApi = this.get('ghostPaths.url').api('slack', 'test');
|
||||
|
||||
if (this.get('isSendingTest')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('isSendingTest', true);
|
||||
|
||||
invoke(this, 'save').then(() => {
|
||||
this.get('ajax').post(slackApi).then(() => {
|
||||
notifications.showAlert('Check your slack channel test message.', {type: 'info', key: 'slack-test.send.success'});
|
||||
}).catch((error) => {
|
||||
notifications.showAPIError(error, {key: 'slack-test:send'});
|
||||
});
|
||||
}).catch(() => {
|
||||
// noop - error already handled in .save
|
||||
}).finally(() => {
|
||||
this.set('isSendingTest', false);
|
||||
});
|
||||
},
|
||||
|
||||
updateURL(value) {
|
||||
this.set('model.url', value);
|
||||
this.get('model.errors').clear();
|
||||
},
|
||||
|
||||
save() {
|
||||
let slack = this.get('model');
|
||||
let settings = this.get('settings');
|
||||
|
||||
if (this.get('isSaving')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return slack.validate().then(() => {
|
||||
settings.get('slack').clear().pushObject(slack);
|
||||
|
||||
this.set('isSaving', true);
|
||||
|
||||
return settings.save().catch((err) => {
|
||||
this.get('notifications').showErrors(err);
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
this.set('isSaving', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue