diff --git a/app/html/src/client/preview/helpers/simulate-pageload.ts b/app/html/src/client/preview/helpers/simulate-pageload.ts new file mode 100644 index 00000000000..f68b570b0bb --- /dev/null +++ b/app/html/src/client/preview/helpers/simulate-pageload.ts @@ -0,0 +1,97 @@ +import { document } from 'global'; + +// https://html.spec.whatwg.org/multipage/scripting.html +const runScriptTypes = [ + 'application/javascript', + 'application/ecmascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript', +]; + +const SCRIPT = 'script'; +const SCRIPTS_ROOT_ID = 'scripts-root'; + +// trigger DOMContentLoaded +export function simulateDOMContentLoaded() { + const DOMContentLoadedEvent = document.createEvent('Event'); + DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true); + document.dispatchEvent(DOMContentLoadedEvent); +} + +function insertScript($script: any, callback: any, $scriptRoot: any) { + const scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + if ($script.src) { + scriptEl.onload = callback; + scriptEl.onerror = callback; + scriptEl.src = $script.src; + } else { + scriptEl.textContent = $script.innerText; + } + + // re-insert the script tag so it executes. + if ($scriptRoot) $scriptRoot.appendChild(scriptEl); + else document.head.appendChild(scriptEl); + + // clean-up + $script.parentNode.removeChild($script); + + // run the callback immediately for inline scripts + if (!$script.src) callback(); +} + +// runs an array of async functions in sequential order +/* eslint-disable no-param-reassign, no-plusplus */ +function insertScriptsSequentially(scriptsToExecute: any[], callback: any, index: number = 0) { + scriptsToExecute[index](() => { + index++; + if (index === scriptsToExecute.length) { + callback(); + } else { + insertScriptsSequentially(scriptsToExecute, callback, index); + } + }); +} + +export function simulatePageLoad($container: any) { + let $scriptsRoot = document.getElementById(SCRIPTS_ROOT_ID); + if (!$scriptsRoot) { + $scriptsRoot = document.createElement('div'); + $scriptsRoot.id = SCRIPTS_ROOT_ID; + document.body.appendChild($scriptsRoot); + } else { + $scriptsRoot.innerHTML = ''; + } + const $scripts = Array.from($container.querySelectorAll(SCRIPT)); + + if ($scripts.length) { + const scriptsToExecute: any[] = []; + $scripts.forEach(($script: any) => { + const typeAttr = $script.getAttribute('type'); + + // only run script tags without the type attribute + // or with a javascript mime attribute value + if (!typeAttr || !runScriptTypes.includes(typeAttr)) { + scriptsToExecute.push((callback: any) => insertScript($script, callback, $scriptsRoot)); + } + }); + + // insert the script tags sequentially + // to preserve execution order + insertScriptsSequentially(scriptsToExecute, simulateDOMContentLoaded, undefined); + } else { + simulateDOMContentLoaded(); + } +} diff --git a/app/html/src/client/preview/render.ts b/app/html/src/client/preview/render.ts index a885d4d9ad9..66b93aa660d 100644 --- a/app/html/src/client/preview/render.ts +++ b/app/html/src/client/preview/render.ts @@ -1,6 +1,7 @@ import { document, Node } from 'global'; import dedent from 'ts-dedent'; import { RenderContext } from './types'; +import { simulatePageLoad, simulateDOMContentLoaded } from './helpers/simulate-pageload'; const rootElement = document.getElementById('root'); @@ -17,6 +18,7 @@ export default function renderMain({ showMain(); if (typeof element === 'string') { rootElement.innerHTML = element; + simulatePageLoad(rootElement); } else if (element instanceof Node) { // Don't re-mount the element if it didn't change and neither did the story if (rootElement.firstChild === element && forceRender === true) { @@ -25,6 +27,7 @@ export default function renderMain({ rootElement.innerHTML = ''; rootElement.appendChild(element); + simulateDOMContentLoaded(); } else { showError({ title: `Expecting an HTML snippet or DOM node from the story: "${name}" of "${kind}".`, diff --git a/examples/html-kitchen-sink/stories/button.stories.js b/examples/html-kitchen-sink/stories/button.stories.js index bf4c9ea5f99..efaafae5548 100644 --- a/examples/html-kitchen-sink/stories/button.stories.js +++ b/examples/html-kitchen-sink/stories/button.stories.js @@ -24,3 +24,6 @@ export const Effect = () => { return ''; }; + +export const Script = () => + '