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