diff --git a/ghost/admin/app/components/gh-ed-preview.js b/ghost/admin/app/components/gh-ed-preview.js index 0472e26061..bd7115dcaf 100644 --- a/ghost/admin/app/components/gh-ed-preview.js +++ b/ghost/admin/app/components/gh-ed-preview.js @@ -1,7 +1,7 @@ import Ember from 'ember'; +import {formatMarkdown} from 'ghost/helpers/gh-format-markdown'; const { - $, Component, run, uuid @@ -10,16 +10,18 @@ const { export default Component.extend({ _scrollWrapper: null, + previewHTML: '', + init() { this._super(...arguments); this.set('imageUploadComponents', Ember.A([])); + this.buildPreviewHTML(); }, didInsertElement() { this._super(...arguments); this._scrollWrapper = this.$().closest('.entry-preview-content'); this.adjustScrollPosition(this.get('scrollPosition')); - run.scheduleOnce('afterRender', this, this.registerImageUploadComponents); }, didReceiveAttrs(attrs) { @@ -34,14 +36,7 @@ export default Component.extend({ } if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) { - // we need to clear the rendered components as we are unable to - // 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); + run.throttle(this, this.buildPreviewHTML, 30, false); } }, @@ -53,29 +48,54 @@ export default Component.extend({ } }, - registerImageUploadComponents() { - let dropzones = $('.js-drop-zone'); + buildPreviewHTML() { + 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 destinationElementId = `image-uploader-${id}`; - let src = $(el).find('.js-upload-target').attr('src'); + let src; - let imageUpload = Ember.Object.create({ - destinationElementId, - id, - src, - index: i - }); + if (uploadTarget) { + src = uploadTarget.getAttribute('src'); + } + + 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).empty(); - $(el).removeClass('image-uploader'); + el.innerHTML = ''; + el.classList.remove('image-uploader'); - run.schedule('afterRender', () => { - this.get('imageUploadComponents').pushObject(imageUpload); - }); + fragment.replaceChild(el, oldEl); }); + + this.set('previewHTML', fragment); }, actions: { diff --git a/ghost/admin/app/helpers/gh-format-markdown.js b/ghost/admin/app/helpers/gh-format-markdown.js index 5124227aa5..7e09cddbf8 100644 --- a/ghost/admin/app/helpers/gh-format-markdown.js +++ b/ghost/admin/app/helpers/gh-format-markdown.js @@ -6,7 +6,7 @@ const {Helper} = Ember; let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']}); -export default Helper.helper(function (params) { +export function formatMarkdown(params) { if (!params || !params.length) { return; } @@ -29,4 +29,6 @@ export default Helper.helper(function (params) { // jscs:enable requireCamelCaseOrUpperCaseIdentifiers return Ember.String.htmlSafe(escapedhtml); -}); +} + +export default Helper.helper(formatMarkdown); diff --git a/ghost/admin/app/mixins/ed-editor-api.js b/ghost/admin/app/mixins/ed-editor-api.js index e314d0a9fe..64aa610fae 100644 --- a/ghost/admin/app/mixins/ed-editor-api.js +++ b/ghost/admin/app/mixins/ed-editor-api.js @@ -1,6 +1,9 @@ import Ember from 'ember'; -const {Mixin} = Ember; +const { + Mixin, + run +} = Ember; export default Mixin.create({ /** @@ -11,7 +14,7 @@ export default Mixin.create({ * @returns {String} */ getValue() { - return this.$().val(); + return this.readDOMAttr('value'); }, /** @@ -111,27 +114,29 @@ export default Mixin.create({ * by providing selectionEnd. */ replaceSelection(replacement, replacementStart, replacementEnd, cursorPosition) { - let $textarea = this.$(); + run.schedule('afterRender', this, function () { + let $textarea = this.$(); - cursorPosition = cursorPosition || 'collapseToEnd'; - replacementEnd = replacementEnd || replacementStart; + cursorPosition = cursorPosition || 'collapseToEnd'; + replacementEnd = replacementEnd || replacementStart; - $textarea.setSelection(replacementStart, replacementEnd); + $textarea.setSelection(replacementStart, replacementEnd); - if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) { - $textarea.replaceSelectedText(replacement, cursorPosition); - } else { - $textarea.replaceSelectedText(replacement); - if (cursorPosition.hasOwnProperty('start')) { - $textarea.setSelection(cursorPosition.start, cursorPosition.end); + if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) { + $textarea.replaceSelectedText(replacement, cursorPosition); } 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(); - // Tell the editor it has changed, as programmatic replacements won't trigger this automatically - this._elementValueDidChange(); - this.sendAction('onChange'); + $textarea.focus(); + // Tell the editor it has changed, as programmatic replacements won't trigger this automatically + this._elementValueDidChange(); + this.sendAction('onChange'); + }); } }); diff --git a/ghost/admin/app/templates/components/gh-ed-preview.hbs b/ghost/admin/app/templates/components/gh-ed-preview.hbs index f08fc949ca..e54e56c1d3 100644 --- a/ghost/admin/app/templates/components/gh-ed-preview.hbs +++ b/ghost/admin/app/templates/components/gh-ed-preview.hbs @@ -1,4 +1,4 @@ -{{gh-format-markdown markdown}} +{{previewHTML}} {{#each imageUploadComponents as |uploader|}} {{#ember-wormhole to=uploader.destinationElementId}} diff --git a/ghost/admin/app/utils/ed-image-manager.js b/ghost/admin/app/utils/ed-image-manager.js index 9178aa94ec..539e8b976e 100644 --- a/ghost/admin/app/utils/ed-image-manager.js +++ b/ghost/admin/app/utils/ed-image-manager.js @@ -18,7 +18,7 @@ function getSrcRange(content, index) { let replacement = {}; 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 // a little differently if (images[index][2] === undefined) {