mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Fix editor image perf and upload bug
no issue - ~10x speedup in processing time taken on each keypress when there are many images/image upload components in the editor - edit DOM in memory before changing it in the page to avoid double-render - keep upload components around and re-assign them on re-render, adding or removing an image will still re-generate everything - adds a throttle to the preview rendering so that renders don't get queued up - fixes occasional bug where uploading an image didn't update the markdown correctly due to a timing issue
This commit is contained in:
parent
e32106505a
commit
02b57750cf
5 changed files with 74 additions and 47 deletions
|
@ -1,7 +1,7 @@
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
import {formatMarkdown} from 'ghost/helpers/gh-format-markdown';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
$,
|
|
||||||
Component,
|
Component,
|
||||||
run,
|
run,
|
||||||
uuid
|
uuid
|
||||||
|
@ -10,16 +10,18 @@ const {
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
_scrollWrapper: null,
|
_scrollWrapper: null,
|
||||||
|
|
||||||
|
previewHTML: '',
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('imageUploadComponents', Ember.A([]));
|
this.set('imageUploadComponents', Ember.A([]));
|
||||||
|
this.buildPreviewHTML();
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
||||||
this.adjustScrollPosition(this.get('scrollPosition'));
|
this.adjustScrollPosition(this.get('scrollPosition'));
|
||||||
run.scheduleOnce('afterRender', this, this.registerImageUploadComponents);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
didReceiveAttrs(attrs) {
|
didReceiveAttrs(attrs) {
|
||||||
|
@ -34,14 +36,7 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
|
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
|
||||||
// we need to clear the rendered components as we are unable to
|
run.throttle(this, this.buildPreviewHTML, 30, false);
|
||||||
// retain a reliable reference for the component's position in the
|
|
||||||
// document
|
|
||||||
// TODO: it may be possible to extract the dropzones and use the
|
|
||||||
// image src as a key, re-connecting any that match and
|
|
||||||
// dropping/re-rendering any unknown/no-source instances
|
|
||||||
this.set('imageUploadComponents', Ember.A([]));
|
|
||||||
run.scheduleOnce('afterRender', this, this.registerImageUploadComponents);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -53,29 +48,54 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
registerImageUploadComponents() {
|
buildPreviewHTML() {
|
||||||
let dropzones = $('.js-drop-zone');
|
let markdown = this.get('markdown');
|
||||||
|
let html = formatMarkdown([markdown]).string;
|
||||||
|
let template = document.createElement('template');
|
||||||
|
template.innerHTML = html;
|
||||||
|
let fragment = template.content;
|
||||||
|
let dropzones = fragment.querySelectorAll('.js-drop-zone');
|
||||||
|
let components = this.get('imageUploadComponents');
|
||||||
|
|
||||||
dropzones.each((i, el) => {
|
if (dropzones.length !== components.length) {
|
||||||
|
components = Ember.A([]);
|
||||||
|
this.set('imageUploadComponents', components);
|
||||||
|
}
|
||||||
|
|
||||||
|
[...dropzones].forEach((oldEl, i) => {
|
||||||
|
let el = oldEl.cloneNode(true);
|
||||||
|
let component = components[i];
|
||||||
|
let uploadTarget = el.querySelector('.js-upload-target');
|
||||||
let id = uuid();
|
let id = uuid();
|
||||||
let destinationElementId = `image-uploader-${id}`;
|
let destinationElementId = `image-uploader-${id}`;
|
||||||
let src = $(el).find('.js-upload-target').attr('src');
|
let src;
|
||||||
|
|
||||||
let imageUpload = Ember.Object.create({
|
if (uploadTarget) {
|
||||||
destinationElementId,
|
src = uploadTarget.getAttribute('src');
|
||||||
id,
|
}
|
||||||
src,
|
|
||||||
index: i
|
if (component) {
|
||||||
});
|
component.set('destinationElementId', destinationElementId);
|
||||||
|
component.set('src', src);
|
||||||
|
} else {
|
||||||
|
let imageUpload = Ember.Object.create({
|
||||||
|
destinationElementId,
|
||||||
|
id,
|
||||||
|
src,
|
||||||
|
index: i
|
||||||
|
});
|
||||||
|
|
||||||
|
this.get('imageUploadComponents').pushObject(imageUpload);
|
||||||
|
}
|
||||||
|
|
||||||
el.id = destinationElementId;
|
el.id = destinationElementId;
|
||||||
$(el).empty();
|
el.innerHTML = '';
|
||||||
$(el).removeClass('image-uploader');
|
el.classList.remove('image-uploader');
|
||||||
|
|
||||||
run.schedule('afterRender', () => {
|
fragment.replaceChild(el, oldEl);
|
||||||
this.get('imageUploadComponents').pushObject(imageUpload);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.set('previewHTML', fragment);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {Helper} = Ember;
|
||||||
|
|
||||||
let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
|
let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
|
||||||
|
|
||||||
export default Helper.helper(function (params) {
|
export function formatMarkdown(params) {
|
||||||
if (!params || !params.length) {
|
if (!params || !params.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,6 @@ export default Helper.helper(function (params) {
|
||||||
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
|
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
|
||||||
|
|
||||||
return Ember.String.htmlSafe(escapedhtml);
|
return Ember.String.htmlSafe(escapedhtml);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default Helper.helper(formatMarkdown);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
const {Mixin} = Ember;
|
const {
|
||||||
|
Mixin,
|
||||||
|
run
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Mixin.create({
|
export default Mixin.create({
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +14,7 @@ export default Mixin.create({
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
getValue() {
|
getValue() {
|
||||||
return this.$().val();
|
return this.readDOMAttr('value');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,27 +114,29 @@ export default Mixin.create({
|
||||||
* by providing selectionEnd.
|
* by providing selectionEnd.
|
||||||
*/
|
*/
|
||||||
replaceSelection(replacement, replacementStart, replacementEnd, cursorPosition) {
|
replaceSelection(replacement, replacementStart, replacementEnd, cursorPosition) {
|
||||||
let $textarea = this.$();
|
run.schedule('afterRender', this, function () {
|
||||||
|
let $textarea = this.$();
|
||||||
|
|
||||||
cursorPosition = cursorPosition || 'collapseToEnd';
|
cursorPosition = cursorPosition || 'collapseToEnd';
|
||||||
replacementEnd = replacementEnd || replacementStart;
|
replacementEnd = replacementEnd || replacementStart;
|
||||||
|
|
||||||
$textarea.setSelection(replacementStart, replacementEnd);
|
$textarea.setSelection(replacementStart, replacementEnd);
|
||||||
|
|
||||||
if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) {
|
if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) {
|
||||||
$textarea.replaceSelectedText(replacement, cursorPosition);
|
$textarea.replaceSelectedText(replacement, cursorPosition);
|
||||||
} else {
|
|
||||||
$textarea.replaceSelectedText(replacement);
|
|
||||||
if (cursorPosition.hasOwnProperty('start')) {
|
|
||||||
$textarea.setSelection(cursorPosition.start, cursorPosition.end);
|
|
||||||
} else {
|
} else {
|
||||||
$textarea.setSelection(cursorPosition, cursorPosition);
|
$textarea.replaceSelectedText(replacement);
|
||||||
|
if (cursorPosition.hasOwnProperty('start')) {
|
||||||
|
$textarea.setSelection(cursorPosition.start, cursorPosition.end);
|
||||||
|
} else {
|
||||||
|
$textarea.setSelection(cursorPosition, cursorPosition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$textarea.focus();
|
$textarea.focus();
|
||||||
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
||||||
this._elementValueDidChange();
|
this._elementValueDidChange();
|
||||||
this.sendAction('onChange');
|
this.sendAction('onChange');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{{gh-format-markdown markdown}}
|
{{previewHTML}}
|
||||||
|
|
||||||
{{#each imageUploadComponents as |uploader|}}
|
{{#each imageUploadComponents as |uploader|}}
|
||||||
{{#ember-wormhole to=uploader.destinationElementId}}
|
{{#ember-wormhole to=uploader.destinationElementId}}
|
||||||
|
|
|
@ -18,7 +18,7 @@ function getSrcRange(content, index) {
|
||||||
let replacement = {};
|
let replacement = {};
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
// [1] matches the alt test, and 2 matches the url between the ()
|
// [1] matches the alt text, and 2 matches the url between the ()
|
||||||
// if the () are missing entirely, which is valid, [2] will be undefined and we'll need to treat this case
|
// if the () are missing entirely, which is valid, [2] will be undefined and we'll need to treat this case
|
||||||
// a little differently
|
// a little differently
|
||||||
if (images[index][2] === undefined) {
|
if (images[index][2] === undefined) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue