mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
ESLint: Consistent ember property/method ordering
no issue - https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/order-in-components.md - https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/order-in-controllers.md - https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/order-in-routes.md
This commit is contained in:
parent
f2da8a20b8
commit
48e3bf003d
@ -2,14 +2,10 @@ import Component from '@ember/component';
|
||||
import {schedule} from '@ember/runloop';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active'],
|
||||
active: false,
|
||||
classNameBindings: ['active'],
|
||||
linkClasses: null,
|
||||
|
||||
click() {
|
||||
this.$('a').blur();
|
||||
},
|
||||
tagName: 'li',
|
||||
|
||||
actions: {
|
||||
setActive(value) {
|
||||
@ -17,5 +13,9 @@ export default Component.extend({
|
||||
this.set('active', value);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
this.$('a').blur();
|
||||
}
|
||||
});
|
||||
|
@ -3,12 +3,12 @@ import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'article',
|
||||
classNames: ['gh-alert'],
|
||||
classNameBindings: ['typeClass'],
|
||||
|
||||
notifications: service(),
|
||||
|
||||
classNameBindings: ['typeClass'],
|
||||
classNames: ['gh-alert'],
|
||||
tagName: 'article',
|
||||
|
||||
typeClass: computed('message.type', function () {
|
||||
let type = this.get('message.type');
|
||||
let classes = '';
|
||||
|
@ -3,10 +3,10 @@ import {alias} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'aside',
|
||||
classNames: 'gh-alerts',
|
||||
|
||||
notifications: service(),
|
||||
|
||||
classNames: 'gh-alerts',
|
||||
tagName: 'aside',
|
||||
|
||||
messages: alias('notifications.alerts')
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import Component from '@ember/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
config: service(),
|
||||
|
||||
config: service()
|
||||
tagName: ''
|
||||
});
|
||||
|
@ -9,9 +9,10 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
const CmEditorComponent = Component.extend(InvokeActionMixin, {
|
||||
lazyLoader: service(),
|
||||
|
||||
classNameBindings: ['isFocused:focus'],
|
||||
|
||||
_value: boundOneWay('value'), // make sure a value exists
|
||||
isFocused: false,
|
||||
isInitializingCodemirror: true,
|
||||
|
||||
@ -22,8 +23,7 @@ const CmEditorComponent = Component.extend(InvokeActionMixin, {
|
||||
theme: 'xq-light',
|
||||
|
||||
_editor: null, // reference to CodeMirror editor
|
||||
|
||||
lazyLoader: service(),
|
||||
_value: boundOneWay('value'), // make sure a value exists
|
||||
|
||||
didReceiveAttrs() {
|
||||
if (this.get('value') === null || undefined) {
|
||||
@ -36,6 +36,19 @@ const CmEditorComponent = Component.extend(InvokeActionMixin, {
|
||||
this.get('initCodeMirror').perform();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
// Ensure the editor exists before trying to destroy it. This fixes
|
||||
// an error that occurs if codemirror hasn't finished loading before
|
||||
// the component is destroyed.
|
||||
if (this._editor) {
|
||||
let editor = this._editor.getWrapperElement();
|
||||
editor.parentNode.removeChild(editor);
|
||||
this._editor = null;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateFromTextarea(value) {
|
||||
this.invokeAction('update', value);
|
||||
@ -108,19 +121,6 @@ const CmEditorComponent = Component.extend(InvokeActionMixin, {
|
||||
|
||||
_blur(/* codeMirror, event */) {
|
||||
this.set('isFocused', false);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
// Ensure the editor exists before trying to destroy it. This fixes
|
||||
// an error that occurs if codemirror hasn't finished loading before
|
||||
// the component is destroyed.
|
||||
if (this._editor) {
|
||||
let editor = this._editor.getWrapperElement();
|
||||
editor.parentNode.removeChild(editor);
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -10,6 +10,10 @@ export default Component.extend({
|
||||
tagName: '',
|
||||
count: '',
|
||||
|
||||
didInsertElement() {
|
||||
this.get('_poll').perform();
|
||||
},
|
||||
|
||||
_poll: task(function* () {
|
||||
let url = this.get('ghostPaths.count');
|
||||
let pattern = /(-?\d+)(\d{3})/;
|
||||
@ -31,9 +35,5 @@ export default Component.extend({
|
||||
} catch (e) {
|
||||
// no-op - we don't want to create noise for a failed download count
|
||||
}
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this.get('_poll').perform();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -3,6 +3,8 @@ import DropdownMixin from 'ghost-admin/mixins/dropdown-mixin';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend(DropdownMixin, {
|
||||
dropdown: service(),
|
||||
|
||||
tagName: 'button',
|
||||
attributeBindings: ['href', 'role'],
|
||||
role: 'button',
|
||||
@ -10,8 +12,6 @@ export default Component.extend(DropdownMixin, {
|
||||
// matches with the dropdown this button toggles
|
||||
dropdownName: null,
|
||||
|
||||
dropdown: service(),
|
||||
|
||||
// Notify dropdown service this dropdown should be toggled
|
||||
click(event) {
|
||||
this._super(event);
|
||||
|
@ -5,6 +5,8 @@ import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend(DropdownMixin, {
|
||||
dropdown: service(),
|
||||
|
||||
classNames: 'dropdown',
|
||||
classNameBindings: ['fadeIn:fade-in-scale:fade-out', 'isOpen:open:closed'],
|
||||
|
||||
@ -22,7 +24,23 @@ export default Component.extend(DropdownMixin, {
|
||||
return this.get('isOpen') && !this.get('closing');
|
||||
}),
|
||||
|
||||
dropdown: service(),
|
||||
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);
|
||||
},
|
||||
|
||||
open() {
|
||||
this.set('isOpen', true);
|
||||
@ -74,23 +92,5 @@ export default Component.extend(DropdownMixin, {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
@ -6,14 +6,15 @@ import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
export default Component.extend({
|
||||
post: null,
|
||||
isNew: reads('post.isNew'),
|
||||
isScheduled: reads('post.isScheduled'),
|
||||
isSaving: false,
|
||||
|
||||
'data-test-editor-post-status': true,
|
||||
|
||||
_isSaving: false,
|
||||
|
||||
isNew: reads('post.isNew'),
|
||||
isScheduled: reads('post.isScheduled'),
|
||||
|
||||
isPublished: computed('post.{isPublished,pastScheduledTime}', function () {
|
||||
let isPublished = this.get('post.isPublished');
|
||||
let pastScheduledTime = this.get('post.pastScheduledTime');
|
||||
|
@ -43,12 +43,6 @@ export default Component.extend({
|
||||
};
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
window.addEventListener('resize', this._onResizeHandler);
|
||||
this._setHeaderClass();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
let navIsClosed = this.get('navIsClosed');
|
||||
|
||||
@ -59,6 +53,51 @@ export default Component.extend({
|
||||
this._navIsClosed = navIsClosed;
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
window.addEventListener('resize', this._onResizeHandler);
|
||||
this._setHeaderClass();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
window.removeEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleFullScreen(isFullScreen) {
|
||||
this.set('isFullScreen', isFullScreen);
|
||||
this.get('ui').set('isFullScreen', isFullScreen);
|
||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||
},
|
||||
|
||||
togglePreview(isPreview) {
|
||||
this.set('isPreview', isPreview);
|
||||
},
|
||||
|
||||
toggleSplitScreen(isSplitScreen) {
|
||||
this.set('isSplitScreen', isSplitScreen);
|
||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||
},
|
||||
|
||||
uploadImages(fileList, resetInput) {
|
||||
// convert FileList to an array so that resetting the input doesn't
|
||||
// clear the file references before upload actions can be triggered
|
||||
let files = Array.from(fileList);
|
||||
this.set('droppedFiles', files);
|
||||
resetInput();
|
||||
},
|
||||
|
||||
uploadComplete(uploads) {
|
||||
this.set('uploadedImageUrls', uploads.mapBy('url'));
|
||||
this.set('droppedFiles', null);
|
||||
},
|
||||
|
||||
uploadCancelled() {
|
||||
this.set('droppedFiles', null);
|
||||
}
|
||||
},
|
||||
|
||||
_setHeaderClass() {
|
||||
let $editorTitle = this.$('.gh-editor-title');
|
||||
let smallHeaderClass = 'gh-editor-header-small';
|
||||
@ -134,44 +173,5 @@ export default Component.extend({
|
||||
if (event.dataTransfer.files) {
|
||||
this.set('droppedFiles', event.dataTransfer.files);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
window.removeEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleFullScreen(isFullScreen) {
|
||||
this.set('isFullScreen', isFullScreen);
|
||||
this.get('ui').set('isFullScreen', isFullScreen);
|
||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||
},
|
||||
|
||||
togglePreview(isPreview) {
|
||||
this.set('isPreview', isPreview);
|
||||
},
|
||||
|
||||
toggleSplitScreen(isSplitScreen) {
|
||||
this.set('isSplitScreen', isSplitScreen);
|
||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||
},
|
||||
|
||||
uploadImages(fileList, resetInput) {
|
||||
// convert FileList to an array so that resetting the input doesn't
|
||||
// clear the file references before upload actions can be triggered
|
||||
let files = Array.from(fileList);
|
||||
this.set('droppedFiles', files);
|
||||
resetInput();
|
||||
},
|
||||
|
||||
uploadComplete(uploads) {
|
||||
this.set('uploadedImageUrls', uploads.mapBy('url'));
|
||||
this.set('droppedFiles', null);
|
||||
},
|
||||
|
||||
uploadCancelled() {
|
||||
this.set('droppedFiles', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -3,19 +3,13 @@ import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const FeatureFlagComponent = Component.extend({
|
||||
feature: service(),
|
||||
|
||||
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');
|
||||
@ -31,7 +25,13 @@ const FeatureFlagComponent = Component.extend({
|
||||
|
||||
name: computed('flag', function () {
|
||||
return `labs[${this.get('flag')}]`;
|
||||
})
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set('_flagValue', this.get(`feature.${this.get('flag')}`));
|
||||
}
|
||||
});
|
||||
|
||||
FeatureFlagComponent.reopenClass({
|
||||
|
@ -7,18 +7,12 @@ export default Component.extend({
|
||||
uploadButtonText: 'Text',
|
||||
uploadButtonDisabled: true,
|
||||
|
||||
shouldResetForm: true,
|
||||
|
||||
// closure actions
|
||||
onUpload() {},
|
||||
onAdd() {},
|
||||
|
||||
shouldResetForm: true,
|
||||
|
||||
change(event) {
|
||||
this.set('uploadButtonDisabled', false);
|
||||
this.onAdd();
|
||||
this._file = event.target.files[0];
|
||||
},
|
||||
|
||||
actions: {
|
||||
upload() {
|
||||
if (!this.get('uploadButtonDisabled') && this._file) {
|
||||
@ -33,5 +27,11 @@ export default Component.extend({
|
||||
this.$().closest('form')[0].reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
change(event) {
|
||||
this.set('uploadButtonDisabled', false);
|
||||
this.onAdd();
|
||||
this._file = event.target.files[0];
|
||||
}
|
||||
});
|
||||
|
@ -19,6 +19,10 @@ const DEFAULTS = {
|
||||
};
|
||||
|
||||
export default Component.extend({
|
||||
ajax: service(),
|
||||
eventBus: service(),
|
||||
notifications: service(),
|
||||
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
classNameBindings: ['dragClass'],
|
||||
@ -37,10 +41,6 @@ export default Component.extend({
|
||||
failureMessage: null,
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: service(),
|
||||
eventBus: service(),
|
||||
notifications: service(),
|
||||
|
||||
formData: computed('file', function () {
|
||||
let paramName = this.get('paramName');
|
||||
let file = this.get('file');
|
||||
@ -102,6 +102,44 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList, resetInput) {
|
||||
let [file] = Array.from(fileList);
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
upload() {
|
||||
if (this.get('file')) {
|
||||
this.generateRequest();
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
this.set('failureMessage', null);
|
||||
}
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
@ -215,43 +253,5 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList, resetInput) {
|
||||
let [file] = Array.from(fileList);
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
upload() {
|
||||
if (this.get('file')) {
|
||||
this.generateRequest();
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
this.set('failureMessage', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -8,12 +8,11 @@ import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const FullScreenModalComponent = Component.extend({
|
||||
dropdown: service(),
|
||||
|
||||
model: null,
|
||||
modifier: null,
|
||||
|
||||
dropdown: service(),
|
||||
|
||||
modalPath: computed('modal', function () {
|
||||
return `modal-${this.get('modal') || 'unknown'}`;
|
||||
}),
|
||||
|
@ -93,6 +93,50 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList, resetInput) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addUnsplashPhoto(photo) {
|
||||
this.set('url', photo.urls.regular);
|
||||
this.send('saveUrl');
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
}
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
@ -228,49 +272,5 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList, resetInput) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addUnsplashPhoto(photo) {
|
||||
this.set('url', photo.urls.regular);
|
||||
this.send('saveUrl');
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -9,12 +9,12 @@ export default Component.extend({
|
||||
// prevents unnecessary flash of spinner
|
||||
slowLoadTimeout: 200,
|
||||
|
||||
didInsertElement() {
|
||||
this.get('startSpinnerTimeout').perform();
|
||||
},
|
||||
|
||||
startSpinnerTimeout: task(function* () {
|
||||
yield timeout(this.get('slowLoadTimeout'));
|
||||
this.set('showSpinner', true);
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this.get('startSpinnerTimeout').perform();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -49,13 +49,6 @@ export default Component.extend(ShortcutsMixin, {
|
||||
|
||||
shortcuts: null,
|
||||
|
||||
// Closure actions
|
||||
onChange() {},
|
||||
onFullScreenToggle() {},
|
||||
onImageFilesSelected() {},
|
||||
onPreviewToggle() {},
|
||||
onSplitScreenToggle() {},
|
||||
|
||||
// Internal attributes
|
||||
markdown: null,
|
||||
|
||||
@ -71,6 +64,13 @@ export default Component.extend(ShortcutsMixin, {
|
||||
_toolbar: null,
|
||||
_uploadedImageUrls: null,
|
||||
|
||||
// Closure actions
|
||||
onChange() {},
|
||||
onFullScreenToggle() {},
|
||||
onImageFilesSelected() {},
|
||||
onPreviewToggle() {},
|
||||
onSplitScreenToggle() {},
|
||||
|
||||
simpleMDEOptions: computed('options', function () {
|
||||
let options = this.get('options') || {};
|
||||
let defaultOptions = {
|
||||
@ -247,6 +247,182 @@ export default Component.extend(ShortcutsMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// put the markdown into a new mobiledoc card, trigger external update
|
||||
updateMarkdown(markdown) {
|
||||
let mobiledoc = copy(BLANK_DOC, true);
|
||||
mobiledoc.cards[0][1].markdown = markdown;
|
||||
this.onChange(mobiledoc);
|
||||
},
|
||||
|
||||
// store a reference to the simplemde editor so that we can handle
|
||||
// focusing and image uploads
|
||||
setEditor(editor) {
|
||||
this._editor = editor;
|
||||
|
||||
// disable CodeMirror's drag/drop handling as we want to handle that
|
||||
// in the parent gh-editor component
|
||||
this._editor.codemirror.setOption('dragDrop', false);
|
||||
|
||||
// default to spellchecker being off
|
||||
this._editor.codemirror.setOption('mode', 'gfm');
|
||||
|
||||
// add non-breaking space as a special char
|
||||
this._editor.codemirror.setOption('specialChars', /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff\xa0]/g);
|
||||
|
||||
// HACK: move the toolbar & status bar elements outside of the
|
||||
// editor container so that they can be aligned in fixed positions
|
||||
let container = this.$().closest('.gh-editor').find('.gh-editor-footer');
|
||||
this._toolbar = this.$('.editor-toolbar');
|
||||
this._statusbar = this.$('.editor-statusbar');
|
||||
this._toolbar.appendTo(container);
|
||||
this._statusbar.appendTo(container);
|
||||
|
||||
this._updateButtonState();
|
||||
},
|
||||
|
||||
// used by the title input when the TAB or ENTER keys are pressed
|
||||
focusEditor(position = 'bottom') {
|
||||
this._editor.codemirror.focus();
|
||||
|
||||
if (position === 'bottom') {
|
||||
this._editor.codemirror.execCommand('goDocEnd');
|
||||
} else if (position === 'top') {
|
||||
this._editor.codemirror.execCommand('goDocStart');
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// HACK FIXME (PLEASE):
|
||||
// - clicking toolbar buttons will cause the editor to lose focus
|
||||
// - this is painful because we often want to know if the editor has focus
|
||||
// so that we can insert images and so on in the correct place
|
||||
// - the blur event will always fire before the button action is triggered 😞
|
||||
// - to work around this we track focus state manually and set it to false
|
||||
// after an arbitrary period that's long enough to allow the button action
|
||||
// to trigger first
|
||||
// - this _may_ well have unknown issues due to browser differences,
|
||||
// variations in performance, moon cycles, sun spots, or cosmic rays
|
||||
// - here be 🐲
|
||||
// - (please let it work 🙏)
|
||||
updateFocusState(focused) {
|
||||
if (focused) {
|
||||
this._editorFocused = true;
|
||||
} else {
|
||||
run.later(this, function () {
|
||||
this._editorFocused = false;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
openImageFileDialog() {
|
||||
let captureSelection = this._editor.codemirror.hasFocus();
|
||||
this._openImageFileDialog({captureSelection});
|
||||
},
|
||||
|
||||
toggleUnsplash() {
|
||||
if (this.get('_showUnsplash')) {
|
||||
return this.toggleProperty('_showUnsplash');
|
||||
}
|
||||
|
||||
// capture current selection before it's lost by clicking toolbar btn
|
||||
if (this._editorFocused) {
|
||||
this._imageInsertSelection = {
|
||||
anchor: this._editor.codemirror.getCursor('anchor'),
|
||||
head: this._editor.codemirror.getCursor('head')
|
||||
};
|
||||
}
|
||||
|
||||
this.toggleProperty('_showUnsplash');
|
||||
},
|
||||
|
||||
insertUnsplashPhoto(photo) {
|
||||
let image = {
|
||||
alt: photo.description || '',
|
||||
url: photo.urls.regular,
|
||||
credit: `<small>Photo by [${photo.user.name}](${photo.user.links.html}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit) / [Unsplash](https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit)</small>`
|
||||
};
|
||||
|
||||
this._insertImages([image]);
|
||||
},
|
||||
|
||||
togglePreview() {
|
||||
this._togglePreview();
|
||||
},
|
||||
|
||||
toggleFullScreen() {
|
||||
let isFullScreen = !this.get('_isFullScreen');
|
||||
|
||||
this.set('_isFullScreen', isFullScreen);
|
||||
this._updateButtonState();
|
||||
this.onFullScreenToggle(isFullScreen);
|
||||
|
||||
// leave split screen when exiting full screen mode
|
||||
if (!isFullScreen && this.get('_isSplitScreen')) {
|
||||
this.send('toggleSplitScreen');
|
||||
}
|
||||
},
|
||||
|
||||
toggleSplitScreen() {
|
||||
let isSplitScreen = !this.get('_isSplitScreen');
|
||||
let previewButton = this._editor.toolbarElements.preview;
|
||||
|
||||
this.set('_isSplitScreen', isSplitScreen);
|
||||
this._updateButtonState();
|
||||
|
||||
// set up the preview rendering and scroll sync
|
||||
// afterRender is needed so that necessary components have been
|
||||
// added/removed and editor pane length has settled
|
||||
if (isSplitScreen) {
|
||||
// disable the normal SimpleMDE preview if it's active
|
||||
if (this._editor.isPreviewActive()) {
|
||||
let preview = this._editor.toolbar.find(button => button.name === 'preview');
|
||||
|
||||
preview.action(this._editor);
|
||||
}
|
||||
|
||||
if (previewButton) {
|
||||
previewButton.classList.add('disabled');
|
||||
}
|
||||
|
||||
run.scheduleOnce('afterRender', this, this._connectSplitPreview);
|
||||
} else {
|
||||
if (previewButton) {
|
||||
previewButton.classList.remove('disabled');
|
||||
}
|
||||
|
||||
run.scheduleOnce('afterRender', this, this._disconnectSplitPreview);
|
||||
}
|
||||
|
||||
this.onSplitScreenToggle(isSplitScreen);
|
||||
|
||||
// go fullscreen when entering split screen mode
|
||||
this.send('toggleFullScreen');
|
||||
},
|
||||
|
||||
toggleSpellcheck() {
|
||||
this._toggleSpellcheck();
|
||||
},
|
||||
|
||||
toggleHemingway() {
|
||||
this._toggleHemingway();
|
||||
},
|
||||
|
||||
toggleMarkdownHelp() {
|
||||
this.toggleProperty('showMarkdownHelp');
|
||||
},
|
||||
|
||||
// put the toolbar/statusbar elements back so that SimpleMDE doesn't throw
|
||||
// errors when it tries to remove them
|
||||
destroyEditor() {
|
||||
let container = this.$('.gh-markdown-editor-pane');
|
||||
this._toolbar.appendTo(container);
|
||||
this._statusbar.appendTo(container);
|
||||
this._editor = null;
|
||||
}
|
||||
},
|
||||
|
||||
_preventBodyScroll() {
|
||||
this._preventBodyScrollId = window.requestAnimationFrame(() => {
|
||||
let body = document.querySelector('body');
|
||||
@ -479,181 +655,5 @@ export default Component.extend(ShortcutsMixin, {
|
||||
htmlSafe(notificationText),
|
||||
{key: 'editor.hemingwaymode'}
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
// put the markdown into a new mobiledoc card, trigger external update
|
||||
updateMarkdown(markdown) {
|
||||
let mobiledoc = copy(BLANK_DOC, true);
|
||||
mobiledoc.cards[0][1].markdown = markdown;
|
||||
this.onChange(mobiledoc);
|
||||
},
|
||||
|
||||
// store a reference to the simplemde editor so that we can handle
|
||||
// focusing and image uploads
|
||||
setEditor(editor) {
|
||||
this._editor = editor;
|
||||
|
||||
// disable CodeMirror's drag/drop handling as we want to handle that
|
||||
// in the parent gh-editor component
|
||||
this._editor.codemirror.setOption('dragDrop', false);
|
||||
|
||||
// default to spellchecker being off
|
||||
this._editor.codemirror.setOption('mode', 'gfm');
|
||||
|
||||
// add non-breaking space as a special char
|
||||
this._editor.codemirror.setOption('specialChars', /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff\xa0]/g);
|
||||
|
||||
// HACK: move the toolbar & status bar elements outside of the
|
||||
// editor container so that they can be aligned in fixed positions
|
||||
let container = this.$().closest('.gh-editor').find('.gh-editor-footer');
|
||||
this._toolbar = this.$('.editor-toolbar');
|
||||
this._statusbar = this.$('.editor-statusbar');
|
||||
this._toolbar.appendTo(container);
|
||||
this._statusbar.appendTo(container);
|
||||
|
||||
this._updateButtonState();
|
||||
},
|
||||
|
||||
// used by the title input when the TAB or ENTER keys are pressed
|
||||
focusEditor(position = 'bottom') {
|
||||
this._editor.codemirror.focus();
|
||||
|
||||
if (position === 'bottom') {
|
||||
this._editor.codemirror.execCommand('goDocEnd');
|
||||
} else if (position === 'top') {
|
||||
this._editor.codemirror.execCommand('goDocStart');
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// HACK FIXME (PLEASE):
|
||||
// - clicking toolbar buttons will cause the editor to lose focus
|
||||
// - this is painful because we often want to know if the editor has focus
|
||||
// so that we can insert images and so on in the correct place
|
||||
// - the blur event will always fire before the button action is triggered 😞
|
||||
// - to work around this we track focus state manually and set it to false
|
||||
// after an arbitrary period that's long enough to allow the button action
|
||||
// to trigger first
|
||||
// - this _may_ well have unknown issues due to browser differences,
|
||||
// variations in performance, moon cycles, sun spots, or cosmic rays
|
||||
// - here be 🐲
|
||||
// - (please let it work 🙏)
|
||||
updateFocusState(focused) {
|
||||
if (focused) {
|
||||
this._editorFocused = true;
|
||||
} else {
|
||||
run.later(this, function () {
|
||||
this._editorFocused = false;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
openImageFileDialog() {
|
||||
let captureSelection = this._editor.codemirror.hasFocus();
|
||||
this._openImageFileDialog({captureSelection});
|
||||
},
|
||||
|
||||
toggleUnsplash() {
|
||||
if (this.get('_showUnsplash')) {
|
||||
return this.toggleProperty('_showUnsplash');
|
||||
}
|
||||
|
||||
// capture current selection before it's lost by clicking toolbar btn
|
||||
if (this._editorFocused) {
|
||||
this._imageInsertSelection = {
|
||||
anchor: this._editor.codemirror.getCursor('anchor'),
|
||||
head: this._editor.codemirror.getCursor('head')
|
||||
};
|
||||
}
|
||||
|
||||
this.toggleProperty('_showUnsplash');
|
||||
},
|
||||
|
||||
insertUnsplashPhoto(photo) {
|
||||
let image = {
|
||||
alt: photo.description || '',
|
||||
url: photo.urls.regular,
|
||||
credit: `<small>Photo by [${photo.user.name}](${photo.user.links.html}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit) / [Unsplash](https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit)</small>`
|
||||
};
|
||||
|
||||
this._insertImages([image]);
|
||||
},
|
||||
|
||||
togglePreview() {
|
||||
this._togglePreview();
|
||||
},
|
||||
|
||||
toggleFullScreen() {
|
||||
let isFullScreen = !this.get('_isFullScreen');
|
||||
|
||||
this.set('_isFullScreen', isFullScreen);
|
||||
this._updateButtonState();
|
||||
this.onFullScreenToggle(isFullScreen);
|
||||
|
||||
// leave split screen when exiting full screen mode
|
||||
if (!isFullScreen && this.get('_isSplitScreen')) {
|
||||
this.send('toggleSplitScreen');
|
||||
}
|
||||
},
|
||||
|
||||
toggleSplitScreen() {
|
||||
let isSplitScreen = !this.get('_isSplitScreen');
|
||||
let previewButton = this._editor.toolbarElements.preview;
|
||||
|
||||
this.set('_isSplitScreen', isSplitScreen);
|
||||
this._updateButtonState();
|
||||
|
||||
// set up the preview rendering and scroll sync
|
||||
// afterRender is needed so that necessary components have been
|
||||
// added/removed and editor pane length has settled
|
||||
if (isSplitScreen) {
|
||||
// disable the normal SimpleMDE preview if it's active
|
||||
if (this._editor.isPreviewActive()) {
|
||||
let preview = this._editor.toolbar.find(button => button.name === 'preview');
|
||||
|
||||
preview.action(this._editor);
|
||||
}
|
||||
|
||||
if (previewButton) {
|
||||
previewButton.classList.add('disabled');
|
||||
}
|
||||
|
||||
run.scheduleOnce('afterRender', this, this._connectSplitPreview);
|
||||
} else {
|
||||
if (previewButton) {
|
||||
previewButton.classList.remove('disabled');
|
||||
}
|
||||
|
||||
run.scheduleOnce('afterRender', this, this._disconnectSplitPreview);
|
||||
}
|
||||
|
||||
this.onSplitScreenToggle(isSplitScreen);
|
||||
|
||||
// go fullscreen when entering split screen mode
|
||||
this.send('toggleFullScreen');
|
||||
},
|
||||
|
||||
toggleSpellcheck() {
|
||||
this._toggleSpellcheck();
|
||||
},
|
||||
|
||||
toggleHemingway() {
|
||||
this._toggleHemingway();
|
||||
},
|
||||
|
||||
toggleMarkdownHelp() {
|
||||
this.toggleProperty('showMarkdownHelp');
|
||||
},
|
||||
|
||||
// put the toolbar/statusbar elements back so that SimpleMDE doesn't throw
|
||||
// errors when it tries to remove them
|
||||
destroyEditor() {
|
||||
let container = this.$('.gh-markdown-editor-pane');
|
||||
this._toolbar.appendTo(container);
|
||||
this._statusbar.appendTo(container);
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -14,16 +14,17 @@ import {inject as service} from '@ember/service';
|
||||
closes the mobile menu
|
||||
*/
|
||||
export default Component.extend({
|
||||
classNames: ['gh-menu-toggle'],
|
||||
|
||||
mediaQueries: service(),
|
||||
isMobile: reads('mediaQueries.isMobile'),
|
||||
|
||||
classNames: ['gh-menu-toggle'],
|
||||
maximise: false,
|
||||
|
||||
// closure actions
|
||||
desktopAction() {},
|
||||
mobileAction() {},
|
||||
|
||||
isMobile: reads('mediaQueries.isMobile'),
|
||||
|
||||
iconClass: computed('maximise', 'isMobile', function () {
|
||||
if (this.get('maximise') && !this.get('isMobile')) {
|
||||
return 'icon-maximise';
|
||||
|
@ -19,14 +19,6 @@ export default Component.extend({
|
||||
open: false,
|
||||
iconStyle: '',
|
||||
|
||||
// the menu has a rendering issue (#8307) when the the world is reloaded
|
||||
// during an import which we have worked around by not binding the icon
|
||||
// style directly. However we still need to keep track of changing icons
|
||||
// so that we can refresh when a new icon is uploaded
|
||||
didReceiveAttrs() {
|
||||
this._setIconStyle();
|
||||
},
|
||||
|
||||
showMenuExtension: computed('config.clientExtensions.menu', 'session.user.isOwner', function () {
|
||||
return this.get('config.clientExtensions.menu') && this.get('session.user.isOwner');
|
||||
}),
|
||||
@ -39,6 +31,14 @@ export default Component.extend({
|
||||
return this.get('config.clientExtensions.script') && this.get('session.user.isOwner');
|
||||
}),
|
||||
|
||||
// the menu has a rendering issue (#8307) when the the world is reloaded
|
||||
// during an import which we have worked around by not binding the icon
|
||||
// style directly. However we still need to keep track of changing icons
|
||||
// so that we can refresh when a new icon is uploaded
|
||||
didReceiveAttrs() {
|
||||
this._setIconStyle();
|
||||
},
|
||||
|
||||
// equivalent to "left: auto; right: -20px"
|
||||
userDropdownPosition(trigger, dropdown) {
|
||||
let {horizontalPosition, verticalPosition, style} = calculatePosition(...arguments);
|
||||
|
@ -18,16 +18,6 @@ export default Component.extend(ValidationState, {
|
||||
}
|
||||
}),
|
||||
|
||||
keyPress(event) {
|
||||
// enter key
|
||||
if (event.keyCode === 13 && this.get('navItem.isNew')) {
|
||||
event.preventDefault();
|
||||
run.scheduleOnce('actions', this, function () {
|
||||
this.send('addItem');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addItem() {
|
||||
let action = this.get('addItem');
|
||||
@ -64,5 +54,15 @@ export default Component.extend(ValidationState, {
|
||||
clearUrlErrors() {
|
||||
this.get('navItem.errors').remove('url');
|
||||
}
|
||||
},
|
||||
|
||||
keyPress(event) {
|
||||
// enter key
|
||||
if (event.keyCode === 13 && this.get('navItem.isNew')) {
|
||||
event.preventDefault();
|
||||
run.scheduleOnce('actions', this, function () {
|
||||
this.send('addItem');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -3,14 +3,14 @@ import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
notifications: service(),
|
||||
|
||||
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 = '';
|
||||
|
@ -3,10 +3,10 @@ import {alias} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
notifications: service(),
|
||||
|
||||
tagName: 'aside',
|
||||
classNames: 'gh-notifications',
|
||||
|
||||
notifications: service(),
|
||||
|
||||
messages: alias('notifications.notifications')
|
||||
});
|
||||
|
@ -13,9 +13,6 @@ import {task, timeout} from 'ember-concurrency';
|
||||
const PSM_ANIMATION_LENGTH = 400;
|
||||
|
||||
export default Component.extend(SettingsMenuMixin, {
|
||||
selectedAuthor: null,
|
||||
authors: null,
|
||||
|
||||
store: service(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
@ -25,7 +22,12 @@ export default Component.extend(SettingsMenuMixin, {
|
||||
settings: service(),
|
||||
ui: service(),
|
||||
|
||||
authors: null,
|
||||
post: null,
|
||||
selectedAuthor: null,
|
||||
|
||||
_showSettingsMenu: false,
|
||||
_showThrobbers: false,
|
||||
|
||||
customExcerptScratch: alias('post.customExcerptScratch'),
|
||||
codeinjectionFootScratch: alias('post.codeinjectionFootScratch'),
|
||||
@ -46,8 +48,49 @@ export default Component.extend(SettingsMenuMixin, {
|
||||
twitterImage: or('post.twitterImage', 'post.featureImage'),
|
||||
twitterTitle: or('twitterTitleScratch', 'seoTitle'),
|
||||
|
||||
_showSettingsMenu: false,
|
||||
_showThrobbers: false,
|
||||
twitterImageStyle: computed('twitterImage', function () {
|
||||
let image = this.get('twitterImage');
|
||||
return htmlSafe(`background-image: url(${image})`);
|
||||
}),
|
||||
|
||||
facebookImageStyle: computed('facebookImage', function () {
|
||||
let image = this.get('facebookImage');
|
||||
return htmlSafe(`background-image: url(${image})`);
|
||||
}),
|
||||
|
||||
seoDescription: computed('post.scratch', 'metaDescriptionScratch', function () {
|
||||
let metaDescription = this.get('metaDescriptionScratch') || '';
|
||||
let mobiledoc = this.get('post.scratch');
|
||||
let markdown = mobiledoc.cards && mobiledoc.cards[0][1].markdown;
|
||||
let placeholder;
|
||||
|
||||
if (metaDescription) {
|
||||
placeholder = metaDescription;
|
||||
} else {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = formatMarkdown(markdown, false);
|
||||
|
||||
// Strip HTML
|
||||
placeholder = div.textContent;
|
||||
// Replace new lines and trim
|
||||
placeholder = placeholder.replace(/\n+/g, ' ').trim();
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}),
|
||||
|
||||
seoURL: computed('post.slug', 'config.blogUrl', function () {
|
||||
let blogUrl = this.get('config.blogUrl');
|
||||
let seoSlug = this.get('post.slug') ? this.get('post.slug') : '';
|
||||
let seoURL = `${blogUrl}/${seoSlug}`;
|
||||
|
||||
// only append a slash to the URL if the slug exists
|
||||
if (seoSlug) {
|
||||
seoURL += '/';
|
||||
}
|
||||
|
||||
return seoURL;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -93,62 +136,6 @@ export default Component.extend(SettingsMenuMixin, {
|
||||
this._showSettingsMenu = this.get('showSettingsMenu');
|
||||
},
|
||||
|
||||
twitterImageStyle: computed('twitterImage', function () {
|
||||
let image = this.get('twitterImage');
|
||||
return htmlSafe(`background-image: url(${image})`);
|
||||
}),
|
||||
|
||||
facebookImageStyle: computed('facebookImage', function () {
|
||||
let image = this.get('facebookImage');
|
||||
return htmlSafe(`background-image: url(${image})`);
|
||||
}),
|
||||
|
||||
showThrobbers: task(function* () {
|
||||
yield timeout(PSM_ANIMATION_LENGTH);
|
||||
this.set('_showThrobbers', true);
|
||||
}).restartable(),
|
||||
|
||||
seoDescription: computed('post.scratch', 'metaDescriptionScratch', function () {
|
||||
let metaDescription = this.get('metaDescriptionScratch') || '';
|
||||
let mobiledoc = this.get('post.scratch');
|
||||
let markdown = mobiledoc.cards && mobiledoc.cards[0][1].markdown;
|
||||
let placeholder;
|
||||
|
||||
if (metaDescription) {
|
||||
placeholder = metaDescription;
|
||||
} else {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = formatMarkdown(markdown, false);
|
||||
|
||||
// Strip HTML
|
||||
placeholder = div.textContent;
|
||||
// Replace new lines and trim
|
||||
placeholder = placeholder.replace(/\n+/g, ' ').trim();
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}),
|
||||
|
||||
seoURL: computed('post.slug', 'config.blogUrl', function () {
|
||||
let blogUrl = this.get('config.blogUrl');
|
||||
let seoSlug = this.get('post.slug') ? this.get('post.slug') : '';
|
||||
let seoURL = `${blogUrl}/${seoSlug}`;
|
||||
|
||||
// only append a slash to the URL if the slug exists
|
||||
if (seoSlug) {
|
||||
seoURL += '/';
|
||||
}
|
||||
|
||||
return seoURL;
|
||||
}),
|
||||
|
||||
showError(error) {
|
||||
// TODO: remove null check once ValidationEngine has been removed
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
showSubview(subview) {
|
||||
this._super(...arguments);
|
||||
@ -526,5 +513,17 @@ export default Component.extend(SettingsMenuMixin, {
|
||||
this.get('deletePost')();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showThrobbers: task(function* () {
|
||||
yield timeout(PSM_ANIMATION_LENGTH);
|
||||
this.set('_showThrobbers', true);
|
||||
}).restartable(),
|
||||
|
||||
showError(error) {
|
||||
// TODO: remove null check once ValidationEngine has been removed
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -19,16 +19,16 @@ export default Component.extend({
|
||||
post: null,
|
||||
active: false,
|
||||
|
||||
// closure actions
|
||||
onClick() {},
|
||||
onDoubleClick() {},
|
||||
|
||||
isFeatured: alias('post.featured'),
|
||||
isPage: alias('post.page'),
|
||||
isDraft: equal('post.status', 'draft'),
|
||||
isPublished: equal('post.status', 'published'),
|
||||
isScheduled: equal('post.status', 'scheduled'),
|
||||
|
||||
// closure actions
|
||||
onClick() {},
|
||||
onDoubleClick() {},
|
||||
|
||||
authorName: computed('post.author.{name,email}', function () {
|
||||
return this.get('post.author.name') || this.get('post.author.email');
|
||||
}),
|
||||
|
@ -22,6 +22,9 @@ const ANIMATION_TIMEOUT = 1000;
|
||||
* @property {String} imageBackground String containing the background-image css property with the gravatar url
|
||||
*/
|
||||
export default Component.extend({
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
|
||||
email: '',
|
||||
size: 180,
|
||||
debounce: 300,
|
||||
@ -29,17 +32,14 @@ export default Component.extend({
|
||||
imageFile: null,
|
||||
hasUploadedImage: false,
|
||||
|
||||
_defaultImageUrl: '',
|
||||
|
||||
// closure actions
|
||||
setImage() {},
|
||||
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
|
||||
placeholderStyle: htmlSafe('background-image: url()'),
|
||||
avatarStyle: htmlSafe('display: none'),
|
||||
|
||||
_defaultImageUrl: '',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
@ -55,6 +55,38 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
imageSelected(fileList, resetInput) {
|
||||
// eslint-disable-next-line
|
||||
let imageFile = fileList[0];
|
||||
|
||||
if (imageFile) {
|
||||
let reader = new FileReader();
|
||||
|
||||
this.set('imageFile', imageFile);
|
||||
this.setImage(imageFile);
|
||||
|
||||
reader.addEventListener('load', () => {
|
||||
let dataURL = reader.result;
|
||||
this.set('previewDataURL', dataURL);
|
||||
}, false);
|
||||
|
||||
reader.readAsDataURL(imageFile);
|
||||
}
|
||||
|
||||
resetInput();
|
||||
},
|
||||
|
||||
openFileDialog(event) {
|
||||
// simulate click to open file dialog
|
||||
// using jQuery because IE11 doesn't support MouseEvent
|
||||
$(event.target)
|
||||
.closest('figure')
|
||||
.find('input[type="file"]')
|
||||
.click();
|
||||
}
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
@ -128,37 +160,5 @@ export default Component.extend({
|
||||
action(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
imageSelected(fileList, resetInput) {
|
||||
// eslint-disable-next-line
|
||||
let imageFile = fileList[0];
|
||||
|
||||
if (imageFile) {
|
||||
let reader = new FileReader();
|
||||
|
||||
this.set('imageFile', imageFile);
|
||||
this.setImage(imageFile);
|
||||
|
||||
reader.addEventListener('load', () => {
|
||||
let dataURL = reader.result;
|
||||
this.set('previewDataURL', dataURL);
|
||||
}, false);
|
||||
|
||||
reader.readAsDataURL(imageFile);
|
||||
}
|
||||
|
||||
resetInput();
|
||||
},
|
||||
|
||||
openFileDialog(event) {
|
||||
// simulate click to open file dialog
|
||||
// using jQuery because IE11 doesn't support MouseEvent
|
||||
$(event.target)
|
||||
.closest('figure')
|
||||
.find('input[type="file"]')
|
||||
.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -53,6 +53,12 @@ export default Component.extend({
|
||||
this.get('loadActiveTheme').perform();
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectTemplate(template) {
|
||||
this.onTemplateSelect(template.filename);
|
||||
}
|
||||
},
|
||||
|
||||
// tasks
|
||||
loadActiveTheme: task(function* () {
|
||||
let store = this.get('store');
|
||||
@ -65,11 +71,5 @@ export default Component.extend({
|
||||
let activeTheme = themes.filterBy('active', true).get('firstObject');
|
||||
|
||||
this.set('activeTheme', activeTheme);
|
||||
}),
|
||||
|
||||
actions: {
|
||||
selectTemplate(template) {
|
||||
this.onTemplateSelect(template.filename);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -18,12 +18,6 @@ export default Component.extend({
|
||||
this.send('setSaveType', 'publish');
|
||||
},
|
||||
|
||||
// API only accepts dates at least 2 mins in the future, default the
|
||||
// scheduled date 5 mins in the future to avoid immediate validation errors
|
||||
_getMinDate() {
|
||||
return moment.utc().add(5, 'minutes');
|
||||
},
|
||||
|
||||
actions: {
|
||||
setSaveType(type) {
|
||||
if (this.get('saveType') !== type) {
|
||||
@ -70,5 +64,11 @@ export default Component.extend({
|
||||
post.set('publishedAtBlogTime', time);
|
||||
return post.validate();
|
||||
}
|
||||
},
|
||||
|
||||
// API only accepts dates at least 2 mins in the future, default the
|
||||
// scheduled date 5 mins in the future to avoid immediate validation errors
|
||||
_getMinDate() {
|
||||
return moment.utc().add(5, 'minutes');
|
||||
}
|
||||
});
|
||||
|
@ -11,8 +11,10 @@ export default Component.extend({
|
||||
classNames: 'gh-publishmenu',
|
||||
post: null,
|
||||
saveTask: null,
|
||||
runningText: null,
|
||||
|
||||
_publishedAtBlogTZ: null,
|
||||
_previousStatus: null,
|
||||
|
||||
isClosing: null,
|
||||
|
||||
@ -60,8 +62,6 @@ export default Component.extend({
|
||||
return runningText || 'Publishing';
|
||||
}),
|
||||
|
||||
runningText: null,
|
||||
|
||||
buttonText: computed('postState', 'saveType', function () {
|
||||
let saveType = this.get('saveType');
|
||||
let postState = this.get('postState');
|
||||
@ -102,42 +102,6 @@ export default Component.extend({
|
||||
return buttonText;
|
||||
}),
|
||||
|
||||
save: task(function* () {
|
||||
// runningText needs to be declared before the other states change during the
|
||||
// save action.
|
||||
this.set('runningText', this.get('_runningText'));
|
||||
this.set('_previousStatus', this.get('post.status'));
|
||||
this.get('setSaveType')(this.get('saveType'));
|
||||
|
||||
try {
|
||||
// validate publishedAtBlog first to avoid an alert for displayed errors
|
||||
yield this.get('post').validate({property: 'publishedAtBlog'});
|
||||
|
||||
// actual save will show alert for other failed validations
|
||||
let post = yield this.get('saveTask').perform();
|
||||
|
||||
this._cachePublishedAtBlogTZ();
|
||||
return post;
|
||||
} catch (error) {
|
||||
// re-throw if we don't have a validation error
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
_previousStatus: null,
|
||||
|
||||
_cachePublishedAtBlogTZ() {
|
||||
this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
|
||||
},
|
||||
|
||||
// when closing the menu we reset the publishedAtBlogTZ date so that the
|
||||
// unsaved changes made to the scheduled date aren't reflected in the PSM
|
||||
_resetPublishedAtBlogTZ() {
|
||||
this.get('post').set('publishedAtBlogTZ', this._publishedAtBlogTZ);
|
||||
},
|
||||
|
||||
actions: {
|
||||
setSaveType(saveType) {
|
||||
let post = this.get('post');
|
||||
@ -183,5 +147,39 @@ export default Component.extend({
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
// runningText needs to be declared before the other states change during the
|
||||
// save action.
|
||||
this.set('runningText', this.get('_runningText'));
|
||||
this.set('_previousStatus', this.get('post.status'));
|
||||
this.get('setSaveType')(this.get('saveType'));
|
||||
|
||||
try {
|
||||
// validate publishedAtBlog first to avoid an alert for displayed errors
|
||||
yield this.get('post').validate({property: 'publishedAtBlog'});
|
||||
|
||||
// actual save will show alert for other failed validations
|
||||
let post = yield this.get('saveTask').perform();
|
||||
|
||||
this._cachePublishedAtBlogTZ();
|
||||
return post;
|
||||
} catch (error) {
|
||||
// re-throw if we don't have a validation error
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
_cachePublishedAtBlogTZ() {
|
||||
this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
|
||||
},
|
||||
|
||||
// when closing the menu we reset the publishedAtBlogTZ date so that the
|
||||
// unsaved changes made to the scheduled date aren't reflected in the PSM
|
||||
_resetPublishedAtBlogTZ() {
|
||||
this.get('post').set('publishedAtBlogTZ', this._publishedAtBlogTZ);
|
||||
}
|
||||
});
|
||||
|
@ -2,18 +2,6 @@ import Component from '@ember/component';
|
||||
import {isBlank} from '@ember/utils';
|
||||
|
||||
export default Component.extend({
|
||||
open() {
|
||||
this.get('select.actions').open();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.get('select.actions').close();
|
||||
},
|
||||
|
||||
_focusInput() {
|
||||
this.$('input')[0].focus();
|
||||
},
|
||||
|
||||
actions: {
|
||||
captureMouseDown(e) {
|
||||
e.stopPropagation();
|
||||
@ -50,5 +38,17 @@ export default Component.extend({
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
open() {
|
||||
this.get('select.actions').open();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.get('select.actions').close();
|
||||
},
|
||||
|
||||
_focusInput() {
|
||||
this.$('input')[0].focus();
|
||||
}
|
||||
});
|
||||
|
@ -22,29 +22,87 @@ export function computedGroup(category) {
|
||||
}
|
||||
|
||||
export default Component.extend({
|
||||
store: service('store'),
|
||||
router: service('router'),
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
|
||||
selection: null,
|
||||
content: null,
|
||||
isLoading: false,
|
||||
contentExpiry: 10 * 1000,
|
||||
contentExpiresAt: false,
|
||||
contentExpiry: 10000,
|
||||
currentSearch: '',
|
||||
isLoading: false,
|
||||
selection: null,
|
||||
|
||||
posts: computedGroup('Stories'),
|
||||
pages: computedGroup('Pages'),
|
||||
users: computedGroup('Users'),
|
||||
tags: computedGroup('Tags'),
|
||||
|
||||
_store: service('store'),
|
||||
router: service('router'),
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
groupedContent: computed('posts', 'pages', 'users', 'tags', function () {
|
||||
let groups = [];
|
||||
|
||||
if (!isEmpty(this.get('posts'))) {
|
||||
groups.pushObject({groupName: 'Stories', 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;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.content = [];
|
||||
},
|
||||
|
||||
actions: {
|
||||
openSelected(selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.category === 'Stories' || selected.category === 'Pages') {
|
||||
let id = selected.id.replace('post.', '');
|
||||
this.get('router').transitionTo('editor.edit', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Users') {
|
||||
let id = selected.id.replace('user.', '');
|
||||
this.get('router').transitionTo('team.user', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Tags') {
|
||||
let id = selected.id.replace('tag.', '');
|
||||
this.get('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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refreshContent() {
|
||||
let promises = [];
|
||||
let now = new Date();
|
||||
@ -67,30 +125,8 @@ export default Component.extend({
|
||||
});
|
||||
},
|
||||
|
||||
groupedContent: computed('posts', 'pages', 'users', 'tags', function () {
|
||||
let groups = [];
|
||||
|
||||
if (!isEmpty(this.get('posts'))) {
|
||||
groups.pushObject({groupName: 'Stories', 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 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');
|
||||
@ -107,7 +143,7 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
_loadUsers() {
|
||||
let store = this.get('_store');
|
||||
let store = this.get('store');
|
||||
let usersUrl = `${store.adapterFor('user').urlForQuery({}, 'user')}/`;
|
||||
let usersQuery = {fields: 'name,slug', limit: 'all'};
|
||||
let content = this.get('content');
|
||||
@ -124,7 +160,7 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
_loadTags() {
|
||||
let store = this.get('_store');
|
||||
let store = this.get('store');
|
||||
let tagsUrl = `${store.adapterFor('tag').urlForQuery({}, 'tag')}/`;
|
||||
let tagsQuery = {fields: 'name,slug', limit: 'all'};
|
||||
let content = this.get('content');
|
||||
@ -163,43 +199,5 @@ export default Component.extend({
|
||||
willDestroy() {
|
||||
this._super(...arguments);
|
||||
this._resetKeymasterScope();
|
||||
},
|
||||
|
||||
actions: {
|
||||
openSelected(selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.category === 'Stories' || selected.category === 'Pages') {
|
||||
let id = selected.id.replace('post.', '');
|
||||
this.get('router').transitionTo('editor.edit', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Users') {
|
||||
let id = selected.id.replace('user.', '');
|
||||
this.get('router').transitionTo('team.user', id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Tags') {
|
||||
let id = selected.id.replace('tag.', '');
|
||||
this.get('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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -13,14 +13,14 @@ export default TextArea.extend({
|
||||
value: null,
|
||||
placeholder: '',
|
||||
|
||||
// Private
|
||||
_editor: null,
|
||||
|
||||
// Closure actions
|
||||
onChange() {},
|
||||
onEditorInit() {},
|
||||
onEditorDestroy() {},
|
||||
|
||||
// Private
|
||||
_editor: null,
|
||||
|
||||
// default SimpleMDE options, see docs for available config:
|
||||
// https://github.com/sparksuite/simplemde-markdown-editor#configuration
|
||||
defaultOptions: computed(function () {
|
||||
@ -40,6 +40,23 @@ export default TextArea.extend({
|
||||
}
|
||||
},
|
||||
|
||||
// update the editor when the value property changes from the outside
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (isEmpty(this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// compare values before forcing a content reset to avoid clobbering
|
||||
// the undo behaviour
|
||||
if (this.get('value') !== this._editor.value()) {
|
||||
let cursor = this._editor.codemirror.getDoc().getCursor();
|
||||
this._editor.value(this.get('value'));
|
||||
this._editor.codemirror.getDoc().setCursor(cursor);
|
||||
}
|
||||
},
|
||||
|
||||
// instantiate the editor with the contents of value
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
@ -78,23 +95,6 @@ export default TextArea.extend({
|
||||
this.onEditorInit(this._editor);
|
||||
},
|
||||
|
||||
// update the editor when the value property changes from the outside
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (isEmpty(this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// compare values before forcing a content reset to avoid clobbering
|
||||
// the undo behaviour
|
||||
if (this.get('value') !== this._editor.value()) {
|
||||
let cursor = this._editor.codemirror.getDoc().getCursor();
|
||||
this._editor.value(this.get('value'));
|
||||
this._editor.codemirror.getDoc().setCursor(cursor);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this.onEditorDestroy();
|
||||
this._editor.toTextArea();
|
||||
|
@ -11,21 +11,20 @@ import {inject as service} from '@ember/service';
|
||||
const {Handlebars} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
feature: service(),
|
||||
config: service(),
|
||||
mediaQueries: service(),
|
||||
|
||||
tag: null,
|
||||
|
||||
isViewingSubview: false,
|
||||
|
||||
scratchName: boundOneWay('tag.name'),
|
||||
scratchSlug: boundOneWay('tag.slug'),
|
||||
scratchDescription: boundOneWay('tag.description'),
|
||||
scratchMetaTitle: boundOneWay('tag.metaTitle'),
|
||||
scratchMetaDescription: boundOneWay('tag.metaDescription'),
|
||||
|
||||
isViewingSubview: false,
|
||||
|
||||
feature: service(),
|
||||
config: service(),
|
||||
mediaQueries: service(),
|
||||
|
||||
isMobile: reads('mediaQueries.maxWidth600'),
|
||||
|
||||
title: computed('tag.isNew', function () {
|
||||
@ -97,21 +96,6 @@ export default Component.extend({
|
||||
this._oldTagId = newTagId;
|
||||
},
|
||||
|
||||
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);
|
||||
@ -136,6 +120,21 @@ export default Component.extend({
|
||||
deleteTag() {
|
||||
invokeAction(this, 'showDeleteTagModal');
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('isViewingSubview', false);
|
||||
if (this.$()) {
|
||||
this.$('.settings-menu-pane').scrollTop(0);
|
||||
}
|
||||
},
|
||||
|
||||
focusIn() {
|
||||
key.setScope('tag-settings-form');
|
||||
},
|
||||
|
||||
focusOut() {
|
||||
key.setScope('default');
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -5,27 +5,17 @@ import {isBlank} from '@ember/utils';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
mediaQueries: service(),
|
||||
|
||||
classNames: ['view-container'],
|
||||
classNameBindings: ['isMobile'],
|
||||
|
||||
mediaQueries: service(),
|
||||
|
||||
tags: null,
|
||||
selectedTag: null,
|
||||
|
||||
isMobile: reads('mediaQueries.maxWidth600'),
|
||||
isEmpty: equal('tags.length', 0),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.get('mediaQueries').on('change', this, this._fireMobileChangeActions);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.get('mediaQueries').off('change', this, this._fireMobileChangeActions);
|
||||
},
|
||||
|
||||
displaySettingsPane: computed('isEmpty', 'selectedTag', 'isMobile', function () {
|
||||
let isEmpty = this.get('isEmpty');
|
||||
let selectedTag = this.get('selectedTag');
|
||||
@ -45,6 +35,16 @@ export default Component.extend({
|
||||
return true;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.get('mediaQueries').on('change', this, this._fireMobileChangeActions);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.get('mediaQueries').off('change', this, this._fireMobileChangeActions);
|
||||
},
|
||||
|
||||
_fireMobileChangeActions(key, value) {
|
||||
if (key === 'maxWidth600') {
|
||||
let leftMobileAction = this.get('leftMobile');
|
||||
|
@ -30,7 +30,6 @@ const GhTaskButton = Component.extend({
|
||||
task: null,
|
||||
disabled: false,
|
||||
buttonText: 'Save',
|
||||
runningText: reads('buttonText'),
|
||||
idleClass: '',
|
||||
runningClass: '',
|
||||
successText: 'Saved',
|
||||
@ -39,11 +38,7 @@ const GhTaskButton = Component.extend({
|
||||
failureClass: 'gh-btn-red',
|
||||
|
||||
isRunning: reads('task.last.isRunning'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this._initialPerformCount = this.get('task.performCount');
|
||||
},
|
||||
runningText: reads('buttonText'),
|
||||
|
||||
// hasRun is needed so that a newly rendered button does not show the last
|
||||
// state of the associated task
|
||||
@ -96,6 +91,11 @@ const GhTaskButton = Component.extend({
|
||||
return !this.get('isRunning') && !this.get('isSuccess') && !this.get('isFailure');
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this._initialPerformCount = this.get('task.performCount');
|
||||
},
|
||||
|
||||
click() {
|
||||
// do nothing if disabled externally
|
||||
if (this.get('disabled')) {
|
||||
|
@ -10,12 +10,12 @@ export default OneWayTextarea.extend(TextInputMixin, {
|
||||
|
||||
autoExpand: false,
|
||||
|
||||
willInsertElement() {
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
// disable the draggable resize element that browsers add to textareas
|
||||
// trigger auto-expand any time the value changes
|
||||
if (this.get('autoExpand')) {
|
||||
this.element.style.resize = 'none';
|
||||
run.scheduleOnce('afterRender', this, this._autoExpand);
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,20 +29,20 @@ export default OneWayTextarea.extend(TextInputMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
// trigger auto-expand any time the value changes
|
||||
if (this.get('autoExpand')) {
|
||||
run.scheduleOnce('afterRender', this, this._autoExpand);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._teardownAutoExpand();
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
willInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
// disable the draggable resize element that browsers add to textareas
|
||||
if (this.get('autoExpand')) {
|
||||
this.element.style.resize = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
_autoExpand() {
|
||||
let el = this.element;
|
||||
|
||||
|
@ -6,13 +6,13 @@ import {mapBy} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
clock: service(),
|
||||
|
||||
classNames: ['form-group', 'for-select'],
|
||||
|
||||
activeTimezone: null,
|
||||
availableTimezones: null,
|
||||
|
||||
clock: service(),
|
||||
|
||||
availableTimezoneNames: mapBy('availableTimezones', 'name'),
|
||||
|
||||
hasTimezoneOverride: computed('activeTimezone', 'availableTimezoneNames', function () {
|
||||
|
@ -127,20 +127,6 @@ const GhTourItemComponent = Component.extend({
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
_removeIfViewed(id) {
|
||||
if (id === this.get('throbberId')) {
|
||||
this._remove();
|
||||
}
|
||||
},
|
||||
|
||||
_remove() {
|
||||
this.set('_throbber', null);
|
||||
},
|
||||
|
||||
_close() {
|
||||
this.set('isOpen', false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
open() {
|
||||
this.set('isOpen', true);
|
||||
@ -162,6 +148,20 @@ const GhTourItemComponent = Component.extend({
|
||||
this.set('_throbber', null);
|
||||
this._close();
|
||||
}
|
||||
},
|
||||
|
||||
_removeIfViewed(id) {
|
||||
if (id === this.get('throbberId')) {
|
||||
this._remove();
|
||||
}
|
||||
},
|
||||
|
||||
_remove() {
|
||||
this.set('_throbber', null);
|
||||
},
|
||||
|
||||
_close() {
|
||||
this.set('isOpen', false);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -44,10 +44,10 @@ const UploadTracker = EmberObject.extend({
|
||||
});
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
ajax: service(),
|
||||
|
||||
tagName: '',
|
||||
|
||||
// Public attributes
|
||||
accept: '',
|
||||
extensions: '',
|
||||
@ -98,6 +98,21 @@ export default Component.extend({
|
||||
this._setFiles(files);
|
||||
},
|
||||
|
||||
actions: {
|
||||
setFiles(files, resetInput) {
|
||||
this._setFiles(files);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this._reset();
|
||||
this.onCancel();
|
||||
}
|
||||
},
|
||||
|
||||
_setFiles(files) {
|
||||
this.set('files', files);
|
||||
|
||||
@ -192,6 +207,7 @@ export default Component.extend({
|
||||
this.onComplete(this.get('uploadUrls'));
|
||||
}).drop(),
|
||||
|
||||
// eslint-disable-next-line ghost/ember/order-in-components
|
||||
_uploadFile: task(function* (tracker, file, index) {
|
||||
let ajax = this.get('ajax');
|
||||
let formData = this._getFormData(file);
|
||||
@ -286,20 +302,5 @@ export default Component.extend({
|
||||
this.set('uploadPercentage', 0);
|
||||
this.set('uploadUrls', []);
|
||||
this._uploadTrackers = [];
|
||||
},
|
||||
|
||||
actions: {
|
||||
setFiles(files, resetInput) {
|
||||
this._setFiles(files);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this._reset();
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -7,12 +7,12 @@ Example usage:
|
||||
{{gh-url-preview prefix="tag" slug=theSlugValue tagName="p" classNames="description"}}
|
||||
*/
|
||||
export default Component.extend({
|
||||
config: service(),
|
||||
|
||||
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');
|
||||
|
@ -8,12 +8,12 @@ import {inject as service} from '@ember/service';
|
||||
const {Handlebars} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
ghostPaths: service(),
|
||||
|
||||
tagName: '',
|
||||
|
||||
user: null,
|
||||
|
||||
ghostPaths: service(),
|
||||
|
||||
userDefault: computed('ghostPaths', function () {
|
||||
return `${this.get('ghostPaths.assetRoot')}/img/user-image.png`;
|
||||
}),
|
||||
|
@ -5,14 +5,14 @@ import {isNotFoundError} from 'ember-ajax/errors';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
|
||||
tagName: '',
|
||||
|
||||
invite: null,
|
||||
isSending: false,
|
||||
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
|
||||
createdAt: computed('invite.createdAtUTC', function () {
|
||||
let createdAtUTC = this.get('invite.createdAtUTC');
|
||||
|
||||
|
@ -9,6 +9,26 @@ export default Component.extend({
|
||||
|
||||
_previousKeymasterScope: null,
|
||||
|
||||
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');
|
||||
}
|
||||
},
|
||||
|
||||
_setupShortcuts() {
|
||||
run(function () {
|
||||
document.activeElement.blur();
|
||||
@ -31,25 +51,5 @@ export default Component.extend({
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -9,6 +9,12 @@ export default ModalComponent.extend({
|
||||
store: service(),
|
||||
ajax: service(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteAll').perform();
|
||||
}
|
||||
},
|
||||
|
||||
_deleteAll() {
|
||||
let deleteUrl = this.get('ghostPaths.url').api('db');
|
||||
return this.get('ajax').del(deleteUrl);
|
||||
@ -37,11 +43,5 @@ export default ModalComponent.extend({
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteAll').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -4,11 +4,16 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
notifications: service(),
|
||||
|
||||
post: alias('model.post'),
|
||||
onSuccess: alias('model.onSuccess'),
|
||||
|
||||
notifications: service(),
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deletePost').perform();
|
||||
}
|
||||
},
|
||||
|
||||
_deletePost() {
|
||||
let post = this.get('post');
|
||||
@ -43,11 +48,5 @@ export default ModalComponent.extend({
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deletePost').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -7,13 +7,13 @@ export default ModalComponent.extend({
|
||||
|
||||
subscriber: alias('model'),
|
||||
|
||||
deleteSubscriber: task(function* () {
|
||||
yield invokeAction(this, 'confirm');
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteSubscriber').perform();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
deleteSubscriber: task(function* () {
|
||||
yield invokeAction(this, 'confirm');
|
||||
}).drop()
|
||||
});
|
||||
|
@ -12,17 +12,17 @@ export default ModalComponent.extend({
|
||||
return this.get('tag.count.posts') > 1 ? 'posts' : 'post';
|
||||
}),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteTag').perform();
|
||||
}
|
||||
},
|
||||
|
||||
deleteTag: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteTag').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -8,17 +8,17 @@ export default ModalComponent.extend({
|
||||
theme: alias('model.theme'),
|
||||
download: alias('model.download'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteTheme').perform();
|
||||
}
|
||||
},
|
||||
|
||||
deleteTheme: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteTheme').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -7,17 +7,17 @@ export default ModalComponent.extend({
|
||||
|
||||
user: alias('model'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteUser').perform();
|
||||
}
|
||||
},
|
||||
|
||||
deleteUser: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('deleteUser').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -9,6 +9,9 @@ import {task} from 'ember-concurrency';
|
||||
const {Promise} = RSVP;
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
|
||||
classNames: 'modal-content invite-new-user',
|
||||
|
||||
role: null,
|
||||
@ -17,9 +20,6 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
|
||||
validationType: 'inviteUser',
|
||||
|
||||
notifications: service(),
|
||||
store: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
@ -46,6 +46,16 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
this.set('hasValidated', emberA());
|
||||
},
|
||||
|
||||
actions: {
|
||||
setRole(role) {
|
||||
this.set('role', role);
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('sendInvitation').perform();
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
let email = this.get('email');
|
||||
|
||||
@ -115,15 +125,5 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
setRole(role) {
|
||||
this.set('role', role);
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('sendInvitation').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -8,6 +8,18 @@ export default ModalComponent.extend({
|
||||
|
||||
subscriber: alias('model'),
|
||||
|
||||
actions: {
|
||||
updateEmail(newEmail) {
|
||||
this.set('subscriber.email', newEmail);
|
||||
this.set('subscriber.hasValidated', emberA());
|
||||
this.get('subscriber.errors').clear();
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('addSubscriber').perform();
|
||||
}
|
||||
},
|
||||
|
||||
addSubscriber: task(function* () {
|
||||
try {
|
||||
yield this.get('confirm')();
|
||||
@ -31,17 +43,5 @@ export default ModalComponent.extend({
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
updateEmail(newEmail) {
|
||||
this.set('subscriber.email', newEmail);
|
||||
this.set('subscriber.hasValidated', emberA());
|
||||
this.get('subscriber.errors').clear();
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('addSubscriber').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -8,18 +8,24 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
validationType: 'signin',
|
||||
|
||||
authenticationError: null,
|
||||
|
||||
config: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
|
||||
validationType: 'signin',
|
||||
|
||||
authenticationError: null,
|
||||
|
||||
identification: computed('session.user.email', function () {
|
||||
return this.get('session.user.email');
|
||||
}),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('reauthenticate').perform();
|
||||
}
|
||||
},
|
||||
|
||||
_authenticate() {
|
||||
let session = this.get('session');
|
||||
let authStrategy = 'authenticator:oauth2';
|
||||
@ -68,11 +74,5 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
|
||||
reauthenticate: task(function* () {
|
||||
return yield this._passwordConfirm();
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('reauthenticate').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -7,17 +7,17 @@ export default ModalComponent.extend({
|
||||
|
||||
user: alias('model'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
return this.get('suspendUser').perform();
|
||||
}
|
||||
},
|
||||
|
||||
suspendUser: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
return this.get('suspendUser').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -2,12 +2,12 @@ import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {reads} from '@ember/object/computed';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
'data-test-theme-warnings-modal': true,
|
||||
|
||||
title: reads('model.title'),
|
||||
message: reads('model.message'),
|
||||
warnings: reads('model.warnings'),
|
||||
errors: reads('model.errors'),
|
||||
fatalErrors: reads('model.fatalErrors'),
|
||||
canActivate: reads('model.canActivate'),
|
||||
|
||||
'data-test-theme-warnings-modal': true
|
||||
canActivate: reads('model.canActivate')
|
||||
});
|
||||
|
@ -5,17 +5,17 @@ import {task} from 'ember-concurrency';
|
||||
export default ModalComponent.extend({
|
||||
user: null,
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('transferOwnership').perform();
|
||||
}
|
||||
},
|
||||
|
||||
transferOwnership: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.get('transferOwnership').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -7,17 +7,17 @@ export default ModalComponent.extend({
|
||||
|
||||
user: alias('model'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
return this.get('unsuspendUser').perform();
|
||||
}
|
||||
},
|
||||
|
||||
unsuspendUser: task(function* () {
|
||||
try {
|
||||
yield invokeAction(this, 'confirm');
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
return this.get('unsuspendUser').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -6,15 +6,15 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
config: service(),
|
||||
notifications: service(),
|
||||
|
||||
model: null,
|
||||
|
||||
url: '',
|
||||
newUrl: '',
|
||||
_isUploading: false,
|
||||
|
||||
config: service(),
|
||||
notifications: service(),
|
||||
|
||||
image: computed('model.{model,imageProperty}', {
|
||||
get() {
|
||||
let imageProperty = this.get('model.imageProperty');
|
||||
@ -36,6 +36,26 @@ export default ModalComponent.extend({
|
||||
this.set('newUrl', image);
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileUploaded(url) {
|
||||
this.set('url', url);
|
||||
this.set('newUrl', url);
|
||||
},
|
||||
|
||||
removeImage() {
|
||||
this.set('url', '');
|
||||
this.set('newUrl', '');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('uploadImage').perform();
|
||||
},
|
||||
|
||||
isUploading() {
|
||||
this.toggleProperty('_isUploading');
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
@ -83,25 +103,5 @@ export default ModalComponent.extend({
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
fileUploaded(url) {
|
||||
this.set('url', url);
|
||||
this.set('newUrl', url);
|
||||
},
|
||||
|
||||
removeImage() {
|
||||
this.set('url', '');
|
||||
this.set('newUrl', '');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.get('uploadImage').perform();
|
||||
},
|
||||
|
||||
isUploading() {
|
||||
this.toggleProperty('_isUploading');
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -17,6 +17,8 @@ const DEFAULTS = {
|
||||
};
|
||||
|
||||
export default ModalComponent.extend({
|
||||
eventBus: service(),
|
||||
store: service(),
|
||||
|
||||
accept: null,
|
||||
extensions: null,
|
||||
@ -26,10 +28,8 @@ export default ModalComponent.extend({
|
||||
theme: false,
|
||||
displayOverwriteWarning: false,
|
||||
|
||||
eventBus: service(),
|
||||
store: service(),
|
||||
|
||||
hideUploader: or('theme', 'displayOverwriteWarning'),
|
||||
currentThemeNames: mapBy('model.themes', 'name'),
|
||||
|
||||
uploadUrl: computed(function () {
|
||||
return `${ghostPaths().apiRoot}/themes/upload/`;
|
||||
@ -42,8 +42,6 @@ export default ModalComponent.extend({
|
||||
return themePackage ? `${themePackage.name} - ${themePackage.version}` : name;
|
||||
}),
|
||||
|
||||
currentThemeNames: mapBy('model.themes', 'name'),
|
||||
|
||||
fileThemeName: computed('file', function () {
|
||||
let file = this.get('file');
|
||||
return file.name.replace(/\.zip$/, '');
|
||||
|
@ -4,8 +4,8 @@ import {readOnly} from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
|
||||
error: readOnly('model'),
|
||||
stack: false,
|
||||
error: readOnly('model'),
|
||||
|
||||
code: computed('error.status', function () {
|
||||
return this.get('error.status') > 200 ? this.get('error.status') : 500;
|
||||
|
@ -37,9 +37,14 @@ export default Controller.extend({
|
||||
session: service(),
|
||||
store: service(),
|
||||
|
||||
postsInfinityModel: alias('model'),
|
||||
|
||||
queryParams: ['type', 'author', 'tag', 'order'],
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.availableTypes = TYPES;
|
||||
this.availableOrders = ORDERS;
|
||||
},
|
||||
|
||||
type: null,
|
||||
author: null,
|
||||
tag: null,
|
||||
@ -51,6 +56,8 @@ export default Controller.extend({
|
||||
availableTypes: null,
|
||||
availableOrders: null,
|
||||
|
||||
postsInfinityModel: alias('model'),
|
||||
|
||||
showingAll: computed('type', 'author', 'tag', function () {
|
||||
let {type, author, tag} = this.getProperties(['type', 'author', 'tag']);
|
||||
|
||||
@ -107,12 +114,6 @@ export default Controller.extend({
|
||||
return authors.findBy('slug', author);
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.availableTypes = TYPES;
|
||||
this.availableOrders = ORDERS;
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeType(type) {
|
||||
this.set('type', get(type, 'value'));
|
||||
|
@ -6,6 +6,12 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
|
||||
newPassword: '',
|
||||
ne2Password: '',
|
||||
token: '',
|
||||
@ -13,18 +19,18 @@ export default Controller.extend(ValidationEngine, {
|
||||
|
||||
validationType: 'reset',
|
||||
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
ajax: service(),
|
||||
config: 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];
|
||||
}),
|
||||
|
||||
actions: {
|
||||
submit() {
|
||||
return this.get('resetPassword').perform();
|
||||
}
|
||||
},
|
||||
|
||||
// Used to clear sensitive information
|
||||
clearData() {
|
||||
this.setProperties({
|
||||
@ -68,11 +74,5 @@ export default Controller.extend(ValidationEngine, {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
submit() {
|
||||
return this.get('resetPassword').perform();
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -8,23 +8,9 @@ export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
ampSettings: alias('settings.amp'),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
|
||||
save: task(function* () {
|
||||
let amp = this.get('ampSettings');
|
||||
let settings = this.get('settings');
|
||||
|
||||
settings.set('amp', amp);
|
||||
|
||||
try {
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}).drop(),
|
||||
ampSettings: alias('settings.amp'),
|
||||
|
||||
actions: {
|
||||
update(value) {
|
||||
@ -73,5 +59,19 @@ export default Controller.extend({
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let amp = this.get('ampSettings');
|
||||
let settings = this.get('settings');
|
||||
|
||||
settings.set('amp', amp);
|
||||
|
||||
try {
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -12,53 +12,16 @@ export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
slackSettings: boundOneWay('settings.slack.firstObject'),
|
||||
testNotificationDisabled: empty('slackSettings.url'),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
slackArray: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.slackArray = [];
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let slack = this.get('slackSettings');
|
||||
let settings = this.get('settings');
|
||||
let slackArray = this.get('slackArray');
|
||||
leaveSettingsTransition: null,
|
||||
slackArray: null,
|
||||
|
||||
try {
|
||||
yield slack.validate();
|
||||
// clear existing objects in slackArray to make sure we only push the validated one
|
||||
slackArray.clear().pushObject(slack);
|
||||
yield settings.set('slack', slackArray);
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
sendTestNotification: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let slackApi = this.get('ghostPaths.url').api('slack', 'test');
|
||||
|
||||
try {
|
||||
yield this.get('save').perform();
|
||||
yield this.get('ajax').post(slackApi);
|
||||
notifications.showNotification('Check your Slack channel for the test message!', {type: 'info', key: 'slack-test.send.success'});
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'slack-test:send'});
|
||||
|
||||
if (!isInvalidError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
slackSettings: boundOneWay('settings.slack.firstObject'),
|
||||
testNotificationDisabled: empty('slackSettings.url'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
@ -123,5 +86,42 @@ export default Controller.extend({
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let slack = this.get('slackSettings');
|
||||
let settings = this.get('settings');
|
||||
let slackArray = this.get('slackArray');
|
||||
|
||||
try {
|
||||
yield slack.validate();
|
||||
// clear existing objects in slackArray to make sure we only push the validated one
|
||||
slackArray.clear().pushObject(slack);
|
||||
yield settings.set('slack', slackArray);
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
sendTestNotification: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let slackApi = this.get('ghostPaths.url').api('slack', 'test');
|
||||
|
||||
try {
|
||||
yield this.get('save').perform();
|
||||
yield this.get('ajax').post(slackApi);
|
||||
notifications.showNotification('Check your Slack channel for the test message!', {type: 'info', key: 'slack-test.send.success'});
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'slack-test:send'});
|
||||
|
||||
if (!isInvalidError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -8,28 +8,11 @@ export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
unsplashSettings: alias('settings.unsplash'),
|
||||
dirtyAttributes: null,
|
||||
rollbackValue: null,
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
|
||||
save: task(function* () {
|
||||
let unsplash = this.get('unsplashSettings');
|
||||
let settings = this.get('settings');
|
||||
|
||||
try {
|
||||
settings.set('unsplash', unsplash);
|
||||
this.set('dirtyAttributes', false);
|
||||
this.set('rollbackValue', null);
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
unsplashSettings: alias('settings.unsplash'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
@ -83,5 +66,22 @@ export default Controller.extend({
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let unsplash = this.get('unsplashSettings');
|
||||
let settings = this.get('settings');
|
||||
|
||||
try {
|
||||
settings.set('unsplash', unsplash);
|
||||
this.set('dirtyAttributes', false);
|
||||
this.set('rollbackValue', null);
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
||||
|
@ -7,17 +7,6 @@ export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
save: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
|
||||
try {
|
||||
return yield this.get('settings').save();
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'code-injection.save'});
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('save').perform();
|
||||
@ -62,5 +51,16 @@ export default Controller.extend({
|
||||
return transition.retry();
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
|
||||
try {
|
||||
return yield this.get('settings').save();
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'code-injection.save'});
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -17,6 +17,11 @@ export default Controller.extend({
|
||||
session: service(),
|
||||
settings: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
},
|
||||
|
||||
newNavItem: null,
|
||||
|
||||
dirtyAttributes: false,
|
||||
@ -31,65 +36,6 @@ export default Controller.extend({
|
||||
return url.slice(-1) !== '/' ? `${url}/` : url;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let navItems = this.get('settings.navigation');
|
||||
let newNavItem = this.get('newNavItem');
|
||||
let notifications = this.get('notifications');
|
||||
let validationPromises = [];
|
||||
|
||||
if (!newNavItem.get('isBlank')) {
|
||||
validationPromises.pushObject(this.send('addNavItem'));
|
||||
}
|
||||
|
||||
navItems.map((item) => {
|
||||
validationPromises.pushObject(item.validate());
|
||||
});
|
||||
|
||||
try {
|
||||
yield RSVP.all(validationPromises);
|
||||
this.set('dirtyAttributes', false);
|
||||
return yield this.get('settings').save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
addNewNavItem() {
|
||||
let navItems = this.get('settings.navigation');
|
||||
let newNavItem = this.get('newNavItem');
|
||||
|
||||
newNavItem.set('isNew', false);
|
||||
navItems.pushObject(newNavItem);
|
||||
this.set('dirtyAttributes', true);
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
$('.gh-blognav-line:last input:first').focus();
|
||||
},
|
||||
|
||||
_deleteTheme() {
|
||||
let theme = this.get('store').peekRecord('theme', this.get('themeToDelete').name);
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
return theme.destroyRecord().then(() => {
|
||||
// HACK: this is a private method, we need to unload from the store
|
||||
// here so that uploading another theme with the same "id" doesn't
|
||||
// attempt to update the deleted record
|
||||
theme.unloadRecord();
|
||||
}).catch((error) => {
|
||||
this.get('notifications').showAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('save').perform();
|
||||
@ -260,5 +206,59 @@ export default Controller.extend({
|
||||
reset() {
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let navItems = this.get('settings.navigation');
|
||||
let newNavItem = this.get('newNavItem');
|
||||
let notifications = this.get('notifications');
|
||||
let validationPromises = [];
|
||||
|
||||
if (!newNavItem.get('isBlank')) {
|
||||
validationPromises.pushObject(this.send('addNavItem'));
|
||||
}
|
||||
|
||||
navItems.map((item) => {
|
||||
validationPromises.pushObject(item.validate());
|
||||
});
|
||||
|
||||
try {
|
||||
yield RSVP.all(validationPromises);
|
||||
this.set('dirtyAttributes', false);
|
||||
return yield this.get('settings').save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
addNewNavItem() {
|
||||
let navItems = this.get('settings.navigation');
|
||||
let newNavItem = this.get('newNavItem');
|
||||
|
||||
newNavItem.set('isNew', false);
|
||||
navItems.pushObject(newNavItem);
|
||||
this.set('dirtyAttributes', true);
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
$('.gh-blognav-line:last input:first').focus();
|
||||
},
|
||||
|
||||
_deleteTheme() {
|
||||
let theme = this.get('store').peekRecord('theme', this.get('themeToDelete').name);
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
return theme.destroyRecord().then(() => {
|
||||
// HACK: this is a private method, we need to unload from the store
|
||||
// here so that uploading another theme with the same "id" doesn't
|
||||
// attempt to update the deleted record
|
||||
theme.unloadRecord();
|
||||
}).catch((error) => {
|
||||
this.get('notifications').showAPIError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -20,6 +20,11 @@ export default Controller.extend({
|
||||
session: service(),
|
||||
settings: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.iconExtensions = ICON_EXTENSIONS;
|
||||
},
|
||||
|
||||
availableTimezones: null,
|
||||
iconExtensions: null,
|
||||
iconMimeTypes: 'image/png,image/x-icon',
|
||||
@ -51,44 +56,6 @@ export default Controller.extend({
|
||||
return `${blogUrl}/${publicHash}/rss`;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.iconExtensions = ICON_EXTENSIONS;
|
||||
},
|
||||
|
||||
_deleteTheme() {
|
||||
let theme = this.get('store').peekRecord('theme', this.get('themeToDelete').name);
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
return theme.destroyRecord().catch((error) => {
|
||||
this.get('notifications').showAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let config = this.get('config');
|
||||
|
||||
try {
|
||||
let settings = yield this.get('settings').save();
|
||||
config.set('blogTitle', settings.get('title'));
|
||||
|
||||
// this forces the document title to recompute after
|
||||
// a blog title change
|
||||
this.send('collectTitleTokens', []);
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
notifications.showAPIError(error, {key: 'settings.save'});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('save').perform();
|
||||
@ -294,5 +261,38 @@ export default Controller.extend({
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_deleteTheme() {
|
||||
let theme = this.get('store').peekRecord('theme', this.get('themeToDelete').name);
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
return theme.destroyRecord().catch((error) => {
|
||||
this.get('notifications').showAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let config = this.get('config');
|
||||
|
||||
try {
|
||||
let settings = yield this.get('settings').save();
|
||||
config.set('blogTitle', settings.get('title'));
|
||||
|
||||
// this forces the document title to recompute after
|
||||
// a blog title change
|
||||
this.send('collectTitleTokens', []);
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
notifications.showAPIError(error, {key: 'settings.save'});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -26,16 +26,6 @@ const JSON_EXTENSION = ['json'];
|
||||
const JSON_MIME_TYPE = ['application/json'];
|
||||
|
||||
export default Controller.extend({
|
||||
importErrors: null,
|
||||
importSuccessful: false,
|
||||
showDeleteAllModal: false,
|
||||
submitting: false,
|
||||
uploadButtonText: 'Import',
|
||||
|
||||
importMimeType: null,
|
||||
jsonExtension: null,
|
||||
jsonMimeType: null,
|
||||
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
feature: service(),
|
||||
@ -51,75 +41,15 @@ export default Controller.extend({
|
||||
this.jsonMimeType = JSON_MIME_TYPE;
|
||||
},
|
||||
|
||||
// TODO: convert to ember-concurrency task
|
||||
_validate(file) {
|
||||
// Windows doesn't have mime-types for json files by default, so we
|
||||
// need to have some additional checking
|
||||
if (file.type === '') {
|
||||
// First check file extension so we can early return
|
||||
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
importErrors: null,
|
||||
importSuccessful: false,
|
||||
showDeleteAllModal: false,
|
||||
submitting: false,
|
||||
uploadButtonText: 'Import',
|
||||
|
||||
if (!extension || extension.toLowerCase() !== 'json') {
|
||||
return RSVP.reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Extension is correct, so check the contents of the file
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = function () {
|
||||
let {result} = reader;
|
||||
|
||||
try {
|
||||
JSON.parse(result);
|
||||
|
||||
return resolve();
|
||||
} catch (e) {
|
||||
return reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
let accept = this.get('importMimeType');
|
||||
|
||||
if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) {
|
||||
return RSVP.reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
|
||||
return RSVP.resolve();
|
||||
},
|
||||
|
||||
sendTestEmail: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let emailUrl = this.get('ghostPaths.url').api('mail', 'test');
|
||||
|
||||
try {
|
||||
yield this.get('ajax').post(emailUrl);
|
||||
notifications.showAlert('Check your email for the test message.', {type: 'info', key: 'test-email.send.success'});
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'test-email:send'});
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
redirectUploadResult: task(function* (success) {
|
||||
this.set('redirectSuccess', success);
|
||||
this.set('redirectFailure', !success);
|
||||
|
||||
yield timeout(Ember.testing ? 100 : 5000); // eslint-disable-line
|
||||
|
||||
this.set('redirectSuccess', null);
|
||||
this.set('redirectFailure', null);
|
||||
return true;
|
||||
}).drop(),
|
||||
|
||||
reset() {
|
||||
this.set('importErrors', null);
|
||||
this.set('importSuccessful', false);
|
||||
},
|
||||
importMimeType: null,
|
||||
jsonExtension: null,
|
||||
jsonMimeType: null,
|
||||
|
||||
actions: {
|
||||
onUpload(file) {
|
||||
@ -217,5 +147,75 @@ export default Controller.extend({
|
||||
.find('input[type="file"]')
|
||||
.click();
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: convert to ember-concurrency task
|
||||
_validate(file) {
|
||||
// Windows doesn't have mime-types for json files by default, so we
|
||||
// need to have some additional checking
|
||||
if (file.type === '') {
|
||||
// First check file extension so we can early return
|
||||
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
|
||||
if (!extension || extension.toLowerCase() !== 'json') {
|
||||
return RSVP.reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Extension is correct, so check the contents of the file
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = function () {
|
||||
let {result} = reader;
|
||||
|
||||
try {
|
||||
JSON.parse(result);
|
||||
|
||||
return resolve();
|
||||
} catch (e) {
|
||||
return reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
let accept = this.get('importMimeType');
|
||||
|
||||
if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) {
|
||||
return RSVP.reject(new UnsupportedMediaTypeError());
|
||||
}
|
||||
|
||||
return RSVP.resolve();
|
||||
},
|
||||
|
||||
sendTestEmail: task(function* () {
|
||||
let notifications = this.get('notifications');
|
||||
let emailUrl = this.get('ghostPaths.url').api('mail', 'test');
|
||||
|
||||
try {
|
||||
yield this.get('ajax').post(emailUrl);
|
||||
notifications.showAlert('Check your email for the test message.', {type: 'info', key: 'test-email.send.success'});
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'test-email:send'});
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
redirectUploadResult: task(function* (success) {
|
||||
this.set('redirectSuccess', success);
|
||||
this.set('redirectFailure', !success);
|
||||
|
||||
yield timeout(Ember.testing ? 100 : 5000); // eslint-disable-line
|
||||
|
||||
this.set('redirectSuccess', null);
|
||||
this.set('redirectFailure', null);
|
||||
return true;
|
||||
}).drop(),
|
||||
|
||||
reset() {
|
||||
this.set('importErrors', null);
|
||||
this.set('importSuccessful', false);
|
||||
}
|
||||
});
|
||||
|
@ -26,6 +26,17 @@ export default Controller.extend({
|
||||
return 0;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
leftMobile() {
|
||||
let firstTag = this.get('tags.firstObject');
|
||||
// redirect to first tag if possible so that you're not left with
|
||||
// tag settings blank slate when switching from portrait to landscape
|
||||
if (firstTag && !this.get('tagController.tag')) {
|
||||
this.transitionToRoute('settings.tags.tag', firstTag);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
scrollTagIntoView(tag) {
|
||||
run.scheduleOnce('afterRender', this, function () {
|
||||
let id = `#gh-tag-${tag.get('id')}`;
|
||||
@ -48,17 +59,5 @@ export default Controller.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
leftMobile() {
|
||||
let firstTag = this.get('tags.firstObject');
|
||||
// redirect to first tag if possible so that you're not left with
|
||||
// tag settings blank slate when switching from portrait to landscape
|
||||
if (firstTag && !this.get('tagController.tag')) {
|
||||
this.transitionToRoute('settings.tags.tag', firstTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -3,15 +3,28 @@ import {alias} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Controller.extend({
|
||||
applicationController: controller('application'),
|
||||
tagsController: controller('settings.tags'),
|
||||
notifications: service(),
|
||||
|
||||
showDeleteTagModal: false,
|
||||
|
||||
tag: alias('model'),
|
||||
isMobile: alias('tagsController.isMobile'),
|
||||
|
||||
applicationController: controller('application'),
|
||||
tagsController: controller('settings.tags'),
|
||||
notifications: service(),
|
||||
actions: {
|
||||
setProperty(propKey, value) {
|
||||
this._saveTagProperty(propKey, value);
|
||||
},
|
||||
|
||||
toggleDeleteTagModal() {
|
||||
this.toggleProperty('showDeleteTagModal');
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
return this._deleteTag();
|
||||
}
|
||||
},
|
||||
|
||||
_saveTagProperty(propKey, newValue) {
|
||||
let tag = this.get('tag');
|
||||
@ -60,19 +73,5 @@ export default Controller.extend({
|
||||
|
||||
_deleteTagFailure(error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'tag.delete'});
|
||||
},
|
||||
|
||||
actions: {
|
||||
setProperty(propKey, value) {
|
||||
this._saveTagProperty(propKey, value);
|
||||
},
|
||||
|
||||
toggleDeleteTagModal() {
|
||||
this.toggleProperty('showDeleteTagModal');
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
return this._deleteTag();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -14,12 +14,13 @@ import {task, timeout} from 'ember-concurrency';
|
||||
const {Errors} = DS;
|
||||
|
||||
export default Controller.extend({
|
||||
notifications: service(),
|
||||
two: controller('setup/two'),
|
||||
notifications: service(),
|
||||
|
||||
users: '',
|
||||
|
||||
errors: Errors.create(),
|
||||
hasValidated: emberA(),
|
||||
users: '',
|
||||
ownerEmail: alias('two.email'),
|
||||
|
||||
usersArray: computed('users', function () {
|
||||
@ -71,31 +72,6 @@ export default Controller.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
validate() {
|
||||
let errors = this.get('errors');
|
||||
let validationResult = this.get('validationResult');
|
||||
let property = 'users';
|
||||
|
||||
errors.clear();
|
||||
|
||||
// If property isn't in the `hasValidated` array, add it to mark that this field can show a validation result
|
||||
this.get('hasValidated').addObject(property);
|
||||
|
||||
if (validationResult === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
validationResult.forEach((error) => {
|
||||
// Only one error type here so far, but one day the errors might be more detailed
|
||||
switch (error.error) {
|
||||
case 'email':
|
||||
errors.add(property, `${error.user} is not a valid email.`);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
buttonText: computed('errors.users', 'validUsersArray', 'invalidUsersArray', function () {
|
||||
let usersError = this.get('errors.users.firstObject.message');
|
||||
let validNum = this.get('validUsersArray').length;
|
||||
@ -133,6 +109,46 @@ export default Controller.extend({
|
||||
return this.store.findAll('role', {reload: true}).then(roles => roles.findBy('name', 'Author'));
|
||||
}),
|
||||
|
||||
actions: {
|
||||
validate() {
|
||||
this.validate();
|
||||
},
|
||||
|
||||
invite() {
|
||||
this.get('invite').perform();
|
||||
},
|
||||
|
||||
skipInvite() {
|
||||
this.send('loadServerNotifications');
|
||||
this.transitionToRoute('posts.index');
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
let errors = this.get('errors');
|
||||
let validationResult = this.get('validationResult');
|
||||
let property = 'users';
|
||||
|
||||
errors.clear();
|
||||
|
||||
// If property isn't in the `hasValidated` array, add it to mark that this field can show a validation result
|
||||
this.get('hasValidated').addObject(property);
|
||||
|
||||
if (validationResult === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
validationResult.forEach((error) => {
|
||||
// Only one error type here so far, but one day the errors might be more detailed
|
||||
switch (error.error) {
|
||||
case 'email':
|
||||
errors.add(property, `${error.user} is not a valid email.`);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_transitionAfterSubmission() {
|
||||
if (!this._hasTransitioned) {
|
||||
this._hasTransitioned = true;
|
||||
@ -223,20 +239,5 @@ export default Controller.extend({
|
||||
invitationsString = successCount > 1 ? 'invitations' : 'invitation';
|
||||
notifications.showAlert(`${successCount} ${invitationsString} sent!`, {type: 'success', delayed: true, key: 'signup.send-invitations.success'});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
validate() {
|
||||
this.validate();
|
||||
},
|
||||
|
||||
invite() {
|
||||
this.get('invite').perform();
|
||||
},
|
||||
|
||||
skipInvite() {
|
||||
this.send('loadServerNotifications');
|
||||
this.transitionToRoute('posts.index');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -8,8 +8,8 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
ajax: service(),
|
||||
application: controller(),
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
@ -27,6 +27,23 @@ export default Controller.extend(ValidationEngine, {
|
||||
name: null,
|
||||
password: null,
|
||||
|
||||
actions: {
|
||||
setup() {
|
||||
this.get('setup').perform();
|
||||
},
|
||||
|
||||
preValidate(model) {
|
||||
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
||||
if (this.get(model)) {
|
||||
return this.validate({property: model});
|
||||
}
|
||||
},
|
||||
|
||||
setImage(image) {
|
||||
this.set('profileImage', image);
|
||||
}
|
||||
},
|
||||
|
||||
setup: task(function* () {
|
||||
return yield this._passwordSetup();
|
||||
}),
|
||||
@ -173,22 +190,5 @@ export default Controller.extend(ValidationEngine, {
|
||||
} else {
|
||||
return fetchSettingsAndConfig.then(() => this.transitionToRoute('setup.three'));
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setup() {
|
||||
this.get('setup').perform();
|
||||
},
|
||||
|
||||
preValidate(model) {
|
||||
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
||||
if (this.get(model)) {
|
||||
return this.validate({property: model});
|
||||
}
|
||||
},
|
||||
|
||||
setImage(image) {
|
||||
this.set('profileImage', image);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -9,29 +9,35 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
submitting: false,
|
||||
loggingIn: false,
|
||||
authProperties: null,
|
||||
|
||||
ajax: service(),
|
||||
application: controller(),
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
settings: service(),
|
||||
|
||||
flowErrors: '',
|
||||
signin: alias('model'),
|
||||
|
||||
// ValidationEngine settings
|
||||
validationType: 'signin',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.authProperties = ['identification', 'password'];
|
||||
},
|
||||
|
||||
submitting: false,
|
||||
loggingIn: false,
|
||||
authProperties: null,
|
||||
|
||||
flowErrors: '',
|
||||
// ValidationEngine settings
|
||||
validationType: 'signin',
|
||||
|
||||
signin: alias('model'),
|
||||
|
||||
actions: {
|
||||
authenticate() {
|
||||
this.get('validateAndAuthenticate').perform();
|
||||
}
|
||||
},
|
||||
|
||||
authenticate: task(function* (authStrategy, authentication) {
|
||||
try {
|
||||
let authResult = yield this.get('session')
|
||||
@ -131,11 +137,5 @@ export default Controller.extend(ValidationEngine, {
|
||||
notifications.showAPIError(error, {defaultErrorText: 'There was a problem with the reset, please try again.', key: 'forgot-password.send'});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
authenticate() {
|
||||
this.get('validateAndAuthenticate').perform();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -18,13 +18,23 @@ export default Controller.extend(ValidationEngine, {
|
||||
session: service(),
|
||||
settings: service(),
|
||||
|
||||
// ValidationEngine settings
|
||||
signupDetails: alias('model'),
|
||||
validationType: 'signup',
|
||||
|
||||
flowErrors: '',
|
||||
profileImage: null,
|
||||
|
||||
// ValidationEngine settings
|
||||
validationType: 'signup',
|
||||
signupDetails: alias('model'),
|
||||
|
||||
actions: {
|
||||
signup() {
|
||||
this.get('signup').perform();
|
||||
},
|
||||
|
||||
setImage(image) {
|
||||
this.set('profileImage', image);
|
||||
}
|
||||
},
|
||||
|
||||
authenticate: task(function* (authStrategy, authentication) {
|
||||
try {
|
||||
let authResult = yield this.get('session')
|
||||
@ -155,15 +165,5 @@ export default Controller.extend(ValidationEngine, {
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
signup() {
|
||||
this.get('signup').perform();
|
||||
},
|
||||
|
||||
setImage(image) {
|
||||
this.set('profileImage', image);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Controller.extend(PaginationMixin, {
|
||||
session: service(),
|
||||
|
||||
queryParams: ['order', 'direction'],
|
||||
order: 'created_at',
|
||||
@ -20,8 +21,6 @@ export default Controller.extend(PaginationMixin, {
|
||||
table: null,
|
||||
subscriberToDelete: null,
|
||||
|
||||
session: service(),
|
||||
|
||||
// paginationSettings is replaced by the pagination mixin so we need a
|
||||
// getter/setter CP here so that we don't lose the dynamic order param
|
||||
paginationSettings: computed('order', 'direction', {
|
||||
@ -81,17 +80,6 @@ export default Controller.extend(PaginationMixin, {
|
||||
}];
|
||||
}),
|
||||
|
||||
initializeTable() {
|
||||
this.set('table', new Table(this.get('columns'), this.get('subscribers')));
|
||||
},
|
||||
|
||||
// capture the total from the server any time we fetch a new page
|
||||
didReceivePaginationMeta(meta) {
|
||||
if (meta && meta.pagination) {
|
||||
this.set('total', meta.pagination.total);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadFirstPage() {
|
||||
let table = this.get('table');
|
||||
@ -164,5 +152,16 @@ export default Controller.extend(PaginationMixin, {
|
||||
|
||||
iframe.attr('src', downloadURL);
|
||||
}
|
||||
},
|
||||
|
||||
initializeTable() {
|
||||
this.set('table', new Table(this.get('columns'), this.get('subscribers')));
|
||||
},
|
||||
|
||||
// capture the total from the server any time we fetch a new page
|
||||
didReceivePaginationMeta(meta) {
|
||||
if (meta && meta.pagination) {
|
||||
this.set('total', meta.pagination.total);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4,9 +4,14 @@ import {inject as service} from '@ember/service';
|
||||
import {sort} from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
|
||||
session: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.inviteOrder = ['email'];
|
||||
this.userOrder = ['name', 'email'];
|
||||
},
|
||||
|
||||
showInviteUserModal: false,
|
||||
|
||||
activeUsers: null,
|
||||
@ -20,12 +25,6 @@ export default Controller.extend({
|
||||
sortedActiveUsers: sort('activeUsers', 'userOrder'),
|
||||
sortedSuspendedUsers: sort('suspendedUsers', 'userOrder'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.inviteOrder = ['email'];
|
||||
this.userOrder = ['name', 'email'];
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleInviteUserModal() {
|
||||
this.toggleProperty('showInviteUserModal');
|
||||
|
@ -15,6 +15,14 @@ import {task, taskGroup} from 'ember-concurrency';
|
||||
const {Handlebars} = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
dropdown: service(),
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
slugGenerator: service(),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
dirtyAttributes: false,
|
||||
showDeleteUserModal: false,
|
||||
@ -25,13 +33,7 @@ export default Controller.extend({
|
||||
_scratchFacebook: null,
|
||||
_scratchTwitter: null,
|
||||
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
dropdown: service(),
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
slugGenerator: service(),
|
||||
saveHandlers: taskGroup().enqueue(),
|
||||
|
||||
user: alias('model'),
|
||||
currentUser: alias('session.user'),
|
||||
@ -48,10 +50,10 @@ export default Controller.extend({
|
||||
rolesDropdownIsVisible: and('isNotOwnProfile', 'canAssignRoles', 'isNotOwnersProfile'),
|
||||
userActionsAreVisible: or('deleteUserActionIsVisible', 'canMakeOwner'),
|
||||
|
||||
isNotOwnProfile: not('isOwnProfile'),
|
||||
isOwnProfile: computed('user.id', 'currentUser.id', function () {
|
||||
return this.get('user.id') === this.get('currentUser.id');
|
||||
}),
|
||||
isNotOwnProfile: not('isOwnProfile'),
|
||||
|
||||
deleteUserActionIsVisible: computed('currentUser', 'canAssignRoles', 'user', function () {
|
||||
if ((this.get('canAssignRoles') && this.get('isNotOwnProfile') && !this.get('user.isOwner'))
|
||||
@ -93,113 +95,6 @@ export default Controller.extend({
|
||||
return this.store.query('role', {permissions: 'assign'});
|
||||
}),
|
||||
|
||||
_deleteUser() {
|
||||
if (this.get('deleteUserActionIsVisible')) {
|
||||
let user = this.get('user');
|
||||
return user.destroyRecord();
|
||||
}
|
||||
},
|
||||
|
||||
_deleteUserSuccess() {
|
||||
this.get('notifications').closeAlerts('user.delete');
|
||||
this.store.unloadAll('post');
|
||||
this.transitionToRoute('team');
|
||||
},
|
||||
|
||||
_deleteUserFailure() {
|
||||
this.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'});
|
||||
},
|
||||
|
||||
saveHandlers: taskGroup().enqueue(),
|
||||
|
||||
updateSlug: task(function* (newSlug) {
|
||||
let slug = this.get('user.slug');
|
||||
|
||||
newSlug = newSlug || slug;
|
||||
newSlug = newSlug.trim();
|
||||
|
||||
// Ignore unchanged slugs or candidate slugs that are empty
|
||||
if (!newSlug || slug === newSlug) {
|
||||
this.set('slugValue', slug);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let serverSlug = yield this.get('slugGenerator').generateSlug('user', newSlug);
|
||||
|
||||
// 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 true;
|
||||
}
|
||||
|
||||
// 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 true;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('slugValue', serverSlug);
|
||||
this.set('dirtyAttributes', true);
|
||||
|
||||
return true;
|
||||
}).group('saveHandlers'),
|
||||
|
||||
save: task(function* () {
|
||||
let user = this.get('user');
|
||||
let slugValue = this.get('slugValue');
|
||||
let slugChanged;
|
||||
|
||||
if (user.get('slug') !== slugValue) {
|
||||
slugChanged = true;
|
||||
user.set('slug', slugValue);
|
||||
}
|
||||
|
||||
try {
|
||||
let currentPath,
|
||||
newPath;
|
||||
|
||||
user = yield user.save({format: false});
|
||||
|
||||
// If the user's slug has changed, change the URL and replace
|
||||
// the history so refresh and back button still work
|
||||
if (slugChanged) {
|
||||
currentPath = window.location.hash;
|
||||
|
||||
newPath = currentPath.split('/');
|
||||
newPath[newPath.length - 1] = user.get('slug');
|
||||
newPath = newPath.join('/');
|
||||
|
||||
windowProxy.replaceState({path: newPath}, '', newPath);
|
||||
}
|
||||
|
||||
this.set('dirtyAttributes', false);
|
||||
this.get('notifications').closeAlerts('user.update');
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
// validation engine returns undefined so we have to check
|
||||
// before treating the failure as an API error
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'user.update'});
|
||||
}
|
||||
}
|
||||
}).group('saveHandlers'),
|
||||
|
||||
actions: {
|
||||
changeRole(newRole) {
|
||||
this.get('user').set('role', newRole);
|
||||
@ -460,5 +355,110 @@ export default Controller.extend({
|
||||
this.get('user.hasValidated').removeObject('ne2Password');
|
||||
this.get('user.errors').remove('ne2Password');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_deleteUser() {
|
||||
if (this.get('deleteUserActionIsVisible')) {
|
||||
let user = this.get('user');
|
||||
return user.destroyRecord();
|
||||
}
|
||||
},
|
||||
|
||||
_deleteUserSuccess() {
|
||||
this.get('notifications').closeAlerts('user.delete');
|
||||
this.store.unloadAll('post');
|
||||
this.transitionToRoute('team');
|
||||
},
|
||||
|
||||
_deleteUserFailure() {
|
||||
this.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'});
|
||||
},
|
||||
|
||||
updateSlug: task(function* (newSlug) {
|
||||
let slug = this.get('user.slug');
|
||||
|
||||
newSlug = newSlug || slug;
|
||||
newSlug = newSlug.trim();
|
||||
|
||||
// Ignore unchanged slugs or candidate slugs that are empty
|
||||
if (!newSlug || slug === newSlug) {
|
||||
this.set('slugValue', slug);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let serverSlug = yield this.get('slugGenerator').generateSlug('user', newSlug);
|
||||
|
||||
// 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 true;
|
||||
}
|
||||
|
||||
// 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 true;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('slugValue', serverSlug);
|
||||
this.set('dirtyAttributes', true);
|
||||
|
||||
return true;
|
||||
}).group('saveHandlers'),
|
||||
|
||||
save: task(function* () {
|
||||
let user = this.get('user');
|
||||
let slugValue = this.get('slugValue');
|
||||
let slugChanged;
|
||||
|
||||
if (user.get('slug') !== slugValue) {
|
||||
slugChanged = true;
|
||||
user.set('slug', slugValue);
|
||||
}
|
||||
|
||||
try {
|
||||
let currentPath,
|
||||
newPath;
|
||||
|
||||
user = yield user.save({format: false});
|
||||
|
||||
// If the user's slug has changed, change the URL and replace
|
||||
// the history so refresh and back button still work
|
||||
if (slugChanged) {
|
||||
currentPath = window.location.hash;
|
||||
|
||||
newPath = currentPath.split('/');
|
||||
newPath[newPath.length - 1] = user.get('slug');
|
||||
newPath = newPath.join('/');
|
||||
|
||||
windowProxy.replaceState({path: newPath}, '', newPath);
|
||||
}
|
||||
|
||||
this.set('dirtyAttributes', false);
|
||||
this.get('notifications').closeAlerts('user.update');
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
// validation engine returns undefined so we have to check
|
||||
// before treating the failure as an API error
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'user.update'});
|
||||
}
|
||||
}
|
||||
}).group('saveHandlers')
|
||||
});
|
||||
|
@ -3,13 +3,13 @@ import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, {
|
||||
ghostPaths: service(),
|
||||
ajax: service(),
|
||||
|
||||
titleToken: 'About',
|
||||
|
||||
classNames: ['view-about'],
|
||||
|
||||
ghostPaths: service(),
|
||||
ajax: service(),
|
||||
|
||||
cachedConfig: false,
|
||||
|
||||
model() {
|
||||
|
@ -30,10 +30,6 @@ shortcuts.esc = {action: 'closeMenus', scope: 'default'};
|
||||
shortcuts[`${ctrlOrCmd}+s`] = {action: 'save', scope: 'all'};
|
||||
|
||||
export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||
shortcuts,
|
||||
|
||||
routeAfterAuthentication: 'posts',
|
||||
|
||||
config: service(),
|
||||
feature: service(),
|
||||
notifications: service(),
|
||||
@ -41,6 +37,10 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||
tour: service(),
|
||||
ui: service(),
|
||||
|
||||
shortcuts,
|
||||
|
||||
routeAfterAuthentication: 'posts',
|
||||
|
||||
beforeModel() {
|
||||
return this.get('config').fetch();
|
||||
},
|
||||
@ -86,36 +86,6 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||
}
|
||||
},
|
||||
|
||||
title(tokens) {
|
||||
return `${tokens.join(' - ')} - ${this.get('config.blogTitle')}`;
|
||||
},
|
||||
|
||||
sessionAuthenticated() {
|
||||
if (this.get('session.skipAuthSuccessHandler')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// standard ESA post-sign-in redirect
|
||||
this._super(...arguments);
|
||||
|
||||
// trigger post-sign-in background behaviour
|
||||
this.get('session.user').then((user) => {
|
||||
this.send('signedIn', user);
|
||||
});
|
||||
},
|
||||
|
||||
sessionInvalidated() {
|
||||
let transition = this.get('appLoadTransition');
|
||||
|
||||
if (transition) {
|
||||
transition.send('authorizationFailed');
|
||||
} else {
|
||||
run.scheduleOnce('routerTransitions', this, function () {
|
||||
this.send('authorizationFailed');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
closeMenus() {
|
||||
this.get('ui').closeMenus();
|
||||
@ -233,5 +203,35 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||
// fallback to 500 error page
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
title(tokens) {
|
||||
return `${tokens.join(' - ')} - ${this.get('config.blogTitle')}`;
|
||||
},
|
||||
|
||||
sessionAuthenticated() {
|
||||
if (this.get('session.skipAuthSuccessHandler')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// standard ESA post-sign-in redirect
|
||||
this._super(...arguments);
|
||||
|
||||
// trigger post-sign-in background behaviour
|
||||
this.get('session.user').then((user) => {
|
||||
this.send('signedIn', user);
|
||||
});
|
||||
},
|
||||
|
||||
sessionInvalidated() {
|
||||
let transition = this.get('appLoadTransition');
|
||||
|
||||
if (transition) {
|
||||
transition.send('authorizationFailed');
|
||||
} else {
|
||||
run.scheduleOnce('routerTransitions', this, function () {
|
||||
this.send('authorizationFailed');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -5,11 +5,6 @@ import {assign} from '@ember/polyfills';
|
||||
import {isBlank} from '@ember/utils';
|
||||
|
||||
export default AuthenticatedRoute.extend(InfinityRoute, {
|
||||
titleToken: 'Content',
|
||||
|
||||
perPage: 30,
|
||||
perPageParam: 'limit',
|
||||
totalPagesParam: 'meta.pagination.pages',
|
||||
|
||||
queryParams: {
|
||||
type: {
|
||||
@ -30,6 +25,12 @@ export default AuthenticatedRoute.extend(InfinityRoute, {
|
||||
}
|
||||
},
|
||||
|
||||
titleToken: 'Content',
|
||||
|
||||
perPage: 30,
|
||||
perPageParam: 'limit',
|
||||
totalPagesParam: 'meta.pagination.pages',
|
||||
|
||||
_type: null,
|
||||
|
||||
model(params) {
|
||||
@ -66,6 +67,40 @@ export default AuthenticatedRoute.extend(InfinityRoute, {
|
||||
});
|
||||
},
|
||||
|
||||
// trigger a background load of all tags and authors for use in the filter dropdowns
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
|
||||
if (!controller._hasLoadedTags) {
|
||||
this.get('store').query('tag', {limit: 'all'}).then(() => {
|
||||
controller._hasLoadedTags = true;
|
||||
});
|
||||
}
|
||||
|
||||
this.get('session.user').then((user) => {
|
||||
if (!user.get('isAuthor') && !controller._hasLoadedAuthors) {
|
||||
this.get('store').query('user', {limit: 'all'}).then(() => {
|
||||
controller._hasLoadedAuthors = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
if (this.get('controller')) {
|
||||
this.resetController();
|
||||
}
|
||||
},
|
||||
|
||||
queryParamsDidChange() {
|
||||
// scroll back to the top
|
||||
$('.content-list').scrollTop(0);
|
||||
|
||||
this._super(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
_typeParams(type) {
|
||||
let status = 'all';
|
||||
let staticPages = 'all';
|
||||
@ -102,39 +137,5 @@ export default AuthenticatedRoute.extend(InfinityRoute, {
|
||||
return `${key}:${filter[key]}`;
|
||||
}
|
||||
}).compact().join('+');
|
||||
},
|
||||
|
||||
// trigger a background load of all tags and authors for use in the filter dropdowns
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
|
||||
if (!controller._hasLoadedTags) {
|
||||
this.get('store').query('tag', {limit: 'all'}).then(() => {
|
||||
controller._hasLoadedTags = true;
|
||||
});
|
||||
}
|
||||
|
||||
this.get('session.user').then((user) => {
|
||||
if (!user.get('isAuthor') && !controller._hasLoadedAuthors) {
|
||||
this.get('store').query('user', {limit: 'all'}).then(() => {
|
||||
controller._hasLoadedAuthors = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
if (this.get('controller')) {
|
||||
this.resetController();
|
||||
}
|
||||
},
|
||||
|
||||
queryParamsDidChange() {
|
||||
// scroll back to the top
|
||||
$('.content-list').scrollTop(0);
|
||||
|
||||
this._super(...arguments);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4,11 +4,11 @@ import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Route.extend(styleBody, UnauthenticatedRouteMixin, {
|
||||
classNames: ['ghost-reset'],
|
||||
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
|
||||
classNames: ['ghost-reset'],
|
||||
|
||||
beforeModel() {
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
this.get('notifications').showAlert('You can\'t reset your password while you\'re signed in.', {type: 'warn', delayed: true, key: 'password.reset.signed-in'});
|
||||
|
@ -3,12 +3,12 @@ import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, {
|
||||
settings: service(),
|
||||
|
||||
titleToken: 'Settings - Apps - Slack',
|
||||
|
||||
classNames: ['settings-view-apps-slack'],
|
||||
|
||||
settings: service(),
|
||||
|
||||
afterModel() {
|
||||
return this.get('settings').reload();
|
||||
},
|
||||
|
@ -4,11 +4,11 @@ import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
titleToken: 'Settings - Code injection',
|
||||
classNames: ['settings-view-code'],
|
||||
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
return this.get('session.user')
|
||||
|
@ -5,9 +5,6 @@ import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
|
||||
titleToken: 'Settings - Tags',
|
||||
|
||||
shortcuts: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -20,6 +17,10 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
|
||||
};
|
||||
},
|
||||
|
||||
titleToken: 'Settings - Tags',
|
||||
|
||||
shortcuts: null,
|
||||
|
||||
// authors aren't allowed to manage tags
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
@ -47,31 +48,6 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
|
||||
this.send('resetShortcutsScope');
|
||||
},
|
||||
|
||||
stepThroughTags(step) {
|
||||
let currentTag = this.modelFor('settings.tags.tag');
|
||||
let tags = this.get('controller.sortedTags');
|
||||
let length = tags.get('length');
|
||||
|
||||
if (currentTag && length) {
|
||||
let newPosition = tags.indexOf(currentTag) + step;
|
||||
|
||||
if (newPosition >= length) {
|
||||
return;
|
||||
} else if (newPosition < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transitionTo('settings.tags.tag', tags.objectAt(newPosition));
|
||||
}
|
||||
},
|
||||
|
||||
scrollContent(amount) {
|
||||
let content = $('.tag-settings-pane');
|
||||
let scrolled = content.scrollTop();
|
||||
|
||||
content.scrollTop(scrolled + 50 * amount);
|
||||
},
|
||||
|
||||
actions: {
|
||||
moveUp() {
|
||||
if (this.controller.get('tagContentFocused')) {
|
||||
@ -104,5 +80,30 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
|
||||
resetShortcutsScope() {
|
||||
key.setScope('default');
|
||||
}
|
||||
},
|
||||
|
||||
stepThroughTags(step) {
|
||||
let currentTag = this.modelFor('settings.tags.tag');
|
||||
let tags = this.get('controller.sortedTags');
|
||||
let length = tags.get('length');
|
||||
|
||||
if (currentTag && length) {
|
||||
let newPosition = tags.indexOf(currentTag) + step;
|
||||
|
||||
if (newPosition >= length) {
|
||||
return;
|
||||
} else if (newPosition < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transitionTo('settings.tags.tag', tags.objectAt(newPosition));
|
||||
}
|
||||
},
|
||||
|
||||
scrollContent(amount) {
|
||||
let content = $('.tag-settings-pane');
|
||||
let scrolled = content.scrollTop();
|
||||
|
||||
content.scrollTop(scrolled + 50 * amount);
|
||||
}
|
||||
});
|
||||
|
@ -3,15 +3,15 @@ import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Route.extend(styleBody, {
|
||||
titleToken: 'Setup',
|
||||
|
||||
classNames: ['ghost-setup'],
|
||||
|
||||
ghostPaths: service(),
|
||||
session: service(),
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
|
||||
titleToken: 'Setup',
|
||||
|
||||
classNames: ['ghost-setup'],
|
||||
|
||||
// use the beforeModel hook to check to see whether or not setup has been
|
||||
// previously completed. If it has, stop the transition into the setup page.
|
||||
beforeModel() {
|
||||
|
@ -7,12 +7,12 @@ import {inject as service} from '@ember/service';
|
||||
const {canInvoke} = Ember;
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, {
|
||||
notifications: service(),
|
||||
|
||||
titleToken: 'Sign Out',
|
||||
|
||||
classNames: ['ghost-signout'],
|
||||
|
||||
notifications: service(),
|
||||
|
||||
afterModel(model, transition) {
|
||||
this.get('notifications').clearAll();
|
||||
if (canInvoke(transition, 'send')) {
|
||||
|
@ -10,14 +10,14 @@ const {Promise} = RSVP;
|
||||
const {Errors} = DS;
|
||||
|
||||
export default Route.extend(styleBody, UnauthenticatedRouteMixin, {
|
||||
classNames: ['ghost-signup'],
|
||||
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
|
||||
classNames: ['ghost-signup'],
|
||||
|
||||
beforeModel() {
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
this.get('notifications').showAlert('You need to sign out to register as a new user.', {type: 'warn', delayed: true, key: 'signup.create.already-authenticated'});
|
||||
|
@ -3,10 +3,10 @@ import RSVP from 'rsvp';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend({
|
||||
titleToken: 'Subscribers',
|
||||
|
||||
feature: service(),
|
||||
|
||||
titleToken: 'Subscribers',
|
||||
|
||||
// redirect if subscribers is disabled or user isn't owner/admin
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
|
@ -19,11 +19,6 @@ export default Route.extend({
|
||||
}
|
||||
},
|
||||
|
||||
rollbackModel() {
|
||||
let subscriber = this.controller.get('subscriber');
|
||||
subscriber.rollbackAttributes();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
let subscriber = this.controller.get('subscriber');
|
||||
@ -37,5 +32,10 @@ export default Route.extend({
|
||||
this.rollbackModel();
|
||||
this.transitionTo('subscribers');
|
||||
}
|
||||
},
|
||||
|
||||
rollbackModel() {
|
||||
let subscriber = this.controller.get('subscriber');
|
||||
subscriber.rollbackAttributes();
|
||||
}
|
||||
});
|
||||
|
@ -12,10 +12,6 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
return this.store.queryRecord('user', {slug: params.user_slug, include: 'count.posts'});
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
return {user_slug: model.get('slug')};
|
||||
},
|
||||
|
||||
afterModel(user) {
|
||||
this._super(...arguments);
|
||||
|
||||
@ -32,6 +28,10 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
});
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
return {user_slug: model.get('slug')};
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.modelFor('team.user').get('errors').clear();
|
||||
|
@ -9,14 +9,6 @@ export default Component.extend({
|
||||
layout,
|
||||
hasRendered: false,
|
||||
|
||||
// TODO: remove observer
|
||||
// eslint-disable-next-line ghost/ember/no-observers
|
||||
save: observer('doSave', function () {
|
||||
let payload = this.get('payload');
|
||||
payload.wordcount = counter(payload.html);
|
||||
this.get('env').save(payload, false);
|
||||
}),
|
||||
|
||||
value: computed('payload', {
|
||||
get() {
|
||||
return this.get('payload').html || '';
|
||||
@ -28,6 +20,14 @@ export default Component.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
// TODO: remove observer
|
||||
// eslint-disable-next-line ghost/ember/no-observers
|
||||
save: observer('doSave', function () {
|
||||
let payload = this.get('payload');
|
||||
payload.wordcount = counter(payload.html);
|
||||
this.get('env').save(payload, false);
|
||||
}),
|
||||
|
||||
actions: {
|
||||
selectCard() {
|
||||
invokeAction(this, 'selectCard');
|
||||
|
@ -17,6 +17,9 @@ import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
|
||||
layout,
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
@ -36,14 +39,6 @@ export default Component.extend({
|
||||
url: null,
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.extensions = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
||||
},
|
||||
|
||||
// TODO: this wouldn't be necessary if the server could accept direct
|
||||
// file uploads
|
||||
formData: computed('file', function () {
|
||||
@ -73,6 +68,11 @@ export default Component.extend({
|
||||
return htmlSafe(`width: ${width}`);
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.extensions = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
let image = this.get('payload');
|
||||
if (image.img) {
|
||||
@ -83,6 +83,40 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
|
||||
// jscs:enable requireArrayDestructuring
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
}
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
@ -227,39 +261,5 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
|
||||
// jscs:enable requireArrayDestructuring
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -18,10 +18,11 @@ import {inject as service} from '@ember/service';
|
||||
/* legacyConverter.makeHtml(_.toString(this.get('markdown'))) */
|
||||
|
||||
export default Component.extend({
|
||||
ajax: service(),
|
||||
|
||||
layout,
|
||||
accept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
|
||||
extensions: null,
|
||||
ajax: service(),
|
||||
|
||||
preview: computed('value', function () {
|
||||
return formatMarkdown([this.get('payload').markdown]);
|
||||
@ -54,6 +55,77 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
|
||||
// jscs:enable requireArrayDestructuring
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
},
|
||||
|
||||
selectCard() {
|
||||
invokeAction(this, 'selectCard');
|
||||
},
|
||||
|
||||
didDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let el = this.$('textarea')[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
|
||||
|
||||
let start = el.selectionStart;
|
||||
|
||||
let end = el.selectionEnd;
|
||||
|
||||
let {files} = event.dataTransfer;
|
||||
let combinedLength = 0;
|
||||
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = files[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
|
||||
let placeholderText = `\r\n![uploading:${file.name}]()\r\n`;
|
||||
el.value = el.value.substring(0, start) + placeholderText + el.value.substring(end, el.value.length);
|
||||
combinedLength += placeholderText.length;
|
||||
|
||||
el.selectionStart = start;
|
||||
el.selectionEnd = end + combinedLength;
|
||||
|
||||
this.send('fileSelected', event.dataTransfer.files);
|
||||
},
|
||||
|
||||
didDragOver() {
|
||||
this.$('textarea').addClass('dragOver');
|
||||
},
|
||||
|
||||
didDragLeave() {
|
||||
this.$('textarea').removeClass('dragOver');
|
||||
}
|
||||
},
|
||||
|
||||
_uploadStarted() {
|
||||
invokeAction(this, 'uploadStarted');
|
||||
},
|
||||
@ -180,77 +252,5 @@ export default Component.extend({
|
||||
}).finally(() => {
|
||||
this._uploadFinished();
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
fileSelected(fileList) {
|
||||
// can't use array destructuring here as FileList is not a strict
|
||||
// array and fails in Safari
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = fileList[0];
|
||||
|
||||
// jscs:enable requireArrayDestructuring
|
||||
let validationResult = this._validate(file);
|
||||
|
||||
this.set('file', file);
|
||||
|
||||
invokeAction(this, 'fileSelected', file);
|
||||
|
||||
if (validationResult === true) {
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
} else {
|
||||
this._uploadFailed(validationResult);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
invokeAction(this, 'update', url);
|
||||
},
|
||||
|
||||
selectCard() {
|
||||
invokeAction(this, 'selectCard');
|
||||
},
|
||||
|
||||
didDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let el = this.$('textarea')[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
|
||||
|
||||
let start = el.selectionStart;
|
||||
|
||||
let end = el.selectionEnd;
|
||||
|
||||
let {files} = event.dataTransfer;
|
||||
let combinedLength = 0;
|
||||
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let file = files[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
|
||||
let placeholderText = `\r\n![uploading:${file.name}]()\r\n`;
|
||||
el.value = el.value.substring(0, start) + placeholderText + el.value.substring(end, el.value.length);
|
||||
combinedLength += placeholderText.length;
|
||||
|
||||
el.selectionStart = start;
|
||||
el.selectionEnd = end + combinedLength;
|
||||
|
||||
this.send('fileSelected', event.dataTransfer.files);
|
||||
},
|
||||
|
||||
didDragOver() {
|
||||
this.$('textarea').addClass('dragOver');
|
||||
},
|
||||
|
||||
didDragLeave() {
|
||||
this.$('textarea').removeClass('dragOver');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -248,84 +248,6 @@ export default Component.extend({
|
||||
this.processWordcount();
|
||||
},
|
||||
|
||||
// makes sure the cursor is on screen except when selection is happening in
|
||||
// which case the browser mostly ensures it. there is an issue with keyboard
|
||||
// selection on some browsers though so the next step may be to record mouse
|
||||
// and touch events.
|
||||
cursorMoved() {
|
||||
let editor = this.get('editor');
|
||||
|
||||
if (editor.range.isCollapsed) {
|
||||
let scrollBuffer = 33; // the extra buffer to scroll.
|
||||
|
||||
let position = getPositionOnScreenFromRange(editor, $(this.get('containerSelector')));
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
let windowHeight = window.innerHeight;
|
||||
|
||||
if (position.bottom > windowHeight) {
|
||||
this._domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
|
||||
} else if (position.top < 0) {
|
||||
this._domContainer.scrollTop += position.top - scrollBuffer;
|
||||
}
|
||||
|
||||
if (editor.range && editor.range.headSection && editor.range.headSection.isCardSection) {
|
||||
let id = $(editor.range.headSection.renderNode.element).find('.kg-card > div').attr('id');
|
||||
// let id = card.find('div').attr('id');
|
||||
window.getSelection().removeAllRanges();
|
||||
// if the element is first and we create a card with the '/' menu then the cursor moves before
|
||||
// element is placed in the dom properly. So we figure it out another way.
|
||||
if (!id) {
|
||||
id = editor.range.headSection.renderNode.element.children[0].children[0].id;
|
||||
}
|
||||
|
||||
this.send('selectCardHard', id);
|
||||
} else {
|
||||
this.send('deselectCard');
|
||||
}
|
||||
} else {
|
||||
this.send('deselectCard');
|
||||
}
|
||||
},
|
||||
|
||||
// NOTE: This wordcount function doesn't count words that have been entered in cards.
|
||||
// We should either allow cards to report their own wordcount or use the DOM
|
||||
// (innerText) to calculate the wordcount.
|
||||
processWordcount() {
|
||||
let wordcount = 0;
|
||||
if (this.editor.post.sections.length) {
|
||||
this.editor.post.sections.forEach((section) => {
|
||||
if (section.isMarkerable && section.text.length) {
|
||||
wordcount += counter(section.text);
|
||||
} else if (section.isCardSection && section.payload.wordcount) {
|
||||
wordcount += Number(section.payload.wordcount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let action = this.get('wordcountDidChange');
|
||||
if (action) {
|
||||
action(wordcount);
|
||||
}
|
||||
},
|
||||
|
||||
_willCreateEditor() {
|
||||
let action = this.get('willCreateEditor');
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
},
|
||||
|
||||
_didCreateEditor(editor) {
|
||||
let action = this.get('didCreateEditor');
|
||||
if (action) {
|
||||
action(editor);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this.editor.destroy();
|
||||
this.send('deselectCard');
|
||||
@ -333,39 +255,6 @@ export default Component.extend({
|
||||
document.onkeydown = null;
|
||||
},
|
||||
|
||||
postDidChange(editor) {
|
||||
// store a cache of the local doc so that we don't need to reinitialise it.
|
||||
let serializeVersion = this.get('serializeVersion');
|
||||
let updatedMobiledoc = editor.serialize(serializeVersion);
|
||||
let onChangeAction = this.get('onChange');
|
||||
let onFirstChangeAction = this.get('onFirstChange');
|
||||
|
||||
this._localMobiledoc = updatedMobiledoc;
|
||||
|
||||
if (onChangeAction) {
|
||||
onChangeAction(updatedMobiledoc);
|
||||
}
|
||||
|
||||
// we need to trigger a first-change action so that we can trigger a
|
||||
// save and transition from new-> edit
|
||||
if (this._localMobiledoc !== BLANK_DOC && !this._hasChanged) {
|
||||
this._hasChanged = true;
|
||||
|
||||
if (onFirstChangeAction) {
|
||||
onFirstChangeAction(this._localMobiledoc);
|
||||
}
|
||||
}
|
||||
|
||||
this.processWordcount();
|
||||
},
|
||||
|
||||
_setExpandoProperty(editor) {
|
||||
// Store a reference to the editor for the acceptance test helpers
|
||||
if (this.element && testing) {
|
||||
this.element[TESTING_EXPANDO_PROPERTY] = editor;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// thin border, shows that a card is selected but the user cannot delete
|
||||
// the card with keyboard events.
|
||||
@ -581,6 +470,116 @@ export default Component.extend({
|
||||
// required for drop events to fire on markdown cards in firefox.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// makes sure the cursor is on screen except when selection is happening in
|
||||
// which case the browser mostly ensures it. there is an issue with keyboard
|
||||
// selection on some browsers though so the next step may be to record mouse
|
||||
// and touch events.
|
||||
cursorMoved() {
|
||||
let editor = this.get('editor');
|
||||
|
||||
if (editor.range.isCollapsed) {
|
||||
let scrollBuffer = 33; // the extra buffer to scroll.
|
||||
|
||||
let position = getPositionOnScreenFromRange(editor, $(this.get('containerSelector')));
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
let windowHeight = window.innerHeight;
|
||||
|
||||
if (position.bottom > windowHeight) {
|
||||
this._domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
|
||||
} else if (position.top < 0) {
|
||||
this._domContainer.scrollTop += position.top - scrollBuffer;
|
||||
}
|
||||
|
||||
if (editor.range && editor.range.headSection && editor.range.headSection.isCardSection) {
|
||||
let id = $(editor.range.headSection.renderNode.element).find('.kg-card > div').attr('id');
|
||||
// let id = card.find('div').attr('id');
|
||||
window.getSelection().removeAllRanges();
|
||||
// if the element is first and we create a card with the '/' menu then the cursor moves before
|
||||
// element is placed in the dom properly. So we figure it out another way.
|
||||
if (!id) {
|
||||
id = editor.range.headSection.renderNode.element.children[0].children[0].id;
|
||||
}
|
||||
|
||||
this.send('selectCardHard', id);
|
||||
} else {
|
||||
this.send('deselectCard');
|
||||
}
|
||||
} else {
|
||||
this.send('deselectCard');
|
||||
}
|
||||
},
|
||||
|
||||
// NOTE: This wordcount function doesn't count words that have been entered in cards.
|
||||
// We should either allow cards to report their own wordcount or use the DOM
|
||||
// (innerText) to calculate the wordcount.
|
||||
processWordcount() {
|
||||
let wordcount = 0;
|
||||
if (this.editor.post.sections.length) {
|
||||
this.editor.post.sections.forEach((section) => {
|
||||
if (section.isMarkerable && section.text.length) {
|
||||
wordcount += counter(section.text);
|
||||
} else if (section.isCardSection && section.payload.wordcount) {
|
||||
wordcount += Number(section.payload.wordcount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let action = this.get('wordcountDidChange');
|
||||
if (action) {
|
||||
action(wordcount);
|
||||
}
|
||||
},
|
||||
|
||||
_willCreateEditor() {
|
||||
let action = this.get('willCreateEditor');
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
},
|
||||
|
||||
_didCreateEditor(editor) {
|
||||
let action = this.get('didCreateEditor');
|
||||
if (action) {
|
||||
action(editor);
|
||||
}
|
||||
},
|
||||
|
||||
postDidChange(editor) {
|
||||
// store a cache of the local doc so that we don't need to reinitialise it.
|
||||
let serializeVersion = this.get('serializeVersion');
|
||||
let updatedMobiledoc = editor.serialize(serializeVersion);
|
||||
let onChangeAction = this.get('onChange');
|
||||
let onFirstChangeAction = this.get('onFirstChange');
|
||||
|
||||
this._localMobiledoc = updatedMobiledoc;
|
||||
|
||||
if (onChangeAction) {
|
||||
onChangeAction(updatedMobiledoc);
|
||||
}
|
||||
|
||||
// we need to trigger a first-change action so that we can trigger a
|
||||
// save and transition from new-> edit
|
||||
if (this._localMobiledoc !== BLANK_DOC && !this._hasChanged) {
|
||||
this._hasChanged = true;
|
||||
|
||||
if (onFirstChangeAction) {
|
||||
onFirstChangeAction(this._localMobiledoc);
|
||||
}
|
||||
}
|
||||
|
||||
this.processWordcount();
|
||||
},
|
||||
|
||||
_setExpandoProperty(editor) {
|
||||
// Store a reference to the editor for the acceptance test helpers
|
||||
if (this.element && testing) {
|
||||
this.element[TESTING_EXPANDO_PROPERTY] = editor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -82,30 +82,6 @@ export default Component.extend({
|
||||
});
|
||||
},
|
||||
|
||||
cursorChange() {
|
||||
let editor = this.get('editor');
|
||||
let range = this.get('range');
|
||||
let isOpen = this.get('isOpen');
|
||||
|
||||
// if the cursor isn't in the editor then close the menu
|
||||
if (!range || !editor.range.isCollapsed || editor.range.head.section !== range.section || this.editor.range.head.offset < 1 || !this.editor.range.head.section) {
|
||||
// unless we click on a tool because the tool will close the menu.
|
||||
if (isOpen && !$(window.getSelection().anchorNode).parents('.gh-cardmenu').length) {
|
||||
this.send('closeMenu');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
let queryString = editor.range.head.section.text.substring(range.startOffset, editor.range.head.offset);
|
||||
this.set('query', queryString);
|
||||
// if we've typed 5 characters and have no tools then close.
|
||||
if (queryString.length > 5 && !this.get('toolLength')) {
|
||||
this.send('closeMenu');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
openMenu() {
|
||||
let holder = $(this.get('containerSelector'));
|
||||
@ -259,5 +235,29 @@ export default Component.extend({
|
||||
editor.deleteRange(editor.range);
|
||||
this.send('closeMenu');
|
||||
}
|
||||
},
|
||||
|
||||
cursorChange() {
|
||||
let editor = this.get('editor');
|
||||
let range = this.get('range');
|
||||
let isOpen = this.get('isOpen');
|
||||
|
||||
// if the cursor isn't in the editor then close the menu
|
||||
if (!range || !editor.range.isCollapsed || editor.range.head.section !== range.section || this.editor.range.head.offset < 1 || !this.editor.range.head.section) {
|
||||
// unless we click on a tool because the tool will close the menu.
|
||||
if (isOpen && !$(window.getSelection().anchorNode).parents('.gh-cardmenu').length) {
|
||||
this.send('closeMenu');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
let queryString = editor.range.head.section.text.substring(range.startOffset, editor.range.head.offset);
|
||||
this.set('query', queryString);
|
||||
// if we've typed 5 characters and have no tools then close.
|
||||
if (queryString.length > 5 && !this.get('toolLength')) {
|
||||
this.send('closeMenu');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -16,54 +16,6 @@ export default Component.extend({
|
||||
editorKeyDownListener: null,
|
||||
_hasSetupEventListeners: false,
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let title = this.$('.kg-title-input');
|
||||
|
||||
// setup mutation observer
|
||||
let mutationObserver = new MutationObserver(() => {
|
||||
// on mutate we update.
|
||||
if (title[0].textContent !== '') {
|
||||
title.removeClass('no-content');
|
||||
} else {
|
||||
title.addClass('no-content');
|
||||
}
|
||||
|
||||
// there is no consistency in how characters like nbsp and zwd are handled across browsers
|
||||
// so we replace every whitespace character with a ' '
|
||||
// note: this means that we can't have tabs in the title.
|
||||
let textContent = title[0].textContent.replace(/\s/g, ' ');
|
||||
let innerHTML = title[0].innerHTML.replace(/( |\s)/g, ' ');
|
||||
|
||||
// sanity check if there is formatting reset it.
|
||||
if (innerHTML && innerHTML !== textContent) {
|
||||
// run in next runloop so that we don't get stuck in infinite loops.
|
||||
run.next(() => {
|
||||
title[0].innerHTML = textContent;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.get('val') !== textContent) {
|
||||
let onChangeAction = this.get('onChange');
|
||||
let updateAction = this.get('update');
|
||||
|
||||
this.set('_cachedVal', textContent);
|
||||
this.set('val', textContent);
|
||||
|
||||
if (onChangeAction) {
|
||||
onChangeAction(textContent);
|
||||
}
|
||||
if (updateAction) {
|
||||
updateAction(textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
|
||||
this.set('_mutationObserver', mutationObserver);
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
if (this.get('editorHasRendered') && !this._hasSetupEventListeners) {
|
||||
let editor = this.get('editor');
|
||||
@ -147,6 +99,54 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let title = this.$('.kg-title-input');
|
||||
|
||||
// setup mutation observer
|
||||
let mutationObserver = new MutationObserver(() => {
|
||||
// on mutate we update.
|
||||
if (title[0].textContent !== '') {
|
||||
title.removeClass('no-content');
|
||||
} else {
|
||||
title.addClass('no-content');
|
||||
}
|
||||
|
||||
// there is no consistency in how characters like nbsp and zwd are handled across browsers
|
||||
// so we replace every whitespace character with a ' '
|
||||
// note: this means that we can't have tabs in the title.
|
||||
let textContent = title[0].textContent.replace(/\s/g, ' ');
|
||||
let innerHTML = title[0].innerHTML.replace(/( |\s)/g, ' ');
|
||||
|
||||
// sanity check if there is formatting reset it.
|
||||
if (innerHTML && innerHTML !== textContent) {
|
||||
// run in next runloop so that we don't get stuck in infinite loops.
|
||||
run.next(() => {
|
||||
title[0].innerHTML = textContent;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.get('val') !== textContent) {
|
||||
let onChangeAction = this.get('onChange');
|
||||
let updateAction = this.get('update');
|
||||
|
||||
this.set('_cachedVal', textContent);
|
||||
this.set('val', textContent);
|
||||
|
||||
if (onChangeAction) {
|
||||
onChangeAction(textContent);
|
||||
}
|
||||
if (updateAction) {
|
||||
updateAction(textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
|
||||
this.set('_mutationObserver', mutationObserver);
|
||||
},
|
||||
|
||||
didRender() {
|
||||
let title = this.$('.kg-title-input');
|
||||
if (!this.get('val')) {
|
||||
|
@ -33,10 +33,6 @@ export default Component.extend({
|
||||
return this.get('tool.label');
|
||||
}),
|
||||
|
||||
click() {
|
||||
this.tool.onClick(this.get('editor'));
|
||||
},
|
||||
|
||||
willRender() {
|
||||
// TODO: "selected" doesn't appear to do anything for toolbar items -
|
||||
// it's only used within card menus
|
||||
@ -46,5 +42,9 @@ export default Component.extend({
|
||||
if (this.tool.visibility) {
|
||||
this.set(this.tool.visibility, true);
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
this.tool.onClick(this.get('editor'));
|
||||
}
|
||||
});
|
||||
|
@ -104,6 +104,59 @@ export default Component.extend({
|
||||
this.editor.destroy();
|
||||
},
|
||||
|
||||
actions: {
|
||||
linkKeyDown(event) {
|
||||
// if escape close link
|
||||
if (event.keyCode === 27) {
|
||||
this.send('closeLink');
|
||||
}
|
||||
},
|
||||
|
||||
linkKeyPress(event) {
|
||||
// if enter run link
|
||||
if (event.keyCode === 13) {
|
||||
let url = event.target.value;
|
||||
if (!cajaSanitizers.url(url)) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
this.send('closeLink');
|
||||
this.set('isVisible', false);
|
||||
this.editor.run((postEditor) => {
|
||||
let markup = postEditor.builder.createMarkup('a', {href: url});
|
||||
postEditor.addMarkupToRange(this.get('linkRange'), markup);
|
||||
});
|
||||
|
||||
this.set('linkRange', null);
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
doLink(range) {
|
||||
// if a link is already selected then we remove the links from within the range.
|
||||
let currentLinks = this.get('activeTags').filter(element => element.tagName === 'a');
|
||||
if (currentLinks.length) {
|
||||
this.get('editor').run((postEditor) => {
|
||||
currentLinks.forEach((link) => {
|
||||
postEditor.removeMarkupFromRange(range, link);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.set('isLink', true);
|
||||
this.set('linkRange', range);
|
||||
run.schedule('afterRender', this,
|
||||
() => {
|
||||
this.$('input').focus();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
closeLink() {
|
||||
this.set('isLink', false);
|
||||
}
|
||||
},
|
||||
|
||||
// update the location of the toolbar and display it if the range is visible.
|
||||
updateToolbarToRange(toolbar, holder, isMouseDown) {
|
||||
// if there is no cursor:
|
||||
@ -191,58 +244,5 @@ export default Component.extend({
|
||||
positions.forEach((position) => {
|
||||
this.set(position, position === tickPosition);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
linkKeyDown(event) {
|
||||
// if escape close link
|
||||
if (event.keyCode === 27) {
|
||||
this.send('closeLink');
|
||||
}
|
||||
},
|
||||
|
||||
linkKeyPress(event) {
|
||||
// if enter run link
|
||||
if (event.keyCode === 13) {
|
||||
let url = event.target.value;
|
||||
if (!cajaSanitizers.url(url)) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
this.send('closeLink');
|
||||
this.set('isVisible', false);
|
||||
this.editor.run((postEditor) => {
|
||||
let markup = postEditor.builder.createMarkup('a', {href: url});
|
||||
postEditor.addMarkupToRange(this.get('linkRange'), markup);
|
||||
});
|
||||
|
||||
this.set('linkRange', null);
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
doLink(range) {
|
||||
// if a link is already selected then we remove the links from within the range.
|
||||
let currentLinks = this.get('activeTags').filter(element => element.tagName === 'a');
|
||||
if (currentLinks.length) {
|
||||
this.get('editor').run((postEditor) => {
|
||||
currentLinks.forEach((link) => {
|
||||
postEditor.removeMarkupFromRange(range, link);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.set('isLink', true);
|
||||
this.set('linkRange', range);
|
||||
run.schedule('afterRender', this,
|
||||
() => {
|
||||
this.$('input').focus();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
closeLink() {
|
||||
this.set('isLink', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -116,9 +116,9 @@ describe.skip('Unit: Component: post-settings-menu', function () {
|
||||
it('should be the metaTitle if one exists', function () {
|
||||
let component = this.subject({
|
||||
post: EmberObject.extend({
|
||||
titleScratch: 'should not be used',
|
||||
metaTitle: 'a meta-title',
|
||||
metaTitleScratch: boundOneWay('metaTitle'),
|
||||
titleScratch: 'should not be used'
|
||||
metaTitleScratch: boundOneWay('metaTitle')
|
||||
}).create()
|
||||
});
|
||||
|
||||
@ -138,9 +138,9 @@ describe.skip('Unit: Component: post-settings-menu', function () {
|
||||
it('should be the metaTitle if both title and metaTitle exist', function () {
|
||||
let component = this.subject({
|
||||
post: EmberObject.extend({
|
||||
titleScratch: 'a title',
|
||||
metaTitle: 'a meta-title',
|
||||
metaTitleScratch: boundOneWay('metaTitle'),
|
||||
titleScratch: 'a title'
|
||||
metaTitleScratch: boundOneWay('metaTitle')
|
||||
}).create()
|
||||
});
|
||||
|
||||
@ -150,9 +150,9 @@ describe.skip('Unit: Component: post-settings-menu', function () {
|
||||
it('should revert to the title if explicit metaTitle is removed', function () {
|
||||
let component = this.subject({
|
||||
post: EmberObject.extend({
|
||||
titleScratch: 'a title',
|
||||
metaTitle: 'a meta-title',
|
||||
metaTitleScratch: boundOneWay('metaTitle'),
|
||||
titleScratch: 'a title'
|
||||
metaTitleScratch: boundOneWay('metaTitle')
|
||||
}).create()
|
||||
});
|
||||
|
||||
@ -199,9 +199,9 @@ describe.skip('Unit: Component: post-settings-menu', function () {
|
||||
it('should be generated from the rendered mobiledoc if not explicitly set', function () {
|
||||
let component = this.subject({
|
||||
post: EmberObject.extend({
|
||||
author: RSVP.resolve(),
|
||||
metaDescription: null,
|
||||
metaDescriptionScratch: boundOneWay('metaDescription'),
|
||||
author: RSVP.resolve(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
Loading…
Reference in New Issue
Block a user