/** * 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 {ATTRIBUTE_ROLE} from "../../dom/constants.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. * @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"; const height = calculateMaximumHeight.call(this, this.parentNode); 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 * @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; } } /** * @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); }); 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; } /** * @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:loading, allowpaymentrequest path:features.allowPaymentRequest, allowfullscreen path:features.allowFullScreen, src path:src" ></iframe> </div>`; } registerCustomElement(Iframe);