From 0e23cb22a310cc0fe2435c2f2a2826c9b170c65b Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Wed, 16 Oct 2024 20:17:19 +0200 Subject: [PATCH] feat: new iframe feature for replace target --- development/issues/closed/243-test2.html | 24 +- source/components/layout/iframe.mjs | 501 ++++++++++++----------- 2 files changed, 279 insertions(+), 246 deletions(-) diff --git a/development/issues/closed/243-test2.html b/development/issues/closed/243-test2.html index 4dbdd72a6..fca745416 100644 --- a/development/issues/closed/243-test2.html +++ b/development/issues/closed/243-test2.html @@ -12,14 +12,28 @@ margin: 0; padding: 0; background-color: #77d804; - + } - + main { width: 200px; height: 1200px; - background-color: #bc05a9; + background-color: #ede5eb; } - + </style> -</head><body><main>TEST 2</main></body></html> +</head> +<body> +<main>TEST 2 + <ul> + <li><a href="./243-test1.html" target="_blank">Doc 1 (_blank)</a> + <li><a href="./243-test1.html" target="_self">Doc 1 (_self)</a> + <li><a href="./243-test1.html" target="_parent">Doc 1 (_parent)</a> + <li><a href="./243-test1.html" target="_top">Doc 1 (_top)</a> + <li><a href="./243-test1.html" target="iframe">Doc 1 (iframe)</a> + </ul> + + +</main> +</body> +</html> diff --git a/source/components/layout/iframe.mjs b/source/components/layout/iframe.mjs index 815bf0563..47d087327 100644 --- a/source/components/layout/iframe.mjs +++ b/source/components/layout/iframe.mjs @@ -10,20 +10,20 @@ * For more information about purchasing a commercial license, please contact schukai GmbH. */ -import { instanceSymbol } from "../../constants.mjs"; -import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; -import { CustomElement } from "../../dom/customelement.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {ATTRIBUTE_ROLE} from "../../dom/constants.mjs"; +import {CustomElement} from "../../dom/customelement.mjs"; import { - assembleMethodSymbol, - registerCustomElement, + 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"; +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 }; +export {Iframe}; /** * @private @@ -62,124 +62,133 @@ const timerCallbackSymbol = Symbol("timerCallback"); * @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. - * @return {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]; - } + /** + * This method is called by the `instanceof` operator. + * @return {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 {boolean} features.allowFullScreen=true Allow fullscreen + * @property {boolean} features.allowPaymentRequest=true Allow payment request + * @property {boolean} features.replaceTargets=true Replace parent target in iframe + * @property {string} loading="eager" Loading state + * @property {string} referrerPolicy="no-referrer" Referrer policy + * @property {string} src Source + * @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, + replaceTargets: true, + }, + + loading: "eager", + + 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"; + this.style.boxSizing = "border-box"; - const height = calculateMaximumHeight.call(this, this.parentNode); - if (height < 0 || isNaN(height)) { - return; - } + const height = calculateMaximumHeight.call(this, this.parentNode); + if (height < 0 || isNaN(height)) { + return; + } - this[iframeElementSymbol].style.height = `${height}px`; + this[iframeElementSymbol].style.height = `${height}px`; } /** @@ -188,90 +197,90 @@ function calcHeight() { * @return {*} */ 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; - } + 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); + // 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(); - } + if (this[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } } /** @@ -280,36 +289,46 @@ function disconnectResizeObserver() { * @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; + 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); + }); + + this[iframeElementSymbol].addEventListener("load", () => { + calcHeight.call(this); + if (this.getOption("features.replaceParentTarget")) { + var links = this[iframeElementSymbol].contentDocument.querySelectorAll('a[target="_parent"], form[target="_parent"], a[target="_top"], form[target="_top"]'); + links.forEach(function(link) { + link.target = '_self'; + }); + } + }); + + return this; } /** @@ -317,17 +336,17 @@ function initEventHandler() { * @return {void} */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } - this[PanelElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=control]", - ); + this[PanelElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=control]", + ); - this[iframeElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}="control"] iframe`, - ); + this[iframeElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}="control"] iframe`, + ); } /** @@ -335,16 +354,16 @@ function initControlReferences() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // 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, + referrerpolicy path:referrerPolicy, + loading path:loading, + allowpaymentrequest path:features.allowPaymentRequest, + allowfullscreen path:features.allowFullScreen, src path:src" ></iframe> </div>`; -- GitLab