mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
SimpleMDE editor (#682)
no issue * move "save on first change" behaviour into editor controller * allow TAB events to be specified in keyEvents hash of gh-input * replace mobiledoc-kit/gh-koenig with a SimpleMDE based editor - remove `gh-koenig` in-repo-addon from `package.json` so that test files etc aren't loaded - remove `mobiledoc-kit` dependencies - extends `gh-editor` to handle file drag/drop - adds `gh-uploader` and `gh-progress-bar` components to handle file uploads in a more composable manner - adds `gh-simplemde` component that wraps SimpleMDE
This commit is contained in:
parent
756b6627a9
commit
762c3c4df0
@ -1,13 +1,35 @@
|
||||
import Component from 'ember-component';
|
||||
import run from 'ember-runloop';
|
||||
import {
|
||||
IMAGE_MIME_TYPES,
|
||||
IMAGE_EXTENSIONS
|
||||
} from 'ghost-admin/components/gh-image-uploader';
|
||||
|
||||
const {debounce} = run;
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
headerClass: '',
|
||||
classNameBindings: ['isDraggedOver:-drag-over'],
|
||||
|
||||
// Public attributes
|
||||
navIsClosed: false,
|
||||
|
||||
// Internal attributes
|
||||
droppedFiles: null,
|
||||
headerClass: '',
|
||||
imageExtensions: IMAGE_EXTENSIONS,
|
||||
imageMimeTypes: IMAGE_MIME_TYPES,
|
||||
isDraggedOver: false,
|
||||
uploadedImageUrls: null,
|
||||
|
||||
// Closure actions
|
||||
toggleAutoNav() {},
|
||||
|
||||
// Private
|
||||
_dragCounter: 0,
|
||||
_fullScreenEnabled: false,
|
||||
_navIsClosed: false,
|
||||
_onResizeHandler: null,
|
||||
_viewActionsWidth: 190,
|
||||
|
||||
init() {
|
||||
@ -27,22 +49,18 @@ export default Component.extend({
|
||||
let navIsClosed = this.get('navIsClosed');
|
||||
|
||||
if (navIsClosed !== this._navIsClosed) {
|
||||
this._fullScreenEnabled = navIsClosed;
|
||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||
}
|
||||
|
||||
this._navIsClosed = navIsClosed;
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
window.removeEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
_setHeaderClass() {
|
||||
let $editorInner = this.$('.gh-editor-inner');
|
||||
let $editorTitle = this.$('.gh-editor-title');
|
||||
|
||||
if ($editorInner.length > 0) {
|
||||
let boundingRect = $editorInner[0].getBoundingClientRect();
|
||||
if ($editorTitle.length > 0) {
|
||||
let boundingRect = $editorTitle[0].getBoundingClientRect();
|
||||
let maxRight = window.innerWidth - this._viewActionsWidth;
|
||||
|
||||
if (boundingRect.right >= maxRight) {
|
||||
@ -52,5 +70,89 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
this.set('headerClass', '');
|
||||
},
|
||||
|
||||
// dragOver is needed so that drop works
|
||||
dragOver(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is needed to work around inconsistencies with dropping files
|
||||
// from Chrome's downloads bar
|
||||
let eA = event.dataTransfer.effectAllowed;
|
||||
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
// dragEnter is needed so that the drag class is correctly removed
|
||||
dragEnter(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// the counter technique prevents flickering of the drag class when
|
||||
// dragging across child elements
|
||||
this._dragCounter++;
|
||||
|
||||
this.set('isDraggedOver', true);
|
||||
},
|
||||
|
||||
dragLeave(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this._dragCounter--;
|
||||
if (this._dragCounter === 0) {
|
||||
this.set('isDraggedOver', false);
|
||||
}
|
||||
},
|
||||
|
||||
drop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this._dragCounter = 0;
|
||||
this.set('isDraggedOver', false);
|
||||
|
||||
if (event.dataTransfer.files) {
|
||||
this.set('droppedFiles', event.dataTransfer.files);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
window.removeEventListener('resize', this._onResizeHandler);
|
||||
|
||||
// reset fullscreen mode if it was turned on
|
||||
if (this._fullScreenEnabled) {
|
||||
this.toggleAutoNav();
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleFullScreen() {
|
||||
if (!this._fullScreenWasToggled) {
|
||||
this._fullScreenEnabled = !this.get('isNavOpen');
|
||||
this._fullScreenWasToggled = true;
|
||||
} else {
|
||||
this._fullScreenEnabled = !this._fullScreenEnabled;
|
||||
}
|
||||
this.toggleAutoNav();
|
||||
},
|
||||
|
||||
uploadComplete(uploads) {
|
||||
this.set('uploadedImageUrls', uploads.mapBy('url'));
|
||||
this.set('droppedFiles', null);
|
||||
},
|
||||
|
||||
uploadCancelled() {
|
||||
this.set('droppedFiles', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -14,6 +14,9 @@ import {
|
||||
UnsupportedMediaTypeError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
|
||||
export const IMAGE_MIME_TYPES = 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml';
|
||||
export const IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
@ -37,8 +40,8 @@ export default Component.extend({
|
||||
ajax: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
_defaultAccept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
|
||||
_defaultExtensions: ['gif', 'jpg', 'jpeg', 'png', 'svg'],
|
||||
_defaultAccept: IMAGE_MIME_TYPES,
|
||||
_defaultExtensions: IMAGE_EXTENSIONS,
|
||||
_defaultUploadUrl: '/uploads/',
|
||||
|
||||
// TODO: this wouldn't be necessary if the server could accept direct
|
||||
|
@ -2,5 +2,14 @@ import OneWayInput from 'ember-one-way-controls/components/one-way-input';
|
||||
import TextInputMixin from 'ghost-admin/mixins/text-input';
|
||||
|
||||
export default OneWayInput.extend(TextInputMixin, {
|
||||
classNames: 'gh-input'
|
||||
classNames: 'gh-input',
|
||||
|
||||
// prevent default TAB behaviour if we have a keyEvent for it
|
||||
keyDown(event) {
|
||||
if (event.keyCode === 9 && this.get('keyEvents.9')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
169
ghost/admin/app/components/gh-markdown-editor.js
Normal file
169
ghost/admin/app/components/gh-markdown-editor.js
Normal file
@ -0,0 +1,169 @@
|
||||
import Component from 'ember-component';
|
||||
import computed from 'ember-computed';
|
||||
import {assign} from 'ember-platform';
|
||||
import {copy} from 'ember-metal/utils';
|
||||
import {isEmpty} from 'ember-utils';
|
||||
import run from 'ember-runloop';
|
||||
|
||||
const MOBILEDOC_VERSION = '0.3.1';
|
||||
|
||||
export const BLANK_DOC = {
|
||||
version: MOBILEDOC_VERSION,
|
||||
markups: [],
|
||||
atoms: [],
|
||||
cards: [
|
||||
['card-markdown', {
|
||||
cardName: 'card-markdown',
|
||||
markdown: ''
|
||||
}]
|
||||
],
|
||||
sections: [[10, 0]]
|
||||
};
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
// Public attributes
|
||||
autofocus: false,
|
||||
mobiledoc: null,
|
||||
options: null,
|
||||
placeholder: '',
|
||||
uploadedImageUrls: null,
|
||||
|
||||
// Closure actions
|
||||
onChange() {},
|
||||
onFullScreen() {},
|
||||
showMarkdownHelp() {},
|
||||
|
||||
// Internal attributes
|
||||
markdown: null,
|
||||
|
||||
// Private
|
||||
_editor: null,
|
||||
_isUploading: false,
|
||||
_uploadedImageUrls: null,
|
||||
_statusbar: null,
|
||||
_toolbar: null,
|
||||
|
||||
// Ghost-Specific SimpleMDE toolbar config - allows us to create a bridge
|
||||
// between SimpleMDE buttons and Ember actions
|
||||
simpleMDEOptions: computed('options', function () {
|
||||
let options = this.get('options') || {};
|
||||
let defaultOptions = {
|
||||
toolbar: [
|
||||
'bold', 'italic', 'heading', '|',
|
||||
'quote', 'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', '|',
|
||||
'preview', 'side-by-side',
|
||||
{
|
||||
name: 'fullscreen',
|
||||
action: () => {
|
||||
this.onFullScreen();
|
||||
},
|
||||
className: 'fa fa-arrows-alt no-disable no-mobile',
|
||||
title: 'Toggle Fullscreen (F11)'
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'guide',
|
||||
action: () => {
|
||||
this.showMarkdownHelp();
|
||||
},
|
||||
className: 'fa fa-question-circle',
|
||||
title: 'Markdown Guide'
|
||||
}
|
||||
],
|
||||
status: ['words']
|
||||
};
|
||||
|
||||
return assign(defaultOptions, options);
|
||||
}),
|
||||
|
||||
// extract markdown content from single markdown card
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
let mobiledoc = this.get('mobiledoc') || copy(BLANK_DOC, true);
|
||||
|
||||
let uploadedImageUrls = this.get('uploadedImageUrls');
|
||||
if (!isEmpty(uploadedImageUrls) && uploadedImageUrls !== this._uploadedImageUrls) {
|
||||
this._uploadedImageUrls = uploadedImageUrls;
|
||||
|
||||
// must be done afterRender to avoid double modify of mobiledoc in
|
||||
// a single render
|
||||
run.scheduleOnce('afterRender', this, () => {
|
||||
this._insertImages(uploadedImageUrls);
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ember-suave/prefer-destructuring
|
||||
let markdown = mobiledoc.cards[0][1].markdown;
|
||||
this.set('markdown', markdown);
|
||||
},
|
||||
|
||||
_insertImages(urls) {
|
||||
let cm = this._editor.codemirror;
|
||||
|
||||
// loop through urls and generate image markdown
|
||||
let images = urls.map((url) => {
|
||||
return `![](${url})`;
|
||||
});
|
||||
let text = images.join(' ');
|
||||
|
||||
// focus editor and place cursor at end if not already focused
|
||||
if (!cm.hasFocus()) {
|
||||
this.send('focusEditor');
|
||||
}
|
||||
|
||||
// insert at cursor or replace selection then position cursor at end
|
||||
// of inserted text
|
||||
cm.replaceSelection(text, 'end');
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
// put the toolbar/statusbar elements back so that SimpleMDE doesn't throw
|
||||
// errors when it tries to remove them
|
||||
destroyEditor() {
|
||||
let container = this.$();
|
||||
this._toolbar.appendTo(container);
|
||||
this._statusbar.appendTo(container);
|
||||
this._editor = null;
|
||||
},
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
});
|
23
ghost/admin/app/components/gh-progress-bar.js
Normal file
23
ghost/admin/app/components/gh-progress-bar.js
Normal file
@ -0,0 +1,23 @@
|
||||
import Component from 'ember-component';
|
||||
import {htmlSafe} from 'ember-string';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
// Public attributes
|
||||
percentage: 0,
|
||||
isError: false,
|
||||
|
||||
// Internal attributes
|
||||
progressStyle: '',
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
let percentage = this.get('percentage');
|
||||
let width = (percentage > 0) ? `${percentage}%` : '0';
|
||||
|
||||
this.set('progressStyle', htmlSafe(`width: ${width}`));
|
||||
}
|
||||
|
||||
});
|
103
ghost/admin/app/components/gh-simplemde.js
Normal file
103
ghost/admin/app/components/gh-simplemde.js
Normal file
@ -0,0 +1,103 @@
|
||||
/* global SimpleMDE */
|
||||
import Ember from 'ember';
|
||||
import TextArea from 'ember-components/text-area';
|
||||
import computed from 'ember-computed';
|
||||
import {assign} from 'ember-platform';
|
||||
import {isEmpty} from 'ember-utils';
|
||||
|
||||
// ember-cli-shims doesn't export Ember.testing
|
||||
const {testing} = Ember;
|
||||
|
||||
export default TextArea.extend({
|
||||
|
||||
// Public attributes
|
||||
autofocus: false,
|
||||
options: null,
|
||||
value: null,
|
||||
placeholder: '',
|
||||
|
||||
// Closure actions
|
||||
onChange() {},
|
||||
onEditorInit() {},
|
||||
onEditorDestroy() {},
|
||||
|
||||
// Private
|
||||
_editor: null,
|
||||
|
||||
// default SimpleMDE options, see docs for available config:
|
||||
// https://github.com/NextStepWebs/simplemde-markdown-editor#configuration
|
||||
defaultOptions: computed(function () {
|
||||
return {
|
||||
autofocus: this.get('autofocus'),
|
||||
indentWithTabs: false,
|
||||
placeholder: this.get('placeholder'),
|
||||
shortcuts: {
|
||||
toggleSideBySide: null,
|
||||
toggleFullScreen: null
|
||||
},
|
||||
tabSize: 4
|
||||
};
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (isEmpty(this.get('options'))) {
|
||||
this.set('options', {});
|
||||
}
|
||||
},
|
||||
|
||||
// instantiate the editor with the contents of value
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let editorOptions = assign(
|
||||
{element: document.getElementById(this.elementId)},
|
||||
this.get('defaultOptions'),
|
||||
this.get('options')
|
||||
);
|
||||
|
||||
// disable spellchecker when testing so that the exterally loaded plugin
|
||||
// doesn't fail
|
||||
if (testing) {
|
||||
editorOptions.spellChecker = false;
|
||||
}
|
||||
|
||||
this._editor = new SimpleMDE(editorOptions);
|
||||
this._editor.value(this.get('value') || '');
|
||||
|
||||
this._editor.codemirror.on('change', () => {
|
||||
this.onChange(this._editor.value());
|
||||
});
|
||||
|
||||
if (this.get('autofocus')) {
|
||||
this._editor.codemirror.execCommand('goDocEnd');
|
||||
}
|
||||
|
||||
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();
|
||||
delete this._editor;
|
||||
this._super(...arguments);
|
||||
}
|
||||
});
|
254
ghost/admin/app/components/gh-uploader.js
Normal file
254
ghost/admin/app/components/gh-uploader.js
Normal file
@ -0,0 +1,254 @@
|
||||
import Component from 'ember-component';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isEmpty} from 'ember-utils';
|
||||
import {task, all} from 'ember-concurrency';
|
||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import EmberObject from 'ember-object';
|
||||
|
||||
// TODO: this is designed to be a more re-usable/composable upload component, it
|
||||
// should be able to replace the duplicated upload logic in:
|
||||
// - gh-image-uploader
|
||||
// - gh-file-uploader
|
||||
// - gh-koenig/cards/card-image
|
||||
// - gh-koenig/cards/card-markdown
|
||||
//
|
||||
// In order to support the above components we'll need to introduce an
|
||||
// "allowMultiple" attribute so that single-image uploads don't allow multiple
|
||||
// simultaneous uploads
|
||||
|
||||
const UploadTracker = EmberObject.extend({
|
||||
file: null,
|
||||
total: 0,
|
||||
loaded: 0,
|
||||
|
||||
init() {
|
||||
this.total = this.file && this.file.size || 0;
|
||||
},
|
||||
|
||||
update({loaded, total}) {
|
||||
this.total = total;
|
||||
this.loaded = loaded;
|
||||
}
|
||||
});
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
ajax: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
// Public attributes
|
||||
accept: '',
|
||||
extensions: null,
|
||||
files: null,
|
||||
paramName: 'uploadimage', // TODO: is this the best default?
|
||||
uploadUrl: null,
|
||||
|
||||
// Interal attributes
|
||||
errors: null, // [{fileName: 'x', message: 'y'}, ...]
|
||||
totalSize: 0,
|
||||
uploadedSize: 0,
|
||||
uploadPercentage: 0,
|
||||
uploadUrls: null, // [{filename: 'x', url: 'y'}],
|
||||
|
||||
// Private
|
||||
_defaultUploadUrl: '/uploads/',
|
||||
_files: null,
|
||||
_isUploading: false,
|
||||
_uploadTrackers: null,
|
||||
|
||||
// Closure actions
|
||||
onCancel() {},
|
||||
onComplete() {},
|
||||
onFailed() {},
|
||||
onStart() {},
|
||||
onUploadFail() {},
|
||||
onUploadSuccess() {},
|
||||
|
||||
// Optional closure actions
|
||||
// validate(file) {}
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('errors', []);
|
||||
this.set('uploadUrls', []);
|
||||
this._uploadTrackers = [];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
// set up any defaults
|
||||
if (!this.get('uploadUrl')) {
|
||||
this.set('uploadUrl', this._defaultUploadUrl);
|
||||
}
|
||||
|
||||
// if we have new files, validate and start an upload
|
||||
let files = this.get('files');
|
||||
if (files && files !== this._files) {
|
||||
if (this._isUploading) {
|
||||
throw new Error('Adding new files whilst an upload is in progress is not supported.');
|
||||
}
|
||||
|
||||
this._files = files;
|
||||
|
||||
// we cancel early if any file fails client-side validation
|
||||
if (this._validate()) {
|
||||
this.get('_uploadFiles').perform(files);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_validate() {
|
||||
let files = this.get('files');
|
||||
let validate = this.get('validate') || this._defaultValidator.bind(this);
|
||||
let ok = [];
|
||||
let errors = [];
|
||||
|
||||
for (let file of files) {
|
||||
let result = validate(file);
|
||||
if (result === true) {
|
||||
ok.push(file);
|
||||
} else {
|
||||
errors.push({fileName: file.name, message: result});
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty(errors)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.set('errors', errors);
|
||||
this.onFailed(errors);
|
||||
return false;
|
||||
},
|
||||
|
||||
// we only check the file extension by default because IE doesn't always
|
||||
// expose the mime-type, we'll rely on the API for final validation
|
||||
_defaultValidator(file) {
|
||||
let extensions = this.get('extensions');
|
||||
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
|
||||
if (!isEmberArray(extensions)) {
|
||||
extensions = extensions.split(',');
|
||||
}
|
||||
|
||||
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
|
||||
let validExtensions = `.${extensions.join(', .').toUpperCase()}`;
|
||||
return `The image type you uploaded is not supported. Please use ${validExtensions}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_uploadFiles: task(function* (files) {
|
||||
let uploads = [];
|
||||
|
||||
this.onStart();
|
||||
|
||||
for (let file of files) {
|
||||
uploads.push(this.get('_uploadFile').perform(file));
|
||||
}
|
||||
|
||||
// populates this.errors and this.uploadUrls
|
||||
yield all(uploads);
|
||||
|
||||
this.onComplete(this.get('uploadUrls'));
|
||||
}).drop(),
|
||||
|
||||
_uploadFile: task(function* (file) {
|
||||
let ajax = this.get('ajax');
|
||||
let formData = this._getFormData(file);
|
||||
let url = `${ghostPaths().apiRoot}${this.get('uploadUrl')}`;
|
||||
|
||||
let tracker = new UploadTracker({file});
|
||||
this.get('_uploadTrackers').pushObject(tracker);
|
||||
|
||||
try {
|
||||
let response = yield ajax.post(url, {
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'text',
|
||||
xhr: () => {
|
||||
let xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
tracker.update(event);
|
||||
this._updateProgress();
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: is it safe to assume we'll only get a url back?
|
||||
let uploadUrl = JSON.parse(response);
|
||||
|
||||
this.get('uploadUrls').push({
|
||||
fileName: file.name,
|
||||
url: uploadUrl
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.log('error', error); // eslint-disable-line
|
||||
|
||||
// TODO: check for or expose known error types?
|
||||
this.get('errors').push({
|
||||
fileName: file.name,
|
||||
message: error.errors[0].message
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
// NOTE: this is necessary because the API doesn't accept direct file uploads
|
||||
_getFormData(file) {
|
||||
let formData = new FormData();
|
||||
formData.append(this.get('paramName'), file);
|
||||
return formData;
|
||||
},
|
||||
|
||||
// TODO: this was needed because using CPs directly resulted in infrequent updates
|
||||
// - I think this was because updates were being wrapped up to save
|
||||
// computation but that hypothesis needs testing
|
||||
_updateProgress() {
|
||||
let trackers = this._uploadTrackers;
|
||||
|
||||
let totalSize = trackers.reduce((total, tracker) => {
|
||||
return total + tracker.get('total');
|
||||
}, 0);
|
||||
|
||||
let uploadedSize = trackers.reduce((total, tracker) => {
|
||||
return total + tracker.get('loaded');
|
||||
}, 0);
|
||||
|
||||
this.set('totalSize', totalSize);
|
||||
this.set('uploadedSize', uploadedSize);
|
||||
|
||||
if (totalSize === 0 || uploadedSize === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let uploadPercentage = Math.round((uploadedSize / totalSize) * 100);
|
||||
this.set('uploadPercentage', uploadPercentage);
|
||||
},
|
||||
|
||||
_reset() {
|
||||
this.set('errors', null);
|
||||
this.set('totalSize', 0);
|
||||
this.set('uploadedSize', 0);
|
||||
this.set('uploadPercentage', 0);
|
||||
this._uploadTrackers = [];
|
||||
this._isUploading = false;
|
||||
},
|
||||
|
||||
actions: {
|
||||
cancel() {
|
||||
this._reset();
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
});
|
@ -56,6 +56,8 @@ export default Mixin.create({
|
||||
|
||||
navIsClosed: reads('application.autoNav'),
|
||||
|
||||
_hasChanged: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
window.onbeforeunload = () => {
|
||||
@ -466,6 +468,16 @@ export default Mixin.create({
|
||||
actions: {
|
||||
updateScratch(value) {
|
||||
this.set('model.scratch', value);
|
||||
|
||||
// save on first change to trigger the new->edit transition
|
||||
if (!this._hasChanged && this.get('model.isNew')) {
|
||||
this._hasChanged = true;
|
||||
this.send('save', {silent: true, backgroundSave: true});
|
||||
return;
|
||||
}
|
||||
|
||||
this._hasChanged = true;
|
||||
|
||||
// save 3 seconds after last edit
|
||||
this.get('_autosave').perform();
|
||||
// force save at 60 seconds
|
||||
@ -494,12 +506,6 @@ export default Mixin.create({
|
||||
}
|
||||
},
|
||||
|
||||
autoSaveNew() {
|
||||
if (this.get('model.isNew')) {
|
||||
this.send('save', {silent: true, backgroundSave: true});
|
||||
}
|
||||
},
|
||||
|
||||
closeNavMenu() {
|
||||
this.get('application').send('closeAutoNav');
|
||||
},
|
||||
@ -573,6 +579,10 @@ export default Mixin.create({
|
||||
|
||||
setWordcount(wordcount) {
|
||||
this.set('wordcount', wordcount);
|
||||
},
|
||||
|
||||
toggleAutoNav() {
|
||||
this.get('application').send('toggleAutoNav');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -132,6 +132,9 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
|
||||
controller.set('previousTagNames', []);
|
||||
}
|
||||
|
||||
// reset save-on-first-change
|
||||
controller._hasChanged = false;
|
||||
|
||||
// attach model-related listeners created in editor-base-route
|
||||
this.attachModelHooks(controller, model);
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import {isBlank} from 'ember-utils';
|
||||
|
||||
import {BLANK_DOC} from 'ghost-admin/components/gh-koenig'; // a blank mobile doc
|
||||
// a blank mobile doc containing a single markdown card
|
||||
import {BLANK_DOC} from 'ghost-admin/components/gh-markdown-editor';
|
||||
|
||||
// ember-cli-shims doesn't export these so we must get them manually
|
||||
const {Comparable, compare} = Ember;
|
||||
|
@ -53,7 +53,7 @@
|
||||
|
||||
/* Addons: gh-koenig
|
||||
/* ---------------------------------------------------------- */
|
||||
@import "addons/gh-koenig/gh-koenig.css";
|
||||
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||
|
||||
|
||||
:root {
|
||||
|
@ -53,7 +53,7 @@
|
||||
|
||||
/* Addons: gh-koenig
|
||||
/* ---------------------------------------------------------- */
|
||||
@import "addons/gh-koenig/gh-koenig.css";
|
||||
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||
|
||||
|
||||
/* ---------------------------✈️----------------------------- */
|
||||
|
@ -89,7 +89,14 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gh-image-uploader .progress-container {
|
||||
.gh-image-uploader .failed {
|
||||
margin: 1em 2em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* TODO: remove the gh-image-uploader classes once it's using gh-progrss-bar */
|
||||
.gh-image-uploader .progress-container,
|
||||
.gh-progress-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -97,7 +104,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-image-uploader .progress {
|
||||
.gh-image-uploader .progress,
|
||||
.gh-progress-container-progress {
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
@ -106,17 +114,14 @@
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
||||
}
|
||||
|
||||
.gh-image-uploader .failed {
|
||||
margin: 1em 2em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.gh-image-uploader .bar {
|
||||
.gh-image-uploader .bar,
|
||||
.gh-progress-bar {
|
||||
height: 12px;
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.gh-image-uploader .bar.fail {
|
||||
.gh-image-uploader .bar.fail,
|
||||
.gh-progress-bar.-error {
|
||||
width: 100% !important;
|
||||
background: var(--red);
|
||||
}
|
||||
|
@ -156,25 +156,6 @@
|
||||
.gh-editor-header { padding: 0 4vw; }
|
||||
}
|
||||
|
||||
.gh-editor-status {
|
||||
color: var(--midgrey);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gh-editor-container {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
padding: 10vw 4vw;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gh-editor-inner {
|
||||
margin: 0 auto;
|
||||
max-width: 740px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gh-editor-header-small {
|
||||
height: 43px;
|
||||
padding: 0;
|
||||
@ -192,10 +173,42 @@
|
||||
padding: 13px 15px;
|
||||
}
|
||||
|
||||
.gh-editor-status {
|
||||
color: var(--midgrey);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gh-editor-container {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
padding: 10vw 4vw;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gh-editor-inner {
|
||||
margin: 0 auto;
|
||||
max-width: 760px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gh-editor-title {
|
||||
width: 100%;
|
||||
min-height: 1.3em;
|
||||
margin-bottom: 2vw;
|
||||
border: none;
|
||||
letter-spacing: 0.8px;
|
||||
font-weight: bold;
|
||||
font-size: 3.2rem;
|
||||
line-height: 1.3em;
|
||||
|
||||
}
|
||||
|
||||
.gh-editor-wordcount {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
padding:10px;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@ -203,3 +216,104 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* SimpleMDE editor
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.gh-editor-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gh-editor-footer {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--lightgrey);
|
||||
}
|
||||
|
||||
.gh-editor-footer .editor-toolbar {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gh-editor-footer .editor-toolbar:before,
|
||||
.gh-editor-footer .editor-toolbar:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.gh-editor-footer .editor-toolbar a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.gh-editor .CodeMirror {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gh-editor .gh-editor-title,
|
||||
.gh-editor .CodeMirror-wrap {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.gh-editor .CodeMirror pre {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-editor .editor-preview {
|
||||
height: auto;
|
||||
margin-top: 4px;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gh-editor-drop-target,
|
||||
.gh-editor-image-upload {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid blue;
|
||||
content: '';
|
||||
z-index: 9999;
|
||||
background-color: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
.gh-editor-drop-target .drop-target-message {
|
||||
padding: 1em;
|
||||
background-color: #fff;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.gh-editor-image-upload.-error {
|
||||
border: 2px solid red;
|
||||
}
|
||||
|
||||
.gh-editor-image-upload-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
background-color: #fff;
|
||||
border-radius: 1em;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.gh-editor-image-upload .gh-progress-container-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* TODO: need a way to make stroke + fill global without causing issues with certain icons */
|
||||
.gh-editor-image-upload .gh-btn-grey svg path {
|
||||
stroke: color(var(--darkgrey) l(+15%));
|
||||
}
|
||||
|
@ -1 +1,11 @@
|
||||
{{yield headerClass}}
|
||||
{{yield (hash
|
||||
headerClass=headerClass
|
||||
isDraggedOver=isDraggedOver
|
||||
droppedFiles=droppedFiles
|
||||
uploadedImageUrls=uploadedImageUrls
|
||||
imageMimeTypes=imageMimeTypes
|
||||
imageExtensions=imageExtensions
|
||||
toggleFullScreen=(action "toggleFullScreen")
|
||||
uploadComplete=(action "uploadComplete")
|
||||
uploadCancelled=(action "uploadCancelled")
|
||||
)}}
|
||||
|
11
ghost/admin/app/templates/components/gh-markdown-editor.hbs
Normal file
11
ghost/admin/app/templates/components/gh-markdown-editor.hbs
Normal file
@ -0,0 +1,11 @@
|
||||
{{yield (hash
|
||||
pane=(component "gh-simplemde"
|
||||
value=markdown
|
||||
placeholder=placeholder
|
||||
autofocus=autofocus
|
||||
onChange=(action "updateMarkdown")
|
||||
onEditorInit=(action "setEditor")
|
||||
onEditorDestroy=(action "destroyEditor")
|
||||
options=simpleMDEOptions)
|
||||
focus=(action "focusEditor")
|
||||
)}}
|
5
ghost/admin/app/templates/components/gh-progress-bar.hbs
Normal file
5
ghost/admin/app/templates/components/gh-progress-bar.hbs
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="gh-progress-container">
|
||||
<div class="gh-progress-container-progress">
|
||||
<div class="gh-progress-bar {{if isError "-error"}}" style={{progressStyle}}></div>
|
||||
</div>
|
||||
</div>
|
1
ghost/admin/app/templates/components/gh-simplemde.hbs
Normal file
1
ghost/admin/app/templates/components/gh-simplemde.hbs
Normal file
@ -0,0 +1 @@
|
||||
{{yield}}
|
10
ghost/admin/app/templates/components/gh-uploader.hbs
Normal file
10
ghost/admin/app/templates/components/gh-uploader.hbs
Normal file
@ -0,0 +1,10 @@
|
||||
{{#if hasBlock}}
|
||||
{{yield (hash
|
||||
progressBar=(component "gh-progress-bar" percentage=uploadPercentage)
|
||||
files=files
|
||||
errors=errors
|
||||
cancel=(action "cancel")
|
||||
)}}
|
||||
{{else}}
|
||||
{{!-- TODO: default uploader interface --}}
|
||||
{{/if}}
|
@ -1,5 +1,11 @@
|
||||
{{#gh-editor tagName="section" class="gh-view" navIsClosed=navIsClosed as |headerClass|}}
|
||||
<header class="gh-editor-header {{headerClass}}">
|
||||
{{#gh-editor
|
||||
tagName="section"
|
||||
class="gh-editor gh-view"
|
||||
navIsClosed=navIsClosed
|
||||
toggleAutoNav=(action "toggleAutoNav")
|
||||
as |editor|
|
||||
}}
|
||||
<header class="gh-editor-header {{editor.headerClass}}">
|
||||
<div class="gh-editor-status">
|
||||
{{gh-editor-post-status
|
||||
post=model
|
||||
@ -24,40 +30,83 @@
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<div class="gh-editor-container needsclick">
|
||||
<div class="gh-editor-inner">
|
||||
{{!--
|
||||
NOTE: the mobiledoc property is unbound so that the setting the
|
||||
serialized version onChange doesn't cause a deserialization and
|
||||
re-render of the editor on every key press / editor change
|
||||
--}}
|
||||
{{#gh-koenig
|
||||
mobiledoc=(unbound model.scratch)
|
||||
onChange=(action "updateScratch")
|
||||
onFirstChange=(action "autoSaveNew")
|
||||
autofocus=shouldFocusEditor
|
||||
tabindex="2"
|
||||
titleSelector="#kg-title-input"
|
||||
containerSelector=".gh-editor-container"
|
||||
wordcountDidChange=(action "setWordcount")
|
||||
as |koenig|
|
||||
}}
|
||||
{{koenig-title-input
|
||||
id="koenig-title-input"
|
||||
val=(readonly model.titleScratch)
|
||||
onChange=(action (mut model.titleScratch))
|
||||
tabindex="1"
|
||||
autofocus=shouldFocusTitle
|
||||
focus-out="updateTitle"
|
||||
update=(action (perform updateTitle))
|
||||
editor=(readonly koenig.editor)
|
||||
editorHasRendered=koenig.hasRendered
|
||||
editorMenuIsOpen=koenig.isMenuOpen
|
||||
}}
|
||||
{{/gh-koenig}}
|
||||
{{!--
|
||||
NOTE: title is part of the markdown editor container so that it has
|
||||
access to the markdown editor's "focus" action
|
||||
--}}
|
||||
{{#gh-markdown-editor
|
||||
class="gh-editor-container"
|
||||
tabindex="2"
|
||||
placeholder="Click here to start..."
|
||||
autofocus=shouldFocusEditor
|
||||
uploadedImageUrls=editor.uploadedImageUrls
|
||||
mobiledoc=(readonly model.scratch)
|
||||
onChange=(action "updateScratch")
|
||||
onFullScreen=(action editor.toggleFullScreen)
|
||||
showMarkdownHelp=(route-action "toggleMarkdownHelpModal")
|
||||
as |markdown|
|
||||
}}
|
||||
{{gh-trim-focus-input model.titleScratch
|
||||
type="text"
|
||||
class="gh-editor-title"
|
||||
placeholder="Your Post Title"
|
||||
tabindex="1"
|
||||
shouldFocus=shouldFocusTitle
|
||||
focus-out="updateTitle"
|
||||
update=(action (perform updateTitle))
|
||||
keyEvents=(hash
|
||||
9=(action markdown.focus 'bottom')
|
||||
13=(action markdown.focus 'top')
|
||||
)
|
||||
data-test-editor-title-input=true
|
||||
}}
|
||||
|
||||
{{markdown.pane}}
|
||||
{{/gh-markdown-editor}}
|
||||
|
||||
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}
|
||||
<footer class="gh-editor-footer"></footer>
|
||||
|
||||
{{!-- files are dragged over editor pane --}}
|
||||
{{#if editor.isDraggedOver}}
|
||||
<div class="drop-target gh-editor-drop-target">
|
||||
<div class="drop-target-message">
|
||||
<h3>Drop image(s) here to upload</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>
|
||||
{{/if}}
|
||||
|
||||
{{!-- files have been dropped ready to be uploaded --}}
|
||||
{{#if editor.droppedFiles}}
|
||||
{{#gh-uploader
|
||||
files=editor.droppedFiles
|
||||
accept=editor.imageMimeTypes
|
||||
extensions=editor.imageExtensions
|
||||
onComplete=(action editor.uploadComplete)
|
||||
onCancel=(action editor.uploadCancelled)
|
||||
as |upload|
|
||||
}}
|
||||
<div class="gh-editor-image-upload {{if upload.errors "-error"}}">
|
||||
<div class="gh-editor-image-upload-content">
|
||||
{{#if upload.errors}}
|
||||
<h3>Upload failed</h3>
|
||||
|
||||
{{#each upload.errors as |error|}}
|
||||
<div class="failed">{{error.fileName}} - {{error.message}}</div>
|
||||
{{/each}}
|
||||
|
||||
<button class="gh-btn gh-btn-grey gh-btn-icon" {{action upload.cancel}}>
|
||||
<span>{{inline-svg "close"}} Close</span>
|
||||
</button>
|
||||
{{else}}
|
||||
|
||||
<h3>Uploading {{pluralize upload.files.length "image"}}...</h3>
|
||||
{{upload.progressBar}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/gh-uploader}}
|
||||
{{/if}}
|
||||
{{/gh-editor}}
|
||||
|
||||
{{#if showDeletePostModal}}
|
||||
|
@ -132,11 +132,12 @@ module.exports = function (defaults) {
|
||||
'jquery-deparam': {
|
||||
import: ['jquery-deparam.js']
|
||||
},
|
||||
'mobiledoc-kit': {
|
||||
import: ['dist/amd/mobiledoc-kit.js', 'dist/amd/mobiledoc-kit.map']
|
||||
},
|
||||
'password-generator': {
|
||||
import: ['lib/password-generator.js']
|
||||
},
|
||||
'simplemde': {
|
||||
srcDir: 'dist',
|
||||
import: ['simplemde.min.js', 'simplemde.min.css']
|
||||
}
|
||||
},
|
||||
'ember-cli-selectize': {
|
||||
|
@ -98,19 +98,18 @@
|
||||
"liquid-wormhole": "2.0.4",
|
||||
"loader.js": "4.2.3",
|
||||
"matchdep": "1.0.1",
|
||||
"mobiledoc-kit": "0.10.15",
|
||||
"password-generator": "2.1.0",
|
||||
"postcss-color-function": "3.0.0",
|
||||
"postcss-custom-properties": "5.0.2",
|
||||
"postcss-easy-import": "2.0.0",
|
||||
"simplemde": "1.11.2",
|
||||
"top-gh-contribs": "2.0.4",
|
||||
"torii": "0.8.2",
|
||||
"walk-sync": "0.3.1"
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
"lib/asset-delivery",
|
||||
"lib/gh-koenig"
|
||||
"lib/asset-delivery"
|
||||
]
|
||||
},
|
||||
"greenkeeper": {
|
||||
|
@ -12,7 +12,6 @@ import {invalidateSession, authenticateSession} from 'ghost-admin/tests/helpers/
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
import sinon from 'sinon';
|
||||
import testSelector from 'ember-test-selectors';
|
||||
import {titleRendered, replaceTitleHTML} from '../helpers/editor-helpers';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('Acceptance: Editor', function() {
|
||||
@ -326,11 +325,7 @@ describe('Acceptance: Editor', function() {
|
||||
expect(currentURL(), 'currentURL')
|
||||
.to.equal('/editor/1');
|
||||
|
||||
titleRendered();
|
||||
|
||||
let title = find('#koenig-title-input div');
|
||||
title.html(Array(160).join('a'));
|
||||
|
||||
await fillIn(testSelector('editor-title-input'), Array(160).join('a'));
|
||||
await click(testSelector('publishmenu-trigger'));
|
||||
await click(testSelector('publishmenu-save'));
|
||||
|
||||
@ -345,43 +340,44 @@ describe('Acceptance: Editor', function() {
|
||||
).to.match(/Title cannot be longer than 150 characters/);
|
||||
});
|
||||
|
||||
it('inserts a placeholder if the title is blank', async function () {
|
||||
server.createList('post', 1);
|
||||
|
||||
// post id 1 is a draft, checking for draft behaviour now
|
||||
await visit('/editor/1');
|
||||
|
||||
expect(currentURL(), 'currentURL')
|
||||
.to.equal('/editor/1');
|
||||
|
||||
titleRendered();
|
||||
|
||||
let title = find('#koenig-title-input div');
|
||||
expect(title.data('placeholder')).to.equal('Your Post Title');
|
||||
expect(title.hasClass('no-content')).to.be.false;
|
||||
await title.html('');
|
||||
|
||||
expect(title.hasClass('no-content')).to.be.true;
|
||||
await title.html('test');
|
||||
|
||||
expect(title.hasClass('no-content')).to.be.false;
|
||||
});
|
||||
|
||||
it('removes HTML from the title.', async function () {
|
||||
server.createList('post', 1);
|
||||
|
||||
// post id 1 is a draft, checking for draft behaviour now
|
||||
await visit('/editor/1');
|
||||
|
||||
expect(currentURL(), 'currentURL')
|
||||
.to.equal('/editor/1');
|
||||
|
||||
titleRendered();
|
||||
|
||||
let title = find('#koenig-title-input div');
|
||||
await replaceTitleHTML('<div>TITLE 	    TEST</div> ');
|
||||
expect(title.html()).to.equal('TITLE TEST ');
|
||||
});
|
||||
// NOTE: these tests are specific to the mobiledoc editor
|
||||
// it('inserts a placeholder if the title is blank', async function () {
|
||||
// server.createList('post', 1);
|
||||
//
|
||||
// // post id 1 is a draft, checking for draft behaviour now
|
||||
// await visit('/editor/1');
|
||||
//
|
||||
// expect(currentURL(), 'currentURL')
|
||||
// .to.equal('/editor/1');
|
||||
//
|
||||
// titleRendered();
|
||||
//
|
||||
// let title = find('#koenig-title-input div');
|
||||
// expect(title.data('placeholder')).to.equal('Your Post Title');
|
||||
// expect(title.hasClass('no-content')).to.be.false;
|
||||
// await title.html('');
|
||||
//
|
||||
// expect(title.hasClass('no-content')).to.be.true;
|
||||
// await title.html('test');
|
||||
//
|
||||
// expect(title.hasClass('no-content')).to.be.false;
|
||||
// });
|
||||
//
|
||||
// it('removes HTML from the title.', async function () {
|
||||
// server.createList('post', 1);
|
||||
//
|
||||
// // post id 1 is a draft, checking for draft behaviour now
|
||||
// await visit('/editor/1');
|
||||
//
|
||||
// expect(currentURL(), 'currentURL')
|
||||
// .to.equal('/editor/1');
|
||||
//
|
||||
// titleRendered();
|
||||
//
|
||||
// let title = find('#koenig-title-input div');
|
||||
// await replaceTitleHTML('<div>TITLE 	    TEST</div> ');
|
||||
// expect(title.html()).to.equal('TITLE TEST ');
|
||||
// });
|
||||
|
||||
it('renders first countdown notification before scheduled time', async function () {
|
||||
let clock = sinon.useFakeTimers(moment().valueOf());
|
||||
|
@ -0,0 +1,24 @@
|
||||
import {expect} from 'chai';
|
||||
import {describe, it} from 'mocha';
|
||||
import {setupComponentTest} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describe('Integration: Component: gh-markdown-editor', function() {
|
||||
setupComponentTest('gh-markdown-editor', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#gh-markdown-editor}}
|
||||
// template content
|
||||
// {{/gh-markdown-editor}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{gh-markdown-editor}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import {expect} from 'chai';
|
||||
import {describe, it} from 'mocha';
|
||||
import {setupComponentTest} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describe('Integration: Component: gh-progress-bar', function() {
|
||||
setupComponentTest('gh-progress-bar', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#gh-progress-bar}}
|
||||
// template content
|
||||
// {{/gh-progress-bar}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{gh-progress-bar}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import {expect} from 'chai';
|
||||
import {describe, it} from 'mocha';
|
||||
import {setupComponentTest} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describe('Integration: Component: gh-simplemde', function() {
|
||||
setupComponentTest('gh-simplemde', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#gh-simplemde}}
|
||||
// template content
|
||||
// {{/gh-simplemde}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{gh-simplemde}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
});
|
24
ghost/admin/tests/integration/components/gh-uploader-test.js
Normal file
24
ghost/admin/tests/integration/components/gh-uploader-test.js
Normal file
@ -0,0 +1,24 @@
|
||||
import {expect} from 'chai';
|
||||
import {describe, it} from 'mocha';
|
||||
import {setupComponentTest} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describe('Integration: Component: gh-uploader', function() {
|
||||
setupComponentTest('gh-uploader', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#gh-uploader}}
|
||||
// template content
|
||||
// {{/gh-uploader}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{gh-uploader}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
});
|
@ -1713,7 +1713,13 @@ code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
codemirror@5.25.0:
|
||||
codemirror-spell-checker@*:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
|
||||
dependencies:
|
||||
typo-js "*"
|
||||
|
||||
codemirror@*, codemirror@5.25.0:
|
||||
version "5.25.0"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.25.0.tgz#78e06939c7bb41f65707b8aa9c5328111948b756"
|
||||
|
||||
@ -5115,6 +5121,10 @@ markdown-it@^8.2.0:
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.3"
|
||||
|
||||
marked@*:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
||||
|
||||
match-media@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/match-media/-/match-media-0.2.0.tgz#ea4e09742e7253cc7d7e1599ba627e0fa29fbc50"
|
||||
@ -5300,21 +5310,6 @@ mktemp@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
|
||||
|
||||
mobiledoc-dom-renderer@0.6.5:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/mobiledoc-dom-renderer/-/mobiledoc-dom-renderer-0.6.5.tgz#56c0302c4f9c30840ab5b9b20dfe905aed1e437b"
|
||||
|
||||
mobiledoc-kit@0.10.15:
|
||||
version "0.10.15"
|
||||
resolved "https://registry.yarnpkg.com/mobiledoc-kit/-/mobiledoc-kit-0.10.15.tgz#b4afb65febeafb65d7cef6ae9cd2e71b1de8c910"
|
||||
dependencies:
|
||||
mobiledoc-dom-renderer "0.6.5"
|
||||
mobiledoc-text-renderer "0.3.2"
|
||||
|
||||
mobiledoc-text-renderer@0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/mobiledoc-text-renderer/-/mobiledoc-text-renderer-0.3.2.tgz#126a167a6cf8b6cd7e58c85feb18043603834580"
|
||||
|
||||
mocha@^2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58"
|
||||
@ -6589,6 +6584,14 @@ simple-is@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0"
|
||||
|
||||
simplemde@1.11.2:
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/simplemde/-/simplemde-1.11.2.tgz#a23a35d978d2c40ef07dec008c92f070d8e080e3"
|
||||
dependencies:
|
||||
codemirror "*"
|
||||
codemirror-spell-checker "*"
|
||||
marked "*"
|
||||
|
||||
sinon@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.1.0.tgz#e057a9d2bf1b32f5d6dd62628ca9ee3961b0cafb"
|
||||
@ -7119,6 +7122,10 @@ typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
typo-js@*:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.0.3.tgz#54d8ebc7949f1a7810908b6002c6841526c99d5a"
|
||||
|
||||
uc.micro@^1.0.0, uc.micro@^1.0.1, uc.micro@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
|
||||
|
Loading…
Reference in New Issue
Block a user