mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
Resurrect the old alpha Koenig editor (#916)
requires https://github.com/TryGhost/Ghost/pull/9277 - added a `koenigEditor` feature flag - modified the feature service to accept a `developer` boolean on the options object passed into the internal `feature` method, if `true` the feature flag won't be enabled unless the `enableDeveloperExperiments` config option is also enabled - added "developer feature testing" section in labs that's only visible if `enableDeveloperExperiments` config flag is enabled - added koenig editor toggle to the developer section in labs - enabled a switch between the markdown and koenig editors - modified the default value of the `mobiledoc` attr in the Post model to be a blank mobiledoc or blank markdown mobiledoc depending on the feature flag - modified the `autofocus` switch in editor controller's `setPost` method so that it is always switched, even for new->edit where the post model isn't swapped - added a compatibility check to the editor controller's `setPost` method that shows an alert and force enables the koenig editor if the koenig flag is not enabled and the opened post is not compatible with the markdown editor - fixed various issues that have appeared due to the old koenig alpha becoming out of sync with master
This commit is contained in:
parent
4fc762c1c4
commit
506b2a9388
@ -99,7 +99,7 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
_setHeaderClass() {
|
||||
let $editorTitle = this.$('.gh-editor-title');
|
||||
let $editorTitle = this.$('.gh-editor-title, .kg-title-input');
|
||||
let smallHeaderClass = 'gh-editor-header-small';
|
||||
|
||||
if (this.get('isSplitScreen')) {
|
||||
|
@ -77,6 +77,7 @@ const messageMap = {
|
||||
|
||||
export default Controller.extend({
|
||||
application: controller(),
|
||||
feature: service(),
|
||||
notifications: service(),
|
||||
router: service(),
|
||||
slugGenerator: service(),
|
||||
@ -89,6 +90,10 @@ export default Controller.extend({
|
||||
showDeletePostModal: false,
|
||||
showLeaveEditorModal: false,
|
||||
showReAuthenticateModal: false,
|
||||
useKoenig: false,
|
||||
|
||||
// koenig related properties
|
||||
wordcount: 0,
|
||||
|
||||
/* private properties ----------------------------------------------------*/
|
||||
|
||||
@ -240,6 +245,11 @@ export default Controller.extend({
|
||||
|
||||
toggleReAuthenticateModal() {
|
||||
this.toggleProperty('showReAuthenticateModal');
|
||||
},
|
||||
|
||||
// TODO: this should be part of the koenig component
|
||||
setWordcount(count) {
|
||||
this.set('wordcount', count);
|
||||
}
|
||||
},
|
||||
|
||||
@ -472,7 +482,27 @@ export default Controller.extend({
|
||||
|
||||
// called by the new/edit routes to change the post model
|
||||
setPost(post) {
|
||||
// don't do anything if the post is the same
|
||||
// switch between markdown/koenig depending on feature flag and post
|
||||
// compatibility
|
||||
let koenigEnabled = this.get('feature.koenigEditor');
|
||||
let postIsMarkdownCompatible = post.isCompatibleWithMarkdownEditor();
|
||||
if (koenigEnabled || !postIsMarkdownCompatible) {
|
||||
this.set('useKoenig', true);
|
||||
|
||||
// display an alert if koenig is disabled but we use it anyway
|
||||
// because the post is incompatible with the markdown editor
|
||||
if (!koenigEnabled) {
|
||||
alert('This post will be opened with the Koenig editor because it\'s not compatible with the markdown editor');
|
||||
}
|
||||
} else {
|
||||
this.set('useKoenig', false);
|
||||
}
|
||||
|
||||
// autofocus the editor if we have a new post, this also acts as a
|
||||
// change signal to the persistent editor on new->edit
|
||||
this.set('shouldFocusEditor', post.get('isNew'));
|
||||
|
||||
// don't do anything else if we're setting the same post
|
||||
if (post === this.get('post')) {
|
||||
return;
|
||||
}
|
||||
@ -482,9 +512,6 @@ export default Controller.extend({
|
||||
|
||||
this.set('post', post);
|
||||
|
||||
// only autofocus the editor if we have a new post
|
||||
this.set('shouldFocusEditor', post.get('isNew'));
|
||||
|
||||
// need to set scratch values because they won't be present on first
|
||||
// edit of the post
|
||||
// TODO: can these be `boundOneWay` on the model as per the other attrs?
|
||||
@ -641,10 +668,16 @@ export default Controller.extend({
|
||||
}
|
||||
|
||||
// scratch isn't an attr so needs a manual dirty check
|
||||
let mobiledoc = JSON.stringify(post.get('mobiledoc'));
|
||||
let scratch = JSON.stringify(post.get('scratch'));
|
||||
if (scratch !== mobiledoc) {
|
||||
return true;
|
||||
let mobiledoc = post.get('mobiledoc');
|
||||
let scratch = post.get('scratch');
|
||||
// additional guard in case we are trying to compare null with undefined
|
||||
if (scratch || mobiledoc) {
|
||||
let mobiledocJSON = JSON.stringify(mobiledoc);
|
||||
let scratchJSON = JSON.stringify(scratch);
|
||||
|
||||
if (scratchJSON !== mobiledocJSON) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// new+unsaved posts always return `hasDirtyAttributes: true`
|
||||
|
@ -4,7 +4,8 @@ import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import attr from 'ember-data/attr';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import moment from 'moment';
|
||||
import {BLANK_DOC} from 'ghost-admin/components/gh-markdown-editor';
|
||||
import {BLANK_DOC as BLANK_MARKDOWN} from 'ghost-admin/components/gh-markdown-editor';
|
||||
import {BLANK_DOC as BLANK_MOBILEDOC} from 'gh-koenig/components/gh-koenig';
|
||||
import {belongsTo, hasMany} from 'ember-data/relationships';
|
||||
import {compare} from '@ember/utils';
|
||||
import {computed} from '@ember/object';
|
||||
@ -69,6 +70,7 @@ function publishedAtCompare(postA, postB) {
|
||||
|
||||
export default Model.extend(Comparable, ValidationEngine, {
|
||||
config: service(),
|
||||
feature: service(),
|
||||
ghostPaths: service(),
|
||||
clock: service(),
|
||||
settings: service(),
|
||||
@ -93,7 +95,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
locale: attr('string'),
|
||||
metaDescription: attr('string'),
|
||||
metaTitle: attr('string'),
|
||||
mobiledoc: attr('json-string', {defaultValue: () => BLANK_DOC}),
|
||||
mobiledoc: attr('json-string'),
|
||||
page: attr('boolean', {defaultValue: false}),
|
||||
plaintext: attr('string'),
|
||||
publishedAtUTC: attr('moment-utc'),
|
||||
@ -113,6 +115,28 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
async: false
|
||||
}),
|
||||
|
||||
init() {
|
||||
// we can't use the defaultValue property on the attr because it won't
|
||||
// have access to `this` for the feature check so we do it manually here.
|
||||
if (!this.get('mobiledoc')) {
|
||||
let defaultValue;
|
||||
|
||||
if (this.get('feature.koenigEditor')) {
|
||||
defaultValue = BLANK_MOBILEDOC;
|
||||
} else {
|
||||
defaultValue = BLANK_MARKDOWN;
|
||||
}
|
||||
|
||||
// using this.set() adds the property to the changedAttributes list
|
||||
// which means the editor always sees new posts as dirty. Assigning
|
||||
// the value directly works around that so you can exit the editor
|
||||
// without a warning
|
||||
this.mobiledoc = defaultValue;
|
||||
}
|
||||
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
scratch: null,
|
||||
titleScratch: null,
|
||||
|
||||
@ -313,5 +337,25 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
let publishedAtBlogTZ = this.get('publishedAtBlogTZ');
|
||||
let publishedAtUTC = publishedAtBlogTZ ? publishedAtBlogTZ.utc() : null;
|
||||
this.set('publishedAtUTC', publishedAtUTC);
|
||||
},
|
||||
|
||||
// the markdown editor expects a very specific mobiledoc format, if it
|
||||
// doesn't match then we'll need to handle it by forcing Koenig
|
||||
isCompatibleWithMarkdownEditor() {
|
||||
let mobiledoc = this.get('mobiledoc');
|
||||
|
||||
if (
|
||||
mobiledoc.markups.length === 0
|
||||
&& mobiledoc.cards.length === 1
|
||||
&& mobiledoc.cards[0][0] === 'card-markdown'
|
||||
&& mobiledoc.sections.length === 1
|
||||
&& mobiledoc.sections[0].length === 2
|
||||
&& mobiledoc.sections[0][0] === 10
|
||||
&& mobiledoc.sections[0][1] === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
@ -12,18 +12,24 @@ export function feature(name, options = {}) {
|
||||
|
||||
return computed.apply(Ember, watchedProps.concat({
|
||||
get() {
|
||||
let enabled = false;
|
||||
|
||||
if (user) {
|
||||
return this.get(`accessibility.${name}`);
|
||||
enabled = this.get(`accessibility.${name}`);
|
||||
} else if (this.get(`config.${name}`)) {
|
||||
enabled = this.get(`config.${name}`);
|
||||
} else {
|
||||
enabled = this.get(`labs.${name}`) || false;
|
||||
}
|
||||
|
||||
if (this.get(`config.${name}`)) {
|
||||
return this.get(`config.${name}`);
|
||||
if (options.developer) {
|
||||
enabled = enabled && this.get('config.enableDeveloperExperiments');
|
||||
}
|
||||
|
||||
return this.get(`labs.${name}`) || false;
|
||||
return enabled;
|
||||
},
|
||||
set(key, value) {
|
||||
this.update(key, value, user);
|
||||
this.update(key, value, options);
|
||||
|
||||
if (onChange) {
|
||||
// value must be passed here because the value isn't set until
|
||||
@ -44,6 +50,7 @@ export default Service.extend({
|
||||
notifications: service(),
|
||||
lazyLoader: service(),
|
||||
|
||||
koenigEditor: feature('koenigEditor', {developer: true}),
|
||||
publicAPI: feature('publicAPI'),
|
||||
subscribers: feature('subscribers'),
|
||||
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
||||
@ -80,9 +87,9 @@ export default Service.extend({
|
||||
});
|
||||
},
|
||||
|
||||
update(key, value, user = false) {
|
||||
let serviceProperty = user ? 'accessibility' : 'labs';
|
||||
let model = this.get(user ? '_user' : 'settings');
|
||||
update(key, value, options = {}) {
|
||||
let serviceProperty = options.user ? 'accessibility' : 'labs';
|
||||
let model = this.get(options.user ? '_user' : 'settings');
|
||||
let featureObject = this.get(serviceProperty);
|
||||
|
||||
// set the new key value for either the labs property or the accessibility property
|
||||
@ -102,7 +109,7 @@ export default Service.extend({
|
||||
// we'll always have an errors object unless we hit a
|
||||
// validation error
|
||||
if (!error) {
|
||||
throw new EmberError(`Validation of the feature service ${user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`);
|
||||
throw new EmberError(`Validation of the feature service ${options.user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`);
|
||||
}
|
||||
|
||||
this.get('notifications').showAPIError(error);
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "koenig-toolbar.css";
|
||||
@import "koenig-menu.css";
|
||||
@import "../ghost-editor/cardmenu.css";
|
||||
@import "koenig-cardmenu.css";
|
||||
|
||||
.gh-koenig-container {
|
||||
height: 100%;
|
||||
|
@ -56,7 +56,7 @@
|
||||
|
||||
/* Addons: gh-koenig
|
||||
/* ---------------------------------------------------------- */
|
||||
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||
@import "addons/gh-koenig/gh-koenig.css";
|
||||
|
||||
|
||||
:root {
|
||||
|
@ -56,7 +56,7 @@
|
||||
|
||||
/* Addons: gh-koenig
|
||||
/* ---------------------------------------------------------- */
|
||||
/*@import "addons/gh-koenig/gh-koenig.css";*/
|
||||
@import "addons/gh-koenig/gh-koenig.css";
|
||||
|
||||
|
||||
/* ---------------------------✈️----------------------------- */
|
||||
|
@ -32,100 +32,144 @@
|
||||
</section>
|
||||
</header>
|
||||
|
||||
{{!--
|
||||
NOTE: title is part of the markdown editor container so that it has
|
||||
access to the markdown editor's "focus" action
|
||||
--}}
|
||||
{{#gh-markdown-editor
|
||||
tabindex="2"
|
||||
placeholder="Begin writing your story..."
|
||||
autofocus=shouldFocusEditor
|
||||
uploadedImageUrls=editor.uploadedImageUrls
|
||||
mobiledoc=(readonly post.scratch)
|
||||
isFullScreen=editor.isFullScreen
|
||||
onChange=(action "updateScratch")
|
||||
onFullScreenToggle=(action editor.toggleFullScreen)
|
||||
onPreviewToggle=(action editor.togglePreview)
|
||||
onSplitScreenToggle=(action editor.toggleSplitScreen)
|
||||
onImageFilesSelected=(action editor.uploadImages)
|
||||
imageMimeTypes=editor.imageMimeTypes
|
||||
as |markdown|
|
||||
}}
|
||||
<div class="gh-markdown-editor-pane">
|
||||
{{gh-textarea post.titleScratch
|
||||
class="gh-editor-title"
|
||||
placeholder="Post Title"
|
||||
tabindex="1"
|
||||
autoExpand=".gh-markdown-editor-pane"
|
||||
update=(action "updateTitleScratch")
|
||||
focusOut=(action (perform saveTitle))
|
||||
keyEvents=(hash
|
||||
9=(action markdown.focus 'bottom')
|
||||
13=(action markdown.focus 'top')
|
||||
)
|
||||
data-test-editor-title-input=true
|
||||
}}
|
||||
{{markdown.editor}}
|
||||
</div>
|
||||
{{#if useKoenig}}
|
||||
<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
|
||||
|
||||
{{#if markdown.isSplitScreen}}
|
||||
<div class="gh-markdown-editor-preview">
|
||||
<h1 class="gh-markdown-editor-preview-title">{{post.titleScratch}}</h1>
|
||||
<div class="gh-markdown-editor-preview-content"></div>
|
||||
TODO: note above is no longer correct, changed to readonly to
|
||||
fix a persistent editor content bug that occurred due to the
|
||||
editor not being re-rendered on edit->new transition.
|
||||
|
||||
Needs perf investigation!
|
||||
--}}
|
||||
{{#gh-koenig
|
||||
mobiledoc=(readonly model.scratch)
|
||||
onChange=(action "updateScratch")
|
||||
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 "updateTitleScratch")
|
||||
tabindex="1"
|
||||
autofocus=shouldFocusTitle
|
||||
focusOut=(action (perform saveTitle))
|
||||
editor=(readonly koenig.editor)
|
||||
editorHasRendered=koenig.hasRendered
|
||||
editorMenuIsOpen=koenig.isMenuOpen
|
||||
}}
|
||||
{{/gh-koenig}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>
|
||||
|
||||
{{else}}
|
||||
|
||||
{{!--
|
||||
NOTE: title is part of the markdown editor container so that it has
|
||||
access to the markdown editor's "focus" action
|
||||
--}}
|
||||
{{#gh-markdown-editor
|
||||
tabindex="2"
|
||||
placeholder="Begin writing your story..."
|
||||
autofocus=shouldFocusEditor
|
||||
uploadedImageUrls=editor.uploadedImageUrls
|
||||
mobiledoc=(readonly post.scratch)
|
||||
isFullScreen=editor.isFullScreen
|
||||
onChange=(action "updateScratch")
|
||||
onFullScreenToggle=(action editor.toggleFullScreen)
|
||||
onPreviewToggle=(action editor.togglePreview)
|
||||
onSplitScreenToggle=(action editor.toggleSplitScreen)
|
||||
onImageFilesSelected=(action editor.uploadImages)
|
||||
imageMimeTypes=editor.imageMimeTypes
|
||||
as |markdown|
|
||||
}}
|
||||
<div class="gh-markdown-editor-pane">
|
||||
{{gh-textarea post.titleScratch
|
||||
class="gh-editor-title"
|
||||
placeholder="Post Title"
|
||||
tabindex="1"
|
||||
autoExpand=".gh-markdown-editor-pane"
|
||||
update=(action "updateTitleScratch")
|
||||
focusOut=(action (perform saveTitle))
|
||||
keyEvents=(hash
|
||||
9=(action markdown.focus 'bottom')
|
||||
13=(action markdown.focus 'top')
|
||||
)
|
||||
data-test-editor-title-input=true
|
||||
}}
|
||||
{{markdown.editor}}
|
||||
</div>
|
||||
|
||||
{{#if markdown.isSplitScreen}}
|
||||
<div class="gh-markdown-editor-preview">
|
||||
<h1 class="gh-markdown-editor-preview-title">{{post.titleScratch}}</h1>
|
||||
<div class="gh-markdown-editor-preview-content"></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{gh-tour-item "using-the-editor"
|
||||
target=".gh-editor-footer"
|
||||
throbberAttachment="top left"
|
||||
throbberOffset="0 20%"
|
||||
popoverTriangleClass="bottom"
|
||||
}}
|
||||
{{/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>
|
||||
{{/if}}
|
||||
|
||||
{{gh-tour-item "using-the-editor"
|
||||
target=".gh-editor-footer"
|
||||
throbberAttachment="top left"
|
||||
throbberOffset="0 20%"
|
||||
popoverTriangleClass="bottom"
|
||||
}}
|
||||
{{/gh-markdown-editor}}
|
||||
{{!-- 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>
|
||||
|
||||
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}
|
||||
<footer class="gh-editor-footer"></footer>
|
||||
{{#each upload.errors as |error|}}
|
||||
<div class="failed">{{error.fileName}} - {{error.message}}</div>
|
||||
{{/each}}
|
||||
|
||||
{{!-- 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>
|
||||
{{/if}}
|
||||
<button class="gh-btn gh-btn-grey gh-btn-icon" {{action upload.cancel}}>
|
||||
<span>{{inline-svg "close"}} Close</span>
|
||||
</button>
|
||||
{{else}}
|
||||
|
||||
{{!-- 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}}
|
||||
<h3>Uploading {{pluralize upload.files.length "image"}}...</h3>
|
||||
{{upload.progressBar}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/gh-uploader}}
|
||||
{{/if}}
|
||||
{{/gh-uploader}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}} {{!-- end Koenig conditional --}}
|
||||
{{/gh-editor}}
|
||||
|
||||
{{#if showDeletePostModal}}
|
||||
|
@ -155,6 +155,21 @@
|
||||
{{/gh-uploader}}
|
||||
</div>
|
||||
|
||||
{{#if config.enableDeveloperExperiments}}
|
||||
<div class="gh-setting-header">⚠️ Developer-only Feature Testing ⚠️</div>
|
||||
<div class="gh-setting">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Koenig Editor</div>
|
||||
<div class="gh-setting-desc">
|
||||
Highly experimental (i.e. broken) editor. For developer use only.<br>
|
||||
<strong>Warning:</strong> Stories created or edited with Koenig will no longer be compatible with the old markdown editor.</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-checkbox">{{gh-feature-flag "koenigEditor"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
@ -149,6 +149,9 @@ 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']
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Component from '@ember/component';
|
||||
import counter from 'ghost-admin/utils/word-count';
|
||||
import formatMarkdown from 'ghost-admin/utils/format-markdown';
|
||||
import layout from '../../templates/components/card-markdown';
|
||||
import {
|
||||
UnsupportedMediaTypeError,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
isVersionMismatchError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import {computed} from '@ember/object';
|
||||
import {formatMarkdown} from '../../lib/format-markdown';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {isArray as isEmberArray} from '@ember/array';
|
||||
@ -25,7 +25,7 @@ export default Component.extend({
|
||||
extensions: null,
|
||||
|
||||
preview: computed('value', function () {
|
||||
return formatMarkdown([this.get('payload').markdown]);
|
||||
return formatMarkdown(this.get('payload').markdown);
|
||||
}),
|
||||
|
||||
// TODO: remove observer
|
||||
@ -47,7 +47,7 @@ export default Component.extend({
|
||||
|
||||
didReceiveAttrs() {
|
||||
if (!this.get('isEditing')) {
|
||||
this.set('preview', formatMarkdown([this.get('payload').markdown]));
|
||||
this.set('preview', formatMarkdown(this.get('payload').markdown));
|
||||
} else {
|
||||
run.next(() => {
|
||||
this.$('textarea').focus();
|
||||
|
@ -72,7 +72,7 @@ export default Component.extend({
|
||||
cards = defaultCards.concat(cards).map(card => createCard(card));
|
||||
|
||||
// add our default atoms
|
||||
atoms.concat([{
|
||||
atoms = atoms.concat([{
|
||||
name: 'soft-return',
|
||||
type: 'dom',
|
||||
render() {
|
||||
@ -117,6 +117,15 @@ export default Component.extend({
|
||||
this._startedRunLoop = false;
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.get('autofocus') !== this._autofocus) {
|
||||
this._autofocus = this.get('autofocus');
|
||||
this._hasAutofocused = false;
|
||||
}
|
||||
},
|
||||
|
||||
willRender() {
|
||||
// Use a default mobiledoc. If there are no changes, then return early.
|
||||
let mobiledoc = this.get('mobiledoc') || BLANK_DOC;
|
||||
@ -235,7 +244,7 @@ export default Component.extend({
|
||||
// the first lot of content is entered and we expect the caret to be at
|
||||
// the end of the document.
|
||||
// TODO: can this be removed if we refactor the new/edit screens to not re-render?
|
||||
if (this.get('autofocus')) {
|
||||
if (this._autofocus && !this._hasAutofocused) {
|
||||
let range = document.createRange();
|
||||
range.selectNodeContents(this.editor.element);
|
||||
range.collapse(false);
|
||||
@ -243,6 +252,10 @@ export default Component.extend({
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
editor._ensureFocus(); // PRIVATE API
|
||||
|
||||
// ensure we don't run the autofocus more than once between
|
||||
// `autofocus` attr changes
|
||||
this._hasAutofocused = true;
|
||||
}
|
||||
|
||||
this.processWordcount();
|
||||
|
@ -185,7 +185,7 @@ export default Component.extend({
|
||||
|
||||
// if the current paragraph is empty then the position is 0
|
||||
if (!cursorPositionOnScreen || cursorPositionOnScreen.top === 0) {
|
||||
if (editor.activeSection.renderNode) {
|
||||
if (editor.activeSection && editor.activeSection.renderNode) {
|
||||
cursorPositionOnScreen = editor.activeSection.renderNode.element.getBoundingClientRect();
|
||||
} else {
|
||||
this.setCursorAtOffset(0);
|
||||
|
@ -1,34 +0,0 @@
|
||||
/* global Showdown, html_sanitize*/
|
||||
import cajaSanitizers from './caja-sanitizers';
|
||||
import {helper} from '@ember/component/helper';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
let showdown = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']});
|
||||
|
||||
export function formatMarkdown(params) {
|
||||
if (!params || !params.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let markdown = params[0] || '';
|
||||
let escapedhtml = '';
|
||||
|
||||
// convert markdown to HTML
|
||||
escapedhtml = showdown.makeHtml(markdown);
|
||||
|
||||
// replace script and iFrame
|
||||
escapedhtml = escapedhtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
||||
'<pre class="js-embed-placeholder">Embedded JavaScript</pre>');
|
||||
escapedhtml = escapedhtml.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi,
|
||||
'<pre class="iframe-embed-placeholder">Embedded iFrame</pre>');
|
||||
|
||||
// sanitize html
|
||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||
escapedhtml = html_sanitize(escapedhtml, cajaSanitizers.url, cajaSanitizers.id);
|
||||
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
|
||||
|
||||
return htmlSafe(escapedhtml);
|
||||
}
|
||||
|
||||
export default helper(formatMarkdown);
|
@ -4,9 +4,7 @@
|
||||
ondrop={{action "didDrop"}}
|
||||
ondragover={{action "didDragOver"}}
|
||||
ondragleave={{action "didDragLeave"}}
|
||||
>
|
||||
{{value}}
|
||||
</textarea>
|
||||
>{{value}}</textarea>
|
||||
{{else}}
|
||||
{{{preview}}}
|
||||
{{/if}}
|
||||
|
@ -6,7 +6,7 @@
|
||||
],
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"ember-cli-babel": "5.2.4",
|
||||
"ember-cli-htmlbars": "1.1.1"
|
||||
"ember-cli-babel": "^6.11.0",
|
||||
"ember-cli-htmlbars": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +110,7 @@
|
||||
"markdown-it-lazy-headers": "0.1.3",
|
||||
"markdown-it-mark": "2.0.0",
|
||||
"matchdep": "2.0.0",
|
||||
"mobiledoc-kit": "0.10.20",
|
||||
"password-generator": "2.2.0",
|
||||
"postcss-color-function": "4.0.1",
|
||||
"postcss-custom-properties": "6.2.0",
|
||||
@ -121,7 +122,8 @@
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
"lib/asset-delivery"
|
||||
"lib/asset-delivery",
|
||||
"lib/gh-koenig"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ describe('Acceptance: Editor', function () {
|
||||
await datepickerSelect('[data-test-date-time-picker-datepicker]', validTime);
|
||||
|
||||
// hide psm
|
||||
await click('[data-test-psm-trigger]');
|
||||
await click('[data-test-close-settings-menu]');
|
||||
|
||||
// checking the flow of the saving button for a draft
|
||||
expect(
|
||||
|
@ -11,6 +11,7 @@ describe('Unit: Controller: editor', function () {
|
||||
setupTest('controller:editor', {
|
||||
needs: [
|
||||
'controller:application',
|
||||
'service:feature',
|
||||
'service:notifications',
|
||||
// 'service:router',
|
||||
'service:slugGenerator',
|
||||
|
@ -9,10 +9,13 @@ describe('Unit: Model: post', function () {
|
||||
'model:user',
|
||||
'model:tag',
|
||||
'model:role',
|
||||
'service:ajax',
|
||||
'service:clock',
|
||||
'service:config',
|
||||
'service:feature',
|
||||
'service:ghostPaths',
|
||||
'service:lazyLoader',
|
||||
'service:notifications',
|
||||
'service:session',
|
||||
'service:settings'
|
||||
]
|
||||
|
@ -10,9 +10,14 @@ describe('Unit: Serializer: post', function () {
|
||||
'transform:json-string',
|
||||
'model:user',
|
||||
'model:tag',
|
||||
'service:ajax',
|
||||
'service:clock',
|
||||
'service:config',
|
||||
'service:feature',
|
||||
'service:ghostPaths',
|
||||
'service:lazyLoader',
|
||||
'service:notifications',
|
||||
'service:session',
|
||||
'service:settings'
|
||||
]
|
||||
});
|
||||
|
@ -6835,6 +6835,21 @@ 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.20:
|
||||
version "0.10.20"
|
||||
resolved "https://registry.yarnpkg.com/mobiledoc-kit/-/mobiledoc-kit-0.10.20.tgz#2925a6223bac2a1eeb3468a4a94d992618089312"
|
||||
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"
|
||||
|
Loading…
Reference in New Issue
Block a user