0
Fork 0
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:
Kevin Ansfield 2016-05-08 11:20:26 +02:00
parent e32106505a
commit 02b57750cf
5 changed files with 74 additions and 47 deletions

View file

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

View file

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

View file

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

View file

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

View file

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