mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +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 Component from 'ember-component';
|
||||||
import run from 'ember-runloop';
|
import run from 'ember-runloop';
|
||||||
|
import {
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
|
IMAGE_EXTENSIONS
|
||||||
|
} from 'ghost-admin/components/gh-image-uploader';
|
||||||
|
|
||||||
const {debounce} = run;
|
const {debounce} = run;
|
||||||
|
|
||||||
export default Component.extend({
|
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,
|
_navIsClosed: false,
|
||||||
|
_onResizeHandler: null,
|
||||||
_viewActionsWidth: 190,
|
_viewActionsWidth: 190,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -27,22 +49,18 @@ export default Component.extend({
|
|||||||
let navIsClosed = this.get('navIsClosed');
|
let navIsClosed = this.get('navIsClosed');
|
||||||
|
|
||||||
if (navIsClosed !== this._navIsClosed) {
|
if (navIsClosed !== this._navIsClosed) {
|
||||||
|
this._fullScreenEnabled = navIsClosed;
|
||||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._navIsClosed = navIsClosed;
|
this._navIsClosed = navIsClosed;
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
window.removeEventListener('resize', this._onResizeHandler);
|
|
||||||
},
|
|
||||||
|
|
||||||
_setHeaderClass() {
|
_setHeaderClass() {
|
||||||
let $editorInner = this.$('.gh-editor-inner');
|
let $editorTitle = this.$('.gh-editor-title');
|
||||||
|
|
||||||
if ($editorInner.length > 0) {
|
if ($editorTitle.length > 0) {
|
||||||
let boundingRect = $editorInner[0].getBoundingClientRect();
|
let boundingRect = $editorTitle[0].getBoundingClientRect();
|
||||||
let maxRight = window.innerWidth - this._viewActionsWidth;
|
let maxRight = window.innerWidth - this._viewActionsWidth;
|
||||||
|
|
||||||
if (boundingRect.right >= maxRight) {
|
if (boundingRect.right >= maxRight) {
|
||||||
@ -52,5 +70,89 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.set('headerClass', '');
|
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
|
UnsupportedMediaTypeError
|
||||||
} from 'ghost-admin/services/ajax';
|
} 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({
|
export default Component.extend({
|
||||||
tagName: 'section',
|
tagName: 'section',
|
||||||
classNames: ['gh-image-uploader'],
|
classNames: ['gh-image-uploader'],
|
||||||
@ -37,8 +40,8 @@ export default Component.extend({
|
|||||||
ajax: injectService(),
|
ajax: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
|
|
||||||
_defaultAccept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
|
_defaultAccept: IMAGE_MIME_TYPES,
|
||||||
_defaultExtensions: ['gif', 'jpg', 'jpeg', 'png', 'svg'],
|
_defaultExtensions: IMAGE_EXTENSIONS,
|
||||||
_defaultUploadUrl: '/uploads/',
|
_defaultUploadUrl: '/uploads/',
|
||||||
|
|
||||||
// TODO: this wouldn't be necessary if the server could accept direct
|
// 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';
|
import TextInputMixin from 'ghost-admin/mixins/text-input';
|
||||||
|
|
||||||
export default OneWayInput.extend(TextInputMixin, {
|
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'),
|
navIsClosed: reads('application.autoNav'),
|
||||||
|
|
||||||
|
_hasChanged: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
window.onbeforeunload = () => {
|
window.onbeforeunload = () => {
|
||||||
@ -466,6 +468,16 @@ export default Mixin.create({
|
|||||||
actions: {
|
actions: {
|
||||||
updateScratch(value) {
|
updateScratch(value) {
|
||||||
this.set('model.scratch', 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
|
// save 3 seconds after last edit
|
||||||
this.get('_autosave').perform();
|
this.get('_autosave').perform();
|
||||||
// force save at 60 seconds
|
// 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() {
|
closeNavMenu() {
|
||||||
this.get('application').send('closeAutoNav');
|
this.get('application').send('closeAutoNav');
|
||||||
},
|
},
|
||||||
@ -573,6 +579,10 @@ export default Mixin.create({
|
|||||||
|
|
||||||
setWordcount(wordcount) {
|
setWordcount(wordcount) {
|
||||||
this.set('wordcount', wordcount);
|
this.set('wordcount', wordcount);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAutoNav() {
|
||||||
|
this.get('application').send('toggleAutoNav');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -132,6 +132,9 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
|
|||||||
controller.set('previousTagNames', []);
|
controller.set('previousTagNames', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset save-on-first-change
|
||||||
|
controller._hasChanged = false;
|
||||||
|
|
||||||
// attach model-related listeners created in editor-base-route
|
// attach model-related listeners created in editor-base-route
|
||||||
this.attachModelHooks(controller, model);
|
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 boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||||
import {isBlank} from 'ember-utils';
|
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
|
// ember-cli-shims doesn't export these so we must get them manually
|
||||||
const {Comparable, compare} = Ember;
|
const {Comparable, compare} = Ember;
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
/* Addons: gh-koenig
|
/* Addons: gh-koenig
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
@import "addons/gh-koenig/gh-koenig.css";
|
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
/* Addons: gh-koenig
|
/* Addons: gh-koenig
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
@import "addons/gh-koenig/gh-koenig.css";
|
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------✈️----------------------------- */
|
/* ---------------------------✈️----------------------------- */
|
||||||
|
@ -89,7 +89,14 @@
|
|||||||
cursor: pointer;
|
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;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -97,7 +104,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .progress {
|
.gh-image-uploader .progress,
|
||||||
|
.gh-progress-container-progress {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
@ -106,17 +114,14 @@
|
|||||||
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .failed {
|
.gh-image-uploader .bar,
|
||||||
margin: 1em 2em;
|
.gh-progress-bar {
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-image-uploader .bar {
|
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: var(--blue);
|
background: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .bar.fail {
|
.gh-image-uploader .bar.fail,
|
||||||
|
.gh-progress-bar.-error {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
}
|
}
|
||||||
|
@ -156,25 +156,6 @@
|
|||||||
.gh-editor-header { padding: 0 4vw; }
|
.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 {
|
.gh-editor-header-small {
|
||||||
height: 43px;
|
height: 43px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -192,10 +173,42 @@
|
|||||||
padding: 13px 15px;
|
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 {
|
.gh-editor-wordcount {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
padding:10px;
|
padding:10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
@ -203,3 +216,104 @@
|
|||||||
display: none;
|
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|}}
|
{{#gh-editor
|
||||||
<header class="gh-editor-header {{headerClass}}">
|
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">
|
<div class="gh-editor-status">
|
||||||
{{gh-editor-post-status
|
{{gh-editor-post-status
|
||||||
post=model
|
post=model
|
||||||
@ -24,40 +30,83 @@
|
|||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="gh-editor-container needsclick">
|
{{!--
|
||||||
<div class="gh-editor-inner">
|
NOTE: title is part of the markdown editor container so that it has
|
||||||
{{!--
|
access to the markdown editor's "focus" action
|
||||||
NOTE: the mobiledoc property is unbound so that the setting the
|
--}}
|
||||||
serialized version onChange doesn't cause a deserialization and
|
{{#gh-markdown-editor
|
||||||
re-render of the editor on every key press / editor change
|
class="gh-editor-container"
|
||||||
--}}
|
tabindex="2"
|
||||||
{{#gh-koenig
|
placeholder="Click here to start..."
|
||||||
mobiledoc=(unbound model.scratch)
|
autofocus=shouldFocusEditor
|
||||||
onChange=(action "updateScratch")
|
uploadedImageUrls=editor.uploadedImageUrls
|
||||||
onFirstChange=(action "autoSaveNew")
|
mobiledoc=(readonly model.scratch)
|
||||||
autofocus=shouldFocusEditor
|
onChange=(action "updateScratch")
|
||||||
tabindex="2"
|
onFullScreen=(action editor.toggleFullScreen)
|
||||||
titleSelector="#kg-title-input"
|
showMarkdownHelp=(route-action "toggleMarkdownHelpModal")
|
||||||
containerSelector=".gh-editor-container"
|
as |markdown|
|
||||||
wordcountDidChange=(action "setWordcount")
|
}}
|
||||||
as |koenig|
|
{{gh-trim-focus-input model.titleScratch
|
||||||
}}
|
type="text"
|
||||||
{{koenig-title-input
|
class="gh-editor-title"
|
||||||
id="koenig-title-input"
|
placeholder="Your Post Title"
|
||||||
val=(readonly model.titleScratch)
|
tabindex="1"
|
||||||
onChange=(action (mut model.titleScratch))
|
shouldFocus=shouldFocusTitle
|
||||||
tabindex="1"
|
focus-out="updateTitle"
|
||||||
autofocus=shouldFocusTitle
|
update=(action (perform updateTitle))
|
||||||
focus-out="updateTitle"
|
keyEvents=(hash
|
||||||
update=(action (perform updateTitle))
|
9=(action markdown.focus 'bottom')
|
||||||
editor=(readonly koenig.editor)
|
13=(action markdown.focus 'top')
|
||||||
editorHasRendered=koenig.hasRendered
|
)
|
||||||
editorMenuIsOpen=koenig.isMenuOpen
|
data-test-editor-title-input=true
|
||||||
}}
|
}}
|
||||||
{{/gh-koenig}}
|
|
||||||
|
{{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>
|
{{/if}}
|
||||||
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>
|
|
||||||
|
{{!-- 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}}
|
{{/gh-editor}}
|
||||||
|
|
||||||
{{#if showDeletePostModal}}
|
{{#if showDeletePostModal}}
|
||||||
|
@ -132,11 +132,12 @@ module.exports = function (defaults) {
|
|||||||
'jquery-deparam': {
|
'jquery-deparam': {
|
||||||
import: ['jquery-deparam.js']
|
import: ['jquery-deparam.js']
|
||||||
},
|
},
|
||||||
'mobiledoc-kit': {
|
|
||||||
import: ['dist/amd/mobiledoc-kit.js', 'dist/amd/mobiledoc-kit.map']
|
|
||||||
},
|
|
||||||
'password-generator': {
|
'password-generator': {
|
||||||
import: ['lib/password-generator.js']
|
import: ['lib/password-generator.js']
|
||||||
|
},
|
||||||
|
'simplemde': {
|
||||||
|
srcDir: 'dist',
|
||||||
|
import: ['simplemde.min.js', 'simplemde.min.css']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'ember-cli-selectize': {
|
'ember-cli-selectize': {
|
||||||
|
@ -98,19 +98,18 @@
|
|||||||
"liquid-wormhole": "2.0.4",
|
"liquid-wormhole": "2.0.4",
|
||||||
"loader.js": "4.2.3",
|
"loader.js": "4.2.3",
|
||||||
"matchdep": "1.0.1",
|
"matchdep": "1.0.1",
|
||||||
"mobiledoc-kit": "0.10.15",
|
|
||||||
"password-generator": "2.1.0",
|
"password-generator": "2.1.0",
|
||||||
"postcss-color-function": "3.0.0",
|
"postcss-color-function": "3.0.0",
|
||||||
"postcss-custom-properties": "5.0.2",
|
"postcss-custom-properties": "5.0.2",
|
||||||
"postcss-easy-import": "2.0.0",
|
"postcss-easy-import": "2.0.0",
|
||||||
|
"simplemde": "1.11.2",
|
||||||
"top-gh-contribs": "2.0.4",
|
"top-gh-contribs": "2.0.4",
|
||||||
"torii": "0.8.2",
|
"torii": "0.8.2",
|
||||||
"walk-sync": "0.3.1"
|
"walk-sync": "0.3.1"
|
||||||
},
|
},
|
||||||
"ember-addon": {
|
"ember-addon": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"lib/asset-delivery",
|
"lib/asset-delivery"
|
||||||
"lib/gh-koenig"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"greenkeeper": {
|
"greenkeeper": {
|
||||||
|
@ -12,7 +12,6 @@ import {invalidateSession, authenticateSession} from 'ghost-admin/tests/helpers/
|
|||||||
import Mirage from 'ember-cli-mirage';
|
import Mirage from 'ember-cli-mirage';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import testSelector from 'ember-test-selectors';
|
import testSelector from 'ember-test-selectors';
|
||||||
import {titleRendered, replaceTitleHTML} from '../helpers/editor-helpers';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
describe('Acceptance: Editor', function() {
|
describe('Acceptance: Editor', function() {
|
||||||
@ -326,11 +325,7 @@ describe('Acceptance: Editor', function() {
|
|||||||
expect(currentURL(), 'currentURL')
|
expect(currentURL(), 'currentURL')
|
||||||
.to.equal('/editor/1');
|
.to.equal('/editor/1');
|
||||||
|
|
||||||
titleRendered();
|
await fillIn(testSelector('editor-title-input'), Array(160).join('a'));
|
||||||
|
|
||||||
let title = find('#koenig-title-input div');
|
|
||||||
title.html(Array(160).join('a'));
|
|
||||||
|
|
||||||
await click(testSelector('publishmenu-trigger'));
|
await click(testSelector('publishmenu-trigger'));
|
||||||
await click(testSelector('publishmenu-save'));
|
await click(testSelector('publishmenu-save'));
|
||||||
|
|
||||||
@ -345,43 +340,44 @@ describe('Acceptance: Editor', function() {
|
|||||||
).to.match(/Title cannot be longer than 150 characters/);
|
).to.match(/Title cannot be longer than 150 characters/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inserts a placeholder if the title is blank', async function () {
|
// NOTE: these tests are specific to the mobiledoc editor
|
||||||
server.createList('post', 1);
|
// 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');
|
// // post id 1 is a draft, checking for draft behaviour now
|
||||||
|
// await visit('/editor/1');
|
||||||
expect(currentURL(), 'currentURL')
|
//
|
||||||
.to.equal('/editor/1');
|
// expect(currentURL(), 'currentURL')
|
||||||
|
// .to.equal('/editor/1');
|
||||||
titleRendered();
|
//
|
||||||
|
// titleRendered();
|
||||||
let title = find('#koenig-title-input div');
|
//
|
||||||
expect(title.data('placeholder')).to.equal('Your Post Title');
|
// let title = find('#koenig-title-input div');
|
||||||
expect(title.hasClass('no-content')).to.be.false;
|
// expect(title.data('placeholder')).to.equal('Your Post Title');
|
||||||
await title.html('');
|
// 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.true;
|
||||||
|
// await title.html('test');
|
||||||
expect(title.hasClass('no-content')).to.be.false;
|
//
|
||||||
});
|
// expect(title.hasClass('no-content')).to.be.false;
|
||||||
|
// });
|
||||||
it('removes HTML from the title.', async function () {
|
//
|
||||||
server.createList('post', 1);
|
// 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');
|
// // post id 1 is a draft, checking for draft behaviour now
|
||||||
|
// await visit('/editor/1');
|
||||||
expect(currentURL(), 'currentURL')
|
//
|
||||||
.to.equal('/editor/1');
|
// expect(currentURL(), 'currentURL')
|
||||||
|
// .to.equal('/editor/1');
|
||||||
titleRendered();
|
//
|
||||||
|
// titleRendered();
|
||||||
let title = find('#koenig-title-input div');
|
//
|
||||||
await replaceTitleHTML('<div>TITLE 	    TEST</div> ');
|
// let title = find('#koenig-title-input div');
|
||||||
expect(title.html()).to.equal('TITLE TEST ');
|
// await replaceTitleHTML('<div>TITLE 	    TEST</div> ');
|
||||||
});
|
// expect(title.html()).to.equal('TITLE TEST ');
|
||||||
|
// });
|
||||||
|
|
||||||
it('renders first countdown notification before scheduled time', async function () {
|
it('renders first countdown notification before scheduled time', async function () {
|
||||||
let clock = sinon.useFakeTimers(moment().valueOf());
|
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"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
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"
|
version "5.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.25.0.tgz#78e06939c7bb41f65707b8aa9c5328111948b756"
|
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"
|
mdurl "^1.0.1"
|
||||||
uc.micro "^1.0.3"
|
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:
|
match-media@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/match-media/-/match-media-0.2.0.tgz#ea4e09742e7253cc7d7e1599ba627e0fa29fbc50"
|
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"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
|
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:
|
mocha@^2.5.3:
|
||||||
version "2.5.3"
|
version "2.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58"
|
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"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0"
|
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:
|
sinon@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.1.0.tgz#e057a9d2bf1b32f5d6dd62628ca9ee3961b0cafb"
|
resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.1.0.tgz#e057a9d2bf1b32f5d6dd62628ca9ee3961b0cafb"
|
||||||
@ -7119,6 +7122,10 @@ typedarray@^0.0.6:
|
|||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
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:
|
uc.micro@^1.0.0, uc.micro@^1.0.1, uc.micro@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
|
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
|
||||||
|
Loading…
Reference in New Issue
Block a user