From 847c4ba8fdda48dae913eb483b14a925aec05534 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Tue, 1 Oct 2024 17:11:53 +0200 Subject: [PATCH] feat: new iframe control #243 --- development/issues/closed/243-test1.html | 25 ++ development/issues/closed/243-test2.html | 25 ++ development/issues/closed/243.html | 39 ++ development/issues/closed/243.mjs | 29 ++ source/components/layout/iframe.mjs | 358 ++++++++++++++++++ source/components/layout/style/iframe.pcss | 39 ++ .../components/layout/stylesheet/iframe.mjs | 38 ++ 7 files changed, 553 insertions(+) create mode 100644 development/issues/closed/243-test1.html create mode 100644 development/issues/closed/243-test2.html create mode 100644 development/issues/closed/243.html create mode 100644 development/issues/closed/243.mjs create mode 100644 source/components/layout/iframe.mjs create mode 100644 source/components/layout/style/iframe.pcss create mode 100644 source/components/layout/stylesheet/iframe.mjs diff --git a/development/issues/closed/243-test1.html b/development/issues/closed/243-test1.html new file mode 100644 index 000000000..257c8cf25 --- /dev/null +++ b/development/issues/closed/243-test1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>new iframe control #243</title> + <style> + body { + font-family: Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + margin: 0; + padding: 0; + background-color: #17b5af; + + } + + main { + width: 200px; + height: 1200px; + background-color: #122dda; + } + + </style> +</head><body><main>Test 1</main></body></html> diff --git a/development/issues/closed/243-test2.html b/development/issues/closed/243-test2.html new file mode 100644 index 000000000..4dbdd72a6 --- /dev/null +++ b/development/issues/closed/243-test2.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>new iframe control #243</title> + <style> + body { + font-family: Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + margin: 0; + padding: 0; + background-color: #77d804; + + } + + main { + width: 200px; + height: 1200px; + background-color: #bc05a9; + } + + </style> +</head><body><main>TEST 2</main></body></html> diff --git a/development/issues/closed/243.html b/development/issues/closed/243.html new file mode 100644 index 000000000..298538e71 --- /dev/null +++ b/development/issues/closed/243.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>new iframe control #243</title> + <script src="./243.mjs" type="module"></script> +</head> +<body> +<h1>new iframe control #243</h1> +<p></p> +<ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/243">Issue #243</a></li> + <li><a href="/">Back to overview</a></li> +</ul> +<main> + + <monster-panel id="panel" style="border: 9px solid yellow;display: block"> + <monster-panel style="border: 10px solid blue" id="inner" data-monster-option-heightadjustment="5"> + <monster-panel style="border: 10px solid green" id="inner2" data-monster-option-heightadjustment="5"> + <monster-panel style="border: 7px solid black" id="inner3" data-monster-option-heightadjustment="5"> + <monster-split-panel data-monster-option-splittype="vertical"> + <monster-panel style="border: 10px solid green" id="inner2" + data-monster-option-heightadjustment="5" slot="start"> + <h1>Start Panel</h1> + </monster-panel> + <monster-panel style="border: 10px solid green" id="inner2" + data-monster-option-heightadjustment="5" slot="end"> + <monster-iframe id="iframe" data-monster-option-src="./243-test1.html" style="border: 10px solid red"></monster-iframe> + </monster-panel> + </monster-split-panel> + </monster-panel> + </monster-panel> + </monster-panel> + </monster-panel> + +</main> +</body> +</html> diff --git a/development/issues/closed/243.mjs b/development/issues/closed/243.mjs new file mode 100644 index 000000000..2d0c1401e --- /dev/null +++ b/development/issues/closed/243.mjs @@ -0,0 +1,29 @@ +/** +* @file development/issues/open/243.mjs +* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/243 +* @description new iframe control +* @issue 243 +*/ + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/link.pcss"; +import "../../../source/components/style/color.pcss"; +import "../../../source/components/style/theme.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; +import "../../../source/components/style/color.pcss"; +import "../../../source/components/layout/panel.mjs"; +import "../../../source/components/layout/iframe.mjs"; +import "../../../source/components/layout/split-panel.mjs"; + + + +setTimeout(() => { + + const iframe = document.getElementById("iframe") + iframe.setOption("src", "./243-test2.html") +}, 1000) \ No newline at end of file diff --git a/source/components/layout/iframe.mjs b/source/components/layout/iframe.mjs new file mode 100644 index 000000000..52797d7a6 --- /dev/null +++ b/source/components/layout/iframe.mjs @@ -0,0 +1,358 @@ +/** + * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. + * Node module: @schukai/monster + * + * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). + * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html + * + * For those who do not wish to adhere to the AGPLv3, a commercial license is available. + * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. + * For more information about purchasing a commercial license, please contact schukai GmbH. + */ + +import { instanceSymbol } from "../../constants.mjs"; +import { addAttributeToken } from "../../dom/attributes.mjs"; +import { + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, +} from "../../dom/constants.mjs"; +import { CustomControl } from "../../dom/customcontrol.mjs"; +import { CustomElement } from "../../dom/customelement.mjs"; +import { + assembleMethodSymbol, + registerCustomElement, +} from "../../dom/customelement.mjs"; +import { findTargetElementFromEvent } from "../../dom/events.mjs"; +import { isFunction } from "../../types/is.mjs"; +import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; +import { IframeStyleSheet } from "./stylesheet/iframe.mjs"; +import { fireCustomEvent } from "../../dom/events.mjs"; + +export { Iframe }; + +/** + * @private + * @type {symbol} + */ +export const iframeElementSymbol = Symbol("iframeElement"); + +/** + * @private + * @type {symbol} + */ +const PanelElementSymbol = Symbol("PanelElement"); + +/** + * local symbol + * @private + * @type {symbol} + */ +const resizeObserverSymbol = Symbol("resizeObserver"); + +/** + * @private + * @type {symbol} + */ +const timerCallbackSymbol = Symbol("timerCallback"); + +/** + * A Iframe Control + * + * @fragments /fragments/components/layout/iframe/ + * + * @example /examples/components/layout/iframe-simple + * + * @since 3.76.0 + * @copyright schukai GmbH + * @summary A cool and fancy Iframe that can make your life easier and also looks good. + */ +class Iframe extends CustomElement { + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/layout/iframe@@instance"); + } + + /** + * + * @return {Components.Layout.Iframe + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + calcHeight.call(this); + return this; + } + + /** + * This method is called by the dom and should not be called directly. + * + * @return {void} + */ + connectedCallback() { + super.connectedCallback(); + attachResizeObserver.call(this); + + // disable scrolling in parent node + if (this.parentNode && this.parentNode instanceof HTMLElement) { + this.parentNode.style.overflow = "hidden"; + } + } + + /** + * This method is called by the dom and should not be called directly. + * + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + disconnectResizeObserver.call(this); + } + + /** + * To set the options via the HTML Tag the attribute `data-monster-options` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {Object} templates Template definitions + * @property {string} templates.main Main template + * @property {Object} labels Label definitions + * @property {Object} actions Callbacks + * @property {string} actions.click="throw Error" Callback when clicked + * @property {Object} features Features + * @property {Object} classes CSS classes + * @property {boolean} disabled=false Disabled state + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + src: null, + + /* "allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation"*/ + sandbox: null, + + labels: {}, + classes: {}, + + name: "", + + referrerpolicy: "no-referrer", + + disabled: false, + features: { + allowfullscreen: true, + allowpaymentrequest: true, + loading: "egager", + }, + actions: { + click: () => { + throw new Error("the click action is not defined"); + }, + }, + }); + } + + /** + * @return {string} + */ + static getTag() { + return "monster-iframe"; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [IframeStyleSheet]; + } +} + +/** + * @private + */ +function calcHeight() { + this.style.boxSizing = "border-box"; + + const height = calculateMaximumHeight.call(this, this.parentNode); + console.log("height", height); + if (height < 0 || isNaN(height)) { + return; + } + + this[iframeElementSymbol].style.height = `${height}px`; +} + +/** + * Calculate the maximum height of an element based on the window's inner height + * @param element + * @returns {*} + */ +function calculateMaximumHeight(element) { + let totalBottomBorder = 0; + let totalBottomPadding = 0; + let totalBottomMargin = 0; + let totalOutlineHeight = 0; + let totalBoxShadowHeight = 0; + let currentElement = element; + + while (currentElement && currentElement !== document.body) { + const style = window.getComputedStyle(currentElement); + const boxSizing = style.boxSizing; + + const elementHeight = currentElement.getBoundingClientRect().height; + + const borderBottomWidth = parseFloat(style.borderBottomWidth); + const paddingBottom = parseFloat(style.paddingBottom); + const marginBottom = parseFloat(style.marginBottom); + + const outlineHeight = parseFloat(style.outlineWidth); + + totalBottomBorder += isNaN(borderBottomWidth) ? 0 : borderBottomWidth; + totalBottomPadding += + isNaN(paddingBottom) || boxSizing === "border-box" ? 0 : paddingBottom; + totalBottomMargin += isNaN(marginBottom) ? 0 : marginBottom; + totalOutlineHeight += isNaN(outlineHeight) ? 0 : outlineHeight; + + const boxShadow = style.boxShadow; + let boxShadowVerticalTotal = 0; + + if (boxShadow !== "none") { + const boxShadowValues = boxShadow.split(" "); + const verticalOffset = parseFloat(boxShadowValues[3]); + const blurRadius = parseFloat(boxShadowValues[4]); + const spreadRadius = parseFloat(boxShadowValues[5]); + + boxShadowVerticalTotal = verticalOffset + blurRadius + spreadRadius; + } + + totalBoxShadowHeight += isNaN(boxShadowVerticalTotal) + ? 0 + : boxShadowVerticalTotal; + + if (elementHeight > 200) { + return ( + elementHeight - + totalBottomBorder - + totalBottomPadding - + totalBottomMargin - + totalOutlineHeight - + totalBoxShadowHeight + ); + } + + currentElement = currentElement.parentNode || currentElement.host; + } +} + +/** + * @private + */ +function attachResizeObserver() { + // against flickering + this[resizeObserverSymbol] = new ResizeObserver(() => { + if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[timerCallbackSymbol].touch(); + return; + } catch (e) { + delete this[timerCallbackSymbol]; + } + } + + this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { + calcHeight.call(this); + }); + }); + + this[resizeObserverSymbol].observe(this.ownerDocument.body); + this[resizeObserverSymbol].observe(document.scrollingElement); +} + +function disconnectResizeObserver() { + if (this[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } +} + +/** + * @private + * @return {initEventHandler} + * @fires monster-iframe-clicked + */ +function initEventHandler() { + const self = this; + const element = this[iframeElementSymbol]; + + const type = "click"; + + element.addEventListener(type, function (event) { + const callback = self.getOption("actions.click"); + + fireCustomEvent(self, "monster-iframe-clicked", { + element: self, + }); + + if (!isFunction(callback)) { + return; + } + + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "control", + ); + + if (!(element instanceof Node && self.hasNode(element))) { + return; + } + + callback.call(self, event); + }); + + return this; +} + +/** + * @private + * @return {void} + */ +function initControlReferences() { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[PanelElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=control]", + ); + + this[iframeElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}="control"] iframe`, + ); +} + +/** + * @private + * @return {string} + */ +function getTemplate() { + // language=HTML + return ` + <div data-monster-role="control" part="control"> + <iframe data-monster-role="iframe" + data-monster-attributes="sandbox path:sandbox, + name path:name, + referrerpolicy path:referrerpolicy, + loading path:features.loading, + allowpaymentrequest path:features.allowpaymentrequest, + allowfullscreen path:features.allowfullscreen, + src path:src" + ></iframe></div>`; +} + +registerCustomElement(Iframe); diff --git a/source/components/layout/style/iframe.pcss b/source/components/layout/style/iframe.pcss new file mode 100644 index 000000000..4745aa929 --- /dev/null +++ b/source/components/layout/style/iframe.pcss @@ -0,0 +1,39 @@ + +:host { + display: block; + border: none; + box-sizing: border-box; + padding: 0; + margin: 0; + height: fill-available; + height: -moz-available; +} + + +iframe { + display: block; + border: none; + box-sizing: border-box; + + padding: 0; + margin: 0; + + height: fill-available; + height: stretch; + width: 100%; +} + + +[data-monster-role="control"] { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + border: none; + box-sizing: border-box; + padding: 0; + margin: 0; + height: fill-available; + height: stretch; +} \ No newline at end of file diff --git a/source/components/layout/stylesheet/iframe.mjs b/source/components/layout/stylesheet/iframe.mjs new file mode 100644 index 000000000..689b5155f --- /dev/null +++ b/source/components/layout/stylesheet/iframe.mjs @@ -0,0 +1,38 @@ +/** + * Copyright © schukai GmbH and all contributing authors, 2024. All rights reserved. + * Node module: @schukai/monster + * + * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). + * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html + * + * For those who do not wish to adhere to the AGPLv3, a commercial license is available. + * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. + * For more information about purchasing a commercial license, please contact schukai GmbH. + */ + +import { addAttributeToken } from "../../../dom/attributes.mjs"; +import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs"; + +export { IframeStyleSheet }; + +/** + * @private + * @type {CSSStyleSheet} + */ +const IframeStyleSheet = new CSSStyleSheet(); + +try { + IframeStyleSheet.insertRule( + ` +@layer iframe { +:host{border:none;box-sizing:border-box;display:block;height:-webkit-fill-available;height:fill-available;height:-moz-available;margin:0;padding:0}iframe{display:block}[data-monster-role=control],iframe{border:none;box-sizing:border-box;height:-webkit-fill-available;height:-moz-available;height:fill-available;height:stretch;margin:0;padding:0;width:100%}[data-monster-role=control]{align-items:center;display:flex;flex-direction:column;justify-content:center} +}`, + 0, + ); +} catch (e) { + addAttributeToken( + document.getRootNode().querySelector("html"), + ATTRIBUTE_ERRORMESSAGE, + e + "", + ); +} -- GitLab