🐛 Koenig - Fix embedding of multiple FB Videos

refs https://github.com/TryGhost/Ghost/issues/9623
- wrap all embeds in an `<iframe>` so that their scripts are isolated (fixes FB Video)
- add `MutationObserver` implementation to adjust iframe height as embed's content is loaded
- add `noframe.js` to resize embedded iframes such as YouTube videos
This commit is contained in:
Kevin Ansfield 2018-06-13 15:19:24 +01:00
parent f9b08d8d64
commit 57a66f7cdc
5 changed files with 140 additions and 36 deletions

View File

@ -138,6 +138,11 @@ module.exports = function (defaults) {
app.import('node_modules/mobiledoc-kit/dist/amd/mobiledoc-kit.js');
app.import('node_modules/mobiledoc-kit/dist/amd/mobiledoc-kit.map');
app.import('node_modules/simplemde/debug/simplemde.js');
app.import('node_modules/reframe.js/dist/noframe.es.js', {
using: [
{transformation: 'es6', as: 'noframe.js'}
]
});
// pull things we rely on via lazy-loading into the test-support.js file so
// that tests don't break when running via http://localhost:4200/tests

View File

@ -1,5 +1,6 @@
import Component from '@ember/component';
import layout from '../templates/components/koenig-card-embed';
import noframe from 'noframe.js';
import {NO_CURSOR_MOVEMENT} from './koenig-editor';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
@ -40,10 +41,22 @@ export default Component.extend({
didInsertElement() {
this._super(...arguments);
this._loadPayloadScript();
this._populateIframe();
this._focusInput();
},
willDestroyElement() {
this._super(...arguments);
run.cancel(this._resizeDebounce);
if (this._iframeMutationObserver) {
this._iframeMutationObserver.disconnect();
}
window.removeEventListener('resize', this._windowResizeHandler);
},
actions: {
onDeselect() {
if (this.payload.url && !this.payload.html && !this.hasError) {
@ -113,7 +126,7 @@ export default Component.extend({
set(this.payload, 'type', response.type);
this.saveCard(this.payload, false);
run.schedule('afterRender', this, this._loadPayloadScript);
run.schedule('afterRender', this, this._populateIframe);
} catch (err) {
this.set('hasError', true);
}
@ -127,44 +140,118 @@ export default Component.extend({
}
},
// some oembeds will have a script tag but it won't automatically run
// due to the way Ember renders the card components. Grab the script
// element and push a new one to force the browser to download+run it
_loadPayloadScript() {
let oldScript = this.element.querySelector('script');
if (oldScript) {
let parent = oldScript.parentElement;
let newScript = document.createElement('script');
newScript.type = 'text/javascript';
_populateIframe() {
let iframe = this.element.querySelector('iframe');
if (iframe) {
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(this.payload.html);
iframe.contentWindow.document.close();
if (oldScript.src) {
// hide the original embed html to avoid ugly transitions as the
// script runs (at least on reasonably good network and cpu)
let embedElement = this.element.querySelector('[data-kg-embed]');
embedElement.style.display = 'none';
iframe.contentDocument.body.style.display = 'flex';
iframe.contentDocument.body.style.margin = '0';
iframe.contentDocument.body.style.justifyContent = 'center';
newScript.src = oldScript.src;
// once the script has loaded, wait a little while for it to do it's
// thing before making everything visible again
newScript.onload = run.bind(this, function () {
run.later(this, function () {
embedElement.style.display = null;
}, 500);
});
newScript.onerror = run.bind(this, function () {
embedElement.style.display = null;
});
} else {
newScript.innerHTML = oldScript.innerHTML;
let nestedIframe = iframe.contentDocument.body.firstChild;
if (nestedIframe.nodeName === 'IFRAME') {
noframe(nestedIframe, '[data-kg-embed]');
this._resizeIframe(iframe);
}
oldScript.remove();
parent.appendChild(newScript);
this._iframeResizeHandler = run.bind(this, this._resizeIframe, iframe);
this._iframeMutationObserver = this._createMutationObserver(
iframe.contentWindow.document,
this._iframeResizeHandler
);
this._setupWindowResizeHandler(iframe);
}
},
_createMutationObserver(target, callback) {
function addImageLoadListeners(mutation) {
function addImageLoadListener(element) {
if (element.complete === false) {
element.addEventListener('load', imageEventTriggered, false);
element.addEventListener('error', imageEventTriggered, false);
imageElements.push(element);
}
}
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
addImageLoadListener(mutation.target);
} else if (mutation.type === 'childList') {
Array.prototype.forEach.call(
mutation.target.querySelectorAll('img'),
addImageLoadListener
);
}
}
function removeFromElements(element) {
imageElements.splice(imageElements.indexOf(element), 1);
}
function removeImageLoadListener(element) {
element.removeEventListener('load', imageEventTriggered, false);
element.removeEventListener('error', imageEventTriggered, false);
removeFromElements(element);
}
function imageEventTriggered(event) {
removeImageLoadListener(event.target);
callback();
}
function mutationObserved(mutations) {
callback();
// deal with async image loads when tags are injected into the page
mutations.forEach(addImageLoadListeners);
}
function createMutationObserver(target) {
let config = {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true
};
let observer = new MutationObserver(mutationObserved);
observer.observe(target, config); // eslint-disable-line ghost/ember/no-observers
return observer;
}
let imageElements = [];
let observer = createMutationObserver(target);
return {
disconnect() {
if ('disconnect' in observer) {
observer.disconnect(); // eslint-disable-line ghost/ember/no-observers
imageElements.forEach(removeImageLoadListener);
}
}
};
},
_resizeIframe(iframe) {
this._resizeDebounce = run.debounce(this, this.__debouncedResizeIframe, iframe, 66);
},
__debouncedResizeIframe(iframe) {
iframe.style.height = null;
let height = iframe.contentDocument.scrollingElement.scrollHeight;
iframe.style.height = `${height}px`;
},
_setupWindowResizeHandler(iframe) {
this._windowResizeHandler = run.bind(this, this._resizeIframe, iframe);
window.addEventListener('resize', this._windowResizeHandler, {passive: true});
},
_deleteIfEmpty() {
if (isBlank(this.payload.html) && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);

View File

@ -1,5 +1,5 @@
{{#koenig-card
class="flex flex-column"
class="flex flex-column nl2 nr2"
isSelected=isSelected
isEditing=isEditing
selectCard=(action selectCard)
@ -17,7 +17,7 @@
{{#if payload.html}}
<div class="kg-card-hover">
<div class="koenig-embed-{{payload.type}} flex justify-center relative" data-kg-embed>
{{{payload.html}}}
<iframe class="bn miw-100" scrolling="no"></iframe>
<div class="koenig-card-click-overlay ba b--white" data-kg-overlay></div>
</div>
@ -54,4 +54,4 @@
onkeydown={{action "urlKeydown"}}>
{{/if}}
{{/if}}
{{/koenig-card}}
{{/koenig-card}}

View File

@ -52,6 +52,7 @@
"ember-cli-cjs-transform": "1.3.0",
"ember-cli-code-coverage": "0.4.2",
"ember-cli-dependency-checker": "2.1.1",
"ember-cli-es6-transform": "^0.0.3",
"ember-cli-eslint": "4.2.3",
"ember-cli-ghost-spirit": "0.0.6",
"ember-cli-htmlbars": "2.0.3",
@ -115,6 +116,7 @@
"mobiledoc-kit": "0.10.21",
"normalize.css": "3.0.3",
"password-generator": "2.2.0",
"reframe.js": "2.2.1",
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost",
"testem": "2.6.0",
"top-gh-contribs": "2.0.4",

View File

@ -3369,6 +3369,12 @@ ember-cli-dependency-checker@2.1.1:
resolve "^1.5.0"
semver "^5.3.0"
ember-cli-es6-transform@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/ember-cli-es6-transform/-/ember-cli-es6-transform-0.0.3.tgz#99238305c72f533cc1cd3c85b15c6d7a842b8fdf"
dependencies:
ember-cli-babel "^6.6.0"
ember-cli-eslint@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/ember-cli-eslint/-/ember-cli-eslint-4.2.3.tgz#2844d3f5e8184f19b2d7132ba99eb0b370b55598"
@ -8632,6 +8638,10 @@ reduce-css-calc@^2.0.0:
css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0"
reframe.js@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/reframe.js/-/reframe.js-2.2.1.tgz#c4df52c815152b57458843d53d0246cb6b954d59"
regenerate@^1.2.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"