";
+ const expectedCSS = "address { font-variant-caps: small-caps;}a { font-variant: normal;}";
+ const expectedHTML = " James Rockford 2354 Pacific Coast Highway " +
+ "California USA +311-555-2368 Email: " +
+ 'j.rockford@example.com ' +
+ "";
- await page.waitForSelector("#output");
+ await page.waitForSelector("#output-iframe");
+ const outputIframe = await page.$('#output-iframe');
+ const iframeContent = await outputIframe.contentFrame();
- let outputContent = await page.$eval("#output shadow-output", (elem) =>
- /* trim new lines, then trim matches of two or more consecutive
+ await iframeContent.waitForSelector('#html-output');
+
+ const trimInnerHTML = (elem) =>
+ /* trim new lines, then trim matches of two or more consecutive
whitespace characters with a single whitespace character */
- elem.shadowRoot.innerHTML
- .replace(/\r?\n|\r/g, "")
- .replace(/\s{2,}/g, " ")
- );
- await expect(outputContent).toBe(expectedOutput);
+ elem.innerHTML
+ .replace(/\r?\n|\r/g, "")
+ .replace(/\s{2,}/g, " ");
+
+ let htmlOutputContent = await iframeContent.$eval("#html-output", trimInnerHTML);
+ await expect(htmlOutputContent).toBe(expectedHTML);
+
+ let cssOutputContent = await iframeContent.$eval("#css-output", trimInnerHTML);
+ await expect(cssOutputContent).toBe(expectedCSS);
});
it("should switch to CSS editor on tab click", async () => {
diff --git a/editor/css/tabbed-editor.css b/editor/css/tabbed-editor.css
index c87ef9f95..4c1184e26 100644
--- a/editor/css/tabbed-editor.css
+++ b/editor/css/tabbed-editor.css
@@ -25,22 +25,10 @@
border-bottom: 1px solid var(--border-primary);
}
-.shadow-container {
+.output-container {
position: relative;
}
-/* For non shadow-dom supporting browsers */
-.output.style-scope {
- background-color: var(--background-primary);
- border-left: 1px solid var(--border-primary);
- font-size: 0.9rem;
- height: 342px;
- line-height: 1.5;
- margin: 0;
- overflow: scroll;
- padding: 1rem;
-}
-
.tabs {
font-size: 0.9rem;
flex: none;
@@ -144,7 +132,7 @@
height: 580px;
}
- .shadow-container {
+ .output-container {
width: 40%;
height: 100%;
border-left: 1px solid var(--border-primary);
@@ -156,3 +144,24 @@
border-bottom: 0 none;
}
}
+
+#output-iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Those rules apply to content inside iframe, where example is shown */
+#output-root body {
+ background-color: #fff;
+}
+
+#html-output {
+ color: #15141aff;
+ font-size: 0.9rem;
+ overflow: auto;
+ line-height: 1.5;
+ padding: 2rem 1rem 1rem;
+ height: min-content; /* This value ensures correct calculation of clientHeight in /pages/tabbed/img */
+ width: fit-content; /* This value moves horizontal scroll bar to the bottom of iframe */
+}
diff --git a/editor/js/editor-libs/console.js b/editor/js/editor-libs/console.js
index cfb3a64b4..a1319ccb7 100644
--- a/editor/js/editor-libs/console.js
+++ b/editor/js/editor-libs/console.js
@@ -1,7 +1,10 @@
import { writeOutput, formatOutput } from "./console-utils.js";
// Thanks in part to https://stackoverflow.com/questions/11403107/capturing-javascript-console-log
-export default function () {
+export default function (targetWindow) {
+ /* Getting reference to console, either from current window or from the iframe window */
+ var console = targetWindow ? targetWindow.console : window.console;
+
var originalConsoleLogger = console.log; // eslint-disable-line no-console
var originalConsoleError = console.error;
diff --git a/editor/js/editor-libs/mce-utils.js b/editor/js/editor-libs/mce-utils.js
index 81a223149..d2ed5d3fb 100644
--- a/editor/js/editor-libs/mce-utils.js
+++ b/editor/js/editor-libs/mce-utils.js
@@ -47,8 +47,8 @@ export function isPropertySupported(dataset) {
/**
* Interrupts the default click event on external links inside
- * the shadow dom and opens them in a new tab instead
- * @param {Array} externalLinks - all external links inside the shadow dom
+ * the iframe and opens them in a new tab instead
+ * @param {Array} externalLinks - all external links inside the iframe
*/
export function openLinksInNewTab(externalLinks) {
externalLinks.forEach(function (externalLink) {
@@ -61,15 +61,15 @@ export function openLinksInNewTab(externalLinks) {
/**
* Interrupts the default click event on relative links inside
- * the shadow dom and scrolls to the targeted anchor
- * @param {Object} shadow - the shadow dom root
- * @param {Array} relativeLinks - all relative links inside the shadow dom
+ * the iframe and scrolls to the targeted anchor
+ * @param {Object} rootElement - root or body element, that contains referenced links
+ * @param {Array} relativeLinks - all relative links inside the iframe
*/
-export function scrollToAnchors(shadow, relativeLinks) {
+export function scrollToAnchors(rootElement, relativeLinks) {
relativeLinks.forEach(function (relativeLink) {
relativeLink.addEventListener("click", function (event) {
event.preventDefault();
- shadow.querySelector(relativeLink.hash).scrollIntoView();
+ rootElement.querySelector(relativeLink.hash).scrollIntoView();
});
});
}
diff --git a/editor/js/editor-libs/shadow-output.js b/editor/js/editor-libs/shadow-output.js
deleted file mode 100644
index 71d89a95b..000000000
--- a/editor/js/editor-libs/shadow-output.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Initialise a custom output element
- * wrapped in a ShadowDOM container.
- */
-class ShadowOutput extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({ mode: "open" });
- }
-
- connectedCallback() {
- ShadyCSS.styleElement(this);
- }
-}
-
-export default ShadowOutput;
diff --git a/editor/js/editor-libs/template-utils.js b/editor/js/editor-libs/template-utils.js
deleted file mode 100644
index 5ff8ac7d1..000000000
--- a/editor/js/editor-libs/template-utils.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Return the base style rules for the output class
- * @returns base style rules for the output class
- */
-export function getOutputBaseStyle() {
- return ".output{background-color:#fff;color:#15141aff;font-size:0.9rem;line-height:1.5;overflow:scroll;padding:2rem 1rem 1rem;height:100%;}";
-}
-
-/**
- * Return the base script to inject into the shadowDOM
- * @returns base JavaScript util which return the `shadowRoot`
- */
-export function getBaseJS() {
- return "function getShadowRoot() { return document.querySelector('shadow-output').shadowRoot; }";
-}
-
-/**
- * Get the template element and return its content
- * @returns The .content of the template element
- */
-export function getTemplateOutput() {
- return document.getElementById("code_tmpl").content;
-}
-
-/**
- * Create a template element and populate it with the content of
- * the editor panes. If native shadowDOM is not supported, it uses
- * ShadyCSS to prepare the template before it is injected into
- * the shadowDOM element.
- * @param {Object} contents - The content from the editor panes
- * Example
- * --------
- * {
- * cssContent: 'h1 { background-color: #333; }',
- * htmlContent: '
Title
'
- * }
- */
-export function createTemplate(contents) {
- var html = document.createElement("div");
- var output = document.getElementById("output");
- var previousTmpl = document.getElementById("code_tmpl");
- var outputStyleElem = document.createElement("style");
- var styleElem = document.createElement("style");
- var tmpl = document.createElement("template");
-
- /* First remove the existing template if it exists.
- This ensures that prepareTemplate will process
- the template. */
- if (previousTmpl) {
- output.removeChild(previousTmpl);
- }
-
- tmpl.setAttribute("id", "code_tmpl");
- output.appendChild(tmpl);
-
- outputStyleElem.textContent = getOutputBaseStyle();
- styleElem.textContent = contents.cssContent;
- html.classList.add("output");
- html.innerHTML = contents.htmlContent;
-
- tmpl.content.appendChild(outputStyleElem);
- tmpl.content.appendChild(styleElem);
- tmpl.content.appendChild(html);
-
- if (contents.jsContent) {
- var jsUtilElem = document.createElement("script");
- var jsElem = document.createElement("script");
-
- jsUtilElem.textContent = getBaseJS();
- /* wrap the example JS in an IIFE to avoid collisions with variables,
- functions etc. in the larger page scope */
- jsElem.textContent = `(function() { 'use strict'; ${contents.jsContent} })();`;
-
- tmpl.content.appendChild(jsUtilElem);
- tmpl.content.appendChild(jsElem);
- }
-
- if (typeof ShadyDOM !== "undefined") {
- ShadyCSS.prepareTemplate(tmpl, "shadow-output");
- }
-}
diff --git a/editor/js/editor.js b/editor/js/editor.js
index 80747f1bb..a7f2c2ca2 100644
--- a/editor/js/editor.js
+++ b/editor/js/editor.js
@@ -2,8 +2,6 @@ import wcb from "@webcomponents/webcomponentsjs";
import mceConsole from "./editor-libs/console.js";
import * as mceEvents from "./editor-libs/events.js";
import * as mceUtils from "./editor-libs/mce-utils.js";
-import shadowOutput from "./editor-libs/shadow-output.js";
-import * as templateUtils from "./editor-libs/template-utils.js";
import * as tabby from "./editor-libs/tabby.js";
import "../css/editor-libs/ui-fonts.css";
@@ -16,14 +14,50 @@ import "../css/tabbed-editor.css";
var cssEditor = document.getElementById("css-editor");
var clearConsole = document.getElementById("clear");
var editorContainer = document.getElementById("editor-container");
+ var tabContainer = document.getElementById("tab-container");
+ var iframeContainer = document.getElementById("output");
var header = document.querySelector(".output-header");
var htmlEditor = document.getElementById("html-editor");
var jsEditor = document.getElementById("js-editor");
var staticCSSCode = cssEditor.querySelector("pre");
var staticHTMLCode = htmlEditor.querySelector("pre");
var staticJSCode = jsEditor.querySelector("pre");
+ var outputIFrame = document.getElementById("output-iframe");
+ var outputTemplate = getOutputTemplate();
+ var appliedHeightAdjustment = false;
var timer;
+ /**
+ * @returns {string} - Interactive example output template, formed by joining together contents of #output-head and #output-body, found in live-tabbed-tmpl.html
+ */
+ function getOutputTemplate() {
+ /* Document is split into two templates, just because parser omits , and tags.*/
+ var templateOutputHead = document.getElementById("output-head");
+ var templateOutputBody = document.getElementById("output-body");
+
+ return `
+
+
+${templateOutputHead.innerHTML}
+${templateOutputBody.innerHTML}
+
+`;
+ }
+
+ /**
+ * Applies CSS, HTML & JavaScript content found in the editor, to the output template
+ * @param outputTemplate - HTML page containing %css-content%, %html-content% and %js-content% texts, that will be replaced with actual values
+ * @param outputData - Object holding CSS, HTML & JavaScript code, that is supposed to be applied on the template
+ * @returns {string} - raw html string
+ */
+ function applyEditorContentToTemplate(outputTemplate, outputData) {
+ var content = outputTemplate;
+ content = content.replace("%css-content%", outputData.cssContent);
+ content = content.replace("%html-content%", outputData.htmlContent);
+ content = content.replace("%js-content%", outputData.jsContent);
+ return content;
+ }
+
/**
* Called by the tabbed editor to combine code from all tabs in an Object
* @returns Object with code from each tab panel
@@ -53,35 +87,72 @@ import "../css/tabbed-editor.css";
}
/**
- * Set or update the CSS and HTML in the output pane.
- * @param {Object} content - The content of the template element.
+ * Fetches HTML, CSS & JavaScript code from tabbed editor, applies them to output template and updates iframe with new content
*/
- function render(content) {
- let shadow = document.querySelector("shadow-output").shadowRoot;
- let shadowChildren = shadow.children;
+ function refreshOutput() {
+ var editorData = getOutput();
+ var content = applyEditorContentToTemplate(outputTemplate, editorData);
- if (shadowChildren.length) {
- if (typeof ShadyDOM !== "undefined" && ShadyDOM.inUse) {
- shadow.innerHTML = "";
- } else {
- var output = shadow.querySelector(".output");
- output && shadow.removeChild(output);
- var styleElements = shadow.querySelectorAll("style");
-
- for (var styleElement in styleElements) {
- if (
- styleElements.hasOwnProperty(styleElement) &&
- styleElements[styleElement]
- ) {
- shadow.removeChild(styleElements[styleElement]);
- }
- }
+ outputIFrame.srcdoc = content;
+ /* Some time after this operation, browser will invoke load event*/
+ }
+
+ /**
+ * Performs operations on iframe content, that was just loaded and shown to the user
+ * It prepares links, handles URL fragments, adjusts frame height, hooks console logs and then evaluates JS editor code
+ */
+ function onOutputLoaded() {
+ var contentWindow = outputIFrame.contentWindow;
+ var contentBody = contentWindow.document.body;
+
+ mceUtils.openLinksInNewTab(contentBody.querySelectorAll('a[href^="http"]'));
+ mceUtils.scrollToAnchors(contentBody, contentBody.querySelectorAll('a[href^="#"]'));
+
+ adjustFrameHeight();
+
+ /* Listeners are removed, every time content is refreshed */
+ contentWindow.addEventListener('resize', function () {
+ adjustFrameHeight();
+ });
+ /* Hooking console logs */
+ mceConsole(outputIFrame.contentWindow);
+
+ executeJSEditorCode();
+ }
+
+ /**
+ * Executing content of JavaScript editor, passed to global function "executeExample" declared in live-tabbed-tmpl.html
+ * This process is purposefully delayed, so console logs hooks are attached first
+ */
+ function executeJSEditorCode() {
+ outputIFrame.contentWindow.executeExample();
+ }
+
+ /**
+ * When screen width is small and output iframe is located below tab container, this function adjusts iframe height to height of its content
+ * When viewport width changes and iframe gets relocated to near tab container, height value set here is removed
+ */
+ function adjustFrameHeight() {
+ var iframeBelowTabContainer = iframeContainer.offsetTop >= tabContainer.offsetTop + tabContainer.offsetHeight;
+ if(iframeBelowTabContainer) {
+ /* When iframe is below tab container(which happens on small screens), we want it to take as low amount of space as possible */
+ let iframeContent = outputIFrame.contentWindow.document.getElementById("html-output");
+ let iframeContentHeight = iframeContent.clientHeight;
+ let iframeHeight = outputIFrame.clientHeight;
+
+ /* Setting height of iframe to be the same as height of its content */
+ if (iframeContentHeight !== iframeHeight) {
+ outputIFrame.style.height = iframeContentHeight + 'px';
+ appliedHeightAdjustment = true;
+ }
+ } else {
+ /* In case iframe was previously below tab container and its height was set to a fixed value,
+ we need to clear that value, so iframe fills the whole container now */
+ if(appliedHeightAdjustment) {
+ outputIFrame.style.height = "";
+ appliedHeightAdjustment = false;
}
}
-
- shadow.appendChild(document.importNode(content, true));
- mceUtils.openLinksInNewTab(shadow.querySelectorAll('a[href^="http"]'));
- mceUtils.scrollToAnchors(shadow, shadow.querySelectorAll('a[href^="#"]'));
}
/**
@@ -94,11 +165,14 @@ import "../css/tabbed-editor.css";
clearTimeout(timer);
timer = setTimeout(function () {
- templateUtils.createTemplate(getOutput());
- render(templateUtils.getTemplateOutput());
+ refreshOutput();
}, 500);
}
+ outputIFrame.addEventListener("load", function () {
+ onOutputLoaded();
+ });
+
header.addEventListener("click", function (event) {
if (event.target.classList.contains("reset")) {
window.location.reload();
@@ -164,12 +238,7 @@ import "../css/tabbed-editor.css";
tabby.registerEventListeners();
mceEvents.register();
- // register the custom output element
- customElements.define("shadow-output", shadowOutput);
-
- templateUtils.createTemplate(getOutput());
-
document.addEventListener("WebComponentsReady", function () {
- render(templateUtils.getTemplateOutput());
+ refreshOutput();
});
})();
diff --git a/editor/tmpl/live-tabbed-tmpl.html b/editor/tmpl/live-tabbed-tmpl.html
index c9a9b967e..e9d58c8ed 100644
--- a/editor/tmpl/live-tabbed-tmpl.html
+++ b/editor/tmpl/live-tabbed-tmpl.html
@@ -1,6 +1,5 @@
-
@@ -95,9 +94,9 @@