1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00
Kevin Ansfield 2018-01-11 17:43:23 +00:00
parent 0ef503f23d
commit ac16e6504c
98 changed files with 1957 additions and 1965 deletions

View file

@ -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();
}
});

View file

@ -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 = '';

View file

@ -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')
});

View file

@ -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: ''
});

View file

@ -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;
}
}
});

View file

@ -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();
}
})
});

View file

@ -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);

View file

@ -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);
}
});

View file

@ -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');

View file

@ -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);
}
}
});

View file

@ -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({

View file

@ -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];
}
});

View file

@ -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);
}
}
});

View file

@ -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'}`;
}),

View file

@ -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);
}
}
});

View file

@ -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();
}
})
});

View file

@ -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;
}
}
});

View file

@ -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';

View file

@ -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);

View file

@ -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');
});
}
}
});

View file

@ -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 = '';

View file

@ -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')
});

View file

@ -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);
}
}
});

View file

@ -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');
}),

View file

@ -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();
}
}
});

View file

@ -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);
}
}
})
});

View file

@ -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');
}
});

View file

@ -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);
}
});

View file

@ -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();
}
});

View file

@ -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);
});
}
}
});

View file

@ -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();

View file

@ -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');
}
});

View file

@ -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');

View file

@ -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')) {

View file

@ -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;

View file

@ -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 () {

View file

@ -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);
}
});

View file

@ -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();
}
}
});

View file

@ -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');

View file

@ -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`;
}),

View file

@ -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');

View file

@ -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');
}
}
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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')
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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$/, '');

View file

@ -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;

View file

@ -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'));

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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()
});

View file

@ -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;
}
})
});

View file

@ -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);
});
}
});

View file

@ -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;
}
})
});

View file

@ -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);
}
});

View file

@ -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);
}
}
}
});

View file

@ -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();
}
}
});

View file

@ -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');
}
}
});

View file

@ -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);
}
}
});

View file

@ -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();
}
}
})
});

View file

@ -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);
}
}
})
});

View file

@ -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);
}
}
});

View file

@ -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');

View file

@ -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')
});

View file

@ -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() {

View file

@ -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');
});
}
}
});

View file

@ -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);
}
}
});

View file

@ -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'});

View file

@ -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();
},

View file

@ -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')

View file

@ -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);
}
});

View file

@ -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() {

View file

@ -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')) {

View file

@ -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'});

View file

@ -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);

View file

@ -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();
}
});

View file

@ -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();

View file

@ -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');

View file

@ -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);
}
}
});

View file

@ -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');
}
}
});

View file

@ -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;
}
}
});

View file

@ -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');
}
}
}
});

View file

@ -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(/(&nbsp;|\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(/(&nbsp;|\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')) {

View file

@ -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'));
}
});

View file

@ -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);
}
}
});

View file

@ -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);