From cdec37190cd72789743bf71c8d3d1f726a09ff92 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Sat, 23 Mar 2024 22:00:08 +0100 Subject: [PATCH] feat: new split-screen #171 --- playground/dialog/index.html | 42 + playground/dialog/main.js | 13 + playground/dialog/main.pcss | 8 + playground/issues/142.html | 9 +- playground/issues/144.html | 8 +- playground/issues/152.html | 7 +- playground/issues/154.html | 7 +- playground/issues/158.html | 9 +- playground/split-screen/index.html | 75 ++ playground/split-screen/main.js | 13 + playground/split-screen/main.pcss | 8 + playground/tab/index.html | 10 +- playground/tab/main.mjs | 2 +- playground/vite.config.js | 18 +- source/components/form/tabs.mjs | 1026 +--------------- source/components/host/overlay.mjs | 2 +- source/components/host/viewer.mjs | 16 +- source/components/layout/namespace.mjs | 13 + source/components/layout/split-screen.mjs | 341 ++++++ .../components/layout/style/split-screen.pcss | 59 + .../{form => layout}/style/tabs.pcss | 3 - .../layout/stylesheet/split-screen.mjs | 27 + .../{form => layout}/stylesheet/tabs.mjs | 0 source/components/layout/tabs.mjs | 1075 +++++++++++++++++ 24 files changed, 1728 insertions(+), 1063 deletions(-) create mode 100644 playground/dialog/index.html create mode 100644 playground/dialog/main.js create mode 100644 playground/dialog/main.pcss create mode 100644 playground/split-screen/index.html create mode 100644 playground/split-screen/main.js create mode 100644 playground/split-screen/main.pcss create mode 100644 source/components/layout/namespace.mjs create mode 100644 source/components/layout/split-screen.mjs create mode 100644 source/components/layout/style/split-screen.pcss rename source/components/{form => layout}/style/tabs.pcss (99%) create mode 100644 source/components/layout/stylesheet/split-screen.mjs rename source/components/{form => layout}/stylesheet/tabs.mjs (100%) create mode 100644 source/components/layout/tabs.mjs diff --git a/playground/dialog/index.html b/playground/dialog/index.html new file mode 100644 index 000000000..9c29fa671 --- /dev/null +++ b/playground/dialog/index.html @@ -0,0 +1,42 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <title>Form</title> + <script src="./main.js" type="module"></script> +</head> +<body> + + +<main> + + <div id="dialog"> + + + <monster-split-screen data-monster-option-splittype="horizontal"> + + <div slot="start"> + <h1>Start Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + <div slot="end"> + <h1>End Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + + </monster-split-screen> + + + </div> + + +</main> + +</body> +</html> \ No newline at end of file diff --git a/playground/dialog/main.js b/playground/dialog/main.js new file mode 100644 index 000000000..f18ca31b9 --- /dev/null +++ b/playground/dialog/main.js @@ -0,0 +1,13 @@ +import "../../source/components/style/property.pcss"; +import "../../source/components/style/normalize.pcss"; +import "../../source/components/style/color.pcss"; +import "../../source/components/style/theme.pcss"; +import "../../source/components/style/typography.pcss"; +import "../../source/components/style/form.pcss"; +import "../../source/components/style/link.pcss"; +import "../../source/components/style/button.pcss"; +import "../../source/components/style/ripple.pcss"; +import "../../source/components/layout/split-screen.mjs"; +import "./main.pcss"; + + diff --git a/playground/dialog/main.pcss b/playground/dialog/main.pcss new file mode 100644 index 000000000..d62880653 --- /dev/null +++ b/playground/dialog/main.pcss @@ -0,0 +1,8 @@ + +.dialog { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + border: 1px solid #ccc; + border-radius: 5px; +} \ No newline at end of file diff --git a/playground/issues/142.html b/playground/issues/142.html index f62f51629..6d2b7c54a 100644 --- a/playground/issues/142.html +++ b/playground/issues/142.html @@ -12,10 +12,11 @@ <main> - <h1>Issue 142</h1> - <p> - <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/142" target="_blank">issues/142</a> - </p> + <h1>Issues 142</h1> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/142" target="_blank">issues/142</a> + <li><a href="../../">Back to overview</a></li> + </ul> <div style="width: 450px; margin: 0 auto;"> <!-- <monster-select value="value2,value1">--> diff --git a/playground/issues/144.html b/playground/issues/144.html index a3862bf8a..67316df76 100644 --- a/playground/issues/144.html +++ b/playground/issues/144.html @@ -28,9 +28,11 @@ <main> - <h1>Issues</h1> - - <h2>#144 Buttons in der button-bar nicht gleich hoch</h2> + <h1>Issues 144</h1> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/144" target="_blank">issues/144</a> + <li><a href="../../">Back to overview</a></li> + </ul> <div style="width: 550px;margin: 0 auto;"> <monster-button-bar style="width: 550px;" data-monster-option-popper-placement="right"> diff --git a/playground/issues/152.html b/playground/issues/152.html index 03ac74455..4218e8bfd 100644 --- a/playground/issues/152.html +++ b/playground/issues/152.html @@ -13,9 +13,10 @@ <main> <h1>Issues 152</h1> - <p> - <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a> - </p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a> + <li><a href="../../">Back to overview</a></li> + </ul> <div style="width: 250px;margin: 0 auto;"> diff --git a/playground/issues/154.html b/playground/issues/154.html index 7c904826f..10e0d8024 100644 --- a/playground/issues/154.html +++ b/playground/issues/154.html @@ -13,9 +13,10 @@ <main> <h1>Issues 152</h1> - <p> - <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a> - </p> + <ul> + <li> <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a></li> + <li><a href="../../">Back to overview</a></li> + </ul> <div style="width: 250px;margin: 0 auto;"> diff --git a/playground/issues/158.html b/playground/issues/158.html index 8aef191a2..312733939 100644 --- a/playground/issues/158.html +++ b/playground/issues/158.html @@ -13,10 +13,11 @@ <main> <h1>Issues 158</h1> - <p> - <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/158" - target="_blank">issues/158</a> - </p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/158" + target="_blank">issues/158</a></li> + <li><a href="../../">Back to overview</a></li> + </ul> <monster-datasource-rest id="data1" diff --git a/playground/split-screen/index.html b/playground/split-screen/index.html new file mode 100644 index 000000000..7d0e6a1ae --- /dev/null +++ b/playground/split-screen/index.html @@ -0,0 +1,75 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <title>Form</title> + <script src="./main.js" type="module"></script> +</head> +<body> + + +<main> + + <h1>Split Screen</h1> + <ul> + <li><a href="../">Back to overview</a></li> + </ul> + + <div id="dialog" style=""> + + + <monster-split-screen data-monster-option-splittype="horizontal" style=""> + + <div slot="start"> + <h1>Start Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + <div slot="end"> + <monster-split-screen data-monster-option-splittype="vertical" style=""> + + <div slot="start"> + <h1>Start Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + <div slot="end"> + <monster-split-screen data-monster-option-splittype="horizontal" style=""> + + <div slot="start"> + <h1>Start Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + <div slot="end"> + <h1>End Panel</h1> + <p>Some content</p> + <p>Some content</p> + </div> + + + </monster-split-screen> + + </div> + + + </monster-split-screen> + + </div> + + + </monster-split-screen> + + + </div> + + +</main> + +</body> +</html> \ No newline at end of file diff --git a/playground/split-screen/main.js b/playground/split-screen/main.js new file mode 100644 index 000000000..f18ca31b9 --- /dev/null +++ b/playground/split-screen/main.js @@ -0,0 +1,13 @@ +import "../../source/components/style/property.pcss"; +import "../../source/components/style/normalize.pcss"; +import "../../source/components/style/color.pcss"; +import "../../source/components/style/theme.pcss"; +import "../../source/components/style/typography.pcss"; +import "../../source/components/style/form.pcss"; +import "../../source/components/style/link.pcss"; +import "../../source/components/style/button.pcss"; +import "../../source/components/style/ripple.pcss"; +import "../../source/components/layout/split-screen.mjs"; +import "./main.pcss"; + + diff --git a/playground/split-screen/main.pcss b/playground/split-screen/main.pcss new file mode 100644 index 000000000..d62880653 --- /dev/null +++ b/playground/split-screen/main.pcss @@ -0,0 +1,8 @@ + +.dialog { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + border: 1px solid #ccc; + border-radius: 5px; +} \ No newline at end of file diff --git a/playground/tab/index.html b/playground/tab/index.html index 37c504143..619875f94 100644 --- a/playground/tab/index.html +++ b/playground/tab/index.html @@ -13,12 +13,14 @@ <main style="width:98vw"> <h1>Tabs</h1> + <ul> + <li><a href="../">Back to overview</a></li> + </ul> - <monster-tabs - - data-monster-option-classes-button="monster-theme-secondary-1" - data-monster-option-classes-popper="monster-theme-secondary-1" + + data-monster-option-classes-button="monster-theme-secondary-1" + data-monster-option-classes-popper="monster-theme-secondary-1" > <div>Tab 1</div> <div>Tab 2</div> diff --git a/playground/tab/main.mjs b/playground/tab/main.mjs index c6752bab0..14045ed94 100644 --- a/playground/tab/main.mjs +++ b/playground/tab/main.mjs @@ -11,6 +11,6 @@ import "./main.pcss"; -import "../../source/components/form/tabs.mjs"; +import "../../source/components/layout/tabs.mjs"; diff --git a/playground/vite.config.js b/playground/vite.config.js index 8608de8e4..16c0e8b57 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -35,6 +35,12 @@ const playgroundDir = join(rootDir, 'playground') console.log(rootDir) +function buildStylesheets() { + debugger + execSync('build-stylesheets', {cwd: rootDir, stdio: 'inherit'}) +} + + export default defineConfig({ clearScreen: false, @@ -61,8 +67,16 @@ export default defineConfig({ directoryIndex({ }), viteMockServe({ - mockPath:playgroundDir+ "/mock", // Der Pfad zu Ihren Mock-Dateien - }) + mockPath:playgroundDir+ "/mock", + }), + + { + name: 'postbuild-commands', + closeBundle: async () => { + await buildStylesheets() + }, + + }, ], css: { diff --git a/source/components/form/tabs.mjs b/source/components/form/tabs.mjs index 1c7faaaf8..6ed295922 100644 --- a/source/components/form/tabs.mjs +++ b/source/components/form/tabs.mjs @@ -4,134 +4,14 @@ * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ -import { instanceSymbol } from "../../constants.mjs"; -import { createPopper } from "@popperjs/core"; -import { extend } from "../../data/extend.mjs"; -import { Pathfinder } from "../../data/pathfinder.mjs"; +import { Tabs as NewTabs } from "../layout/tabs.mjs"; import { - addAttributeToken, - addToObjectLink, - hasObjectLink, -} from "../../dom/attributes.mjs"; -import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_PREFIX, - ATTRIBUTE_ROLE, -} from "../../dom/constants.mjs"; -import { - assembleMethodSymbol, - CustomElement, - getSlottedElements, registerCustomElement, } from "../../dom/customelement.mjs"; -import { - findTargetElementFromEvent, - fireCustomEvent, -} from "../../dom/events.mjs"; -import { getDocument } from "../../dom/util.mjs"; -import { random } from "../../math/random.mjs"; -import { getGlobal } from "../../types/global.mjs"; -import { ID } from "../../types/id.mjs"; -import { isArray, isString } from "../../types/is.mjs"; -import { TokenList } from "../../types/tokenlist.mjs"; -import { clone } from "../../util/clone.mjs"; -import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; -import { Processing } from "../../util/processing.mjs"; -import { - ATTRIBUTE_BUTTON_LABEL, - ATTRIBUTE_FORM_RELOAD, - ATTRIBUTE_FORM_URL, - STYLE_DISPLAY_MODE_BLOCK, -} from "./constants.mjs"; -import { TabsStyleSheet } from "./stylesheet/tabs.mjs"; -import { loadAndAssignContent } from "./util/fetch.mjs"; -import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; -import { - popperInstanceSymbol, - setEventListenersModifiers, -} from "./util/popper.mjs"; export { Tabs }; -/** - * @private - * @type {symbol} - */ -const popperElementSymbol = Symbol("popperElement"); - -/** - * @private - * @type {symbol} - */ -const popperNavElementSymbol = Symbol("popperNavElement"); - -/** - * @private - * @type {symbol} - */ -const controlElementSymbol = Symbol("controlElement"); - -/** - * @private - * @type {symbol} - */ -const navElementSymbol = Symbol("navElement"); -/** - * @private - * @type {symbol} - */ -const switchElementSymbol = Symbol("switchElement"); - -/** - * @private - * @type {symbol} - */ -const changeTabEventHandler = Symbol("changeTabEventHandler"); -/** - * @private - * @type {symbol} - */ -const removeTabEventHandler = Symbol("removeTabEventHandler"); - -/** - * @private - * @type {symbol} - */ -const popperSwitchEventHandler = Symbol("popperSwitchEventHandler"); - -/** - * local symbol - * @private - * @type {symbol} - */ -const closeEventHandler = Symbol("closeEventHandler"); - -/** - * @private - * @type {symbol} - */ -const mutationObserverSymbol = Symbol("mutationObserver"); - -/** - * @private - * @type {symbol} - */ -const dimensionsSymbol = Symbol("dimensions"); - -/** - * @private - * @type {symbol} - */ -const timerCallbackSymbol = Symbol("timerCallback"); - -/** - * local symbol - * @private - * @type {symbol} - */ -const resizeObserverSymbol = Symbol("resizeObserver"); - /** * This CustomControl creates a tab element with a variety of options. * @@ -162,7 +42,8 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * skinparam shadowing false * HTMLElement <|-- CustomElement * CustomElement <|-- CustomControl - * CustomControl <|-- Tabs + * CustomControl <|-- NewTabs + * NewTabs <|-- Tabs * @enduml * * @since 1.10.0 @@ -170,907 +51,10 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * @memberOf Monster.Components.Form * @summary A configurable tab control * @fires Monster.Components.Form.event:monster-fetched + * @deprecated since 3.59.0 use {@link Monster.Components.Layout.Tabs} */ -class Tabs extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @returns {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/tabs"); - } - - /** - * 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 - * @property {string} labels.new-tab-label="New Tab" - * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) - * @property {String} fetch.redirect=error - * @property {String} fetch.method=GET - * @property {String} fetch.mode=same-origin - * @property {String} fetch.credentials=same-origin - * @property {Object} fetch.headers={"accept":"text/html"}} - * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) - * @property {string} popper.placement=bottom PopperJS placement - * @property {Object[]} modifiers={name:offset} PopperJS placement - */ - get defaults() { - return Object.assign({}, super.defaults, { - templates: { - main: getTemplate(), - }, - labels: { - "new-tab-label": "New Tab", - }, - buttons: { - standard: [], - popper: [], - }, - fetch: { - redirect: "error", - method: "GET", - mode: "same-origin", - credentials: "same-origin", - headers: { - accept: "text/html", - }, - }, - - classes: { - button: "monster-theme-primary-1", - popper: "monster-theme-primary-1", - }, - - popper: { - placement: "bottom", - modifiers: [ - { - name: "offset", - options: { - offset: [0, 2], - }, - }, - - { - name: "eventListeners", - enabled: false, - }, - ], - }, - }); - } - - /** - * This method is called internal and should not be called directly. - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initControlReferences.call(this); - - this[dimensionsSymbol] = new Pathfinder({ data: {} }); - - initEventHandler.call(this); - - // setup structure - initTabButtons.call(this).then(() => { - initPopperSwitch.call(this); - initPopper.call(this); - attachResizeObserver.call(this); - attachTabChangeObserver.call(this); - }); - } - - /** - * This method is called internal and should not be called directly. - * - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [TabsStyleSheet, ThemeStyleSheet]; - } - - /** - * This method is called internal and should not be called directly. - * - * @return {string} - */ - static getTag() { - return "monster-tabs"; - } - - /** - * This method is called by the dom and should not be called directly. - * - * @return {void} - */ - connectedCallback() { - super.connectedCallback(); - - const document = getDocument(); - - for (const [, type] of Object.entries(["click", "touch"])) { - // close on outside ui-events - document.addEventListener(type, this[closeEventHandler]); - } - } - - /** - * This method is called by the dom and should not be called directly. - * - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - - const document = getDocument(); - - // close on outside ui-events - for (const [, type] of Object.entries(["click", "touch"])) { - document.removeEventListener(type, this[closeEventHandler]); - } - } -} - -/** - * @private - */ -function initPopperSwitch() { - const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots - let switchButton; - if (nodes.size === 0) { - switchButton = document.createElement("button"); - switchButton.setAttribute(ATTRIBUTE_ROLE, "switch"); - switchButton.setAttribute("part", "switch"); - switchButton.classList.add("hidden"); - const classList = this.getOption("classes.button"); - if (classList) { - switchButton.classList.add(classList); - } - switchButton.innerHTML = - '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>'; - this[navElementSymbol].prepend(switchButton); - } else { - switchButton = nodes.next(); - } - - /** - * @param {Event} event - */ - this[popperSwitchEventHandler] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch"); - - if (element instanceof HTMLButtonElement) { - togglePopper.call(this); - } - }; - - for (const type of ["click", "touch"]) { - switchButton.addEventListener(type, this[popperSwitchEventHandler]); - } - - this[switchElementSymbol] = switchButton; -} - -/** - * @private - */ -function hidePopper() { - if (!this[popperInstanceSymbol]) { - return; - } - - this[popperElementSymbol].style.display = "none"; - // performance https://popper.js.org/docs/v2/tutorial/#performance - setEventListenersModifiers.call(this, false); -} - -/** - * @private - */ -function showPopper() { - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - return; - } - - this[popperElementSymbol].style.visibility = "hidden"; - this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; - // performance https://popper.js.org/docs/v2/tutorial/#performance - setEventListenersModifiers.call(this, true); - - this[popperInstanceSymbol].update(); - - new Processing(() => { - this[popperElementSymbol].style.removeProperty("visibility"); - }) - .run(undefined) - .then(() => {}) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); -} - -/** - * @private - */ -function togglePopper() { - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - hidePopper.call(this); - } else { - showPopper.call(this); - } -} - -/** - * @private - */ -function attachResizeObserver() { - // against flickering - this[resizeObserverSymbol] = new ResizeObserver((entries) => { - if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { - try { - this[timerCallbackSymbol].touch(); - return; - } catch (e) { - delete this[timerCallbackSymbol]; - } - } - - this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { - this[dimensionsSymbol].setVia("data.calculated", false); - checkAndRearrangeButtons.call(this); - }); - }); - - this[resizeObserverSymbol].observe(this[navElementSymbol]); -} - -/** - * @private - */ -function attachTabChangeObserver() { - // against flickering - new MutationObserver((mutations) => { - let runUpdate = false; - - for (const mutation of mutations) { - if (mutation.type === "childList") { - if ( - mutation.addedNodes.length > 0 || - mutation.removedNodes.length > 0 - ) { - runUpdate = true; - break; - } - } - } - - if (runUpdate === true) { - this[dimensionsSymbol].setVia("data.calculated", false); - initTabButtons.call(this); - } - }).observe(this, { - childList: true, - }); -} - -/** - * @private - * @return {Select} - * @external "external:createPopper" - * @see {@link Plugins} - */ -function initPopper() { - const self = this; - - const options = extend({}, self.getOption("popper")); - - self[popperInstanceSymbol] = createPopper( - self[switchElementSymbol], - self[popperElementSymbol], - options, - ); - - const observer1 = new MutationObserver(function (mutations) { - let runUpdate = false; - - for (const mutation of mutations) { - if (mutation.type === "childList") { - if ( - mutation.addedNodes.length > 0 || - mutation.removedNodes.length > 0 - ) { - runUpdate = true; - break; - } - } - } - - if (runUpdate === true) { - self[popperInstanceSymbol].update(); - } - }); - - observer1.observe(self[popperNavElementSymbol], { - childList: true, - subtree: true, - }); - - return self; -} - -/** - * @private - * @param {HTMLElement} element - */ -function show(element) { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`); - - const nodes = getSlottedElements.call(this); - for (const node of nodes) { - const id = node.getAttribute("id"); - - if (id === reference) { - node.classList.add("active"); - - // get all data- from button and filter out data-monster-attributes and data-monster-insert - const data = {}; - const mask = [ - "data-monster-attributes", - "data-monster-insert-reference", - "data-monster-state", - "data-monster-button-label", - "data-monster-objectlink", - "data-monster-role", - ]; - - for (const [, attr] of Object.entries(node.attributes)) { - if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) { - data[attr.name] = attr.value; - } - } - - if (node.hasAttribute(ATTRIBUTE_FORM_URL)) { - const url = node.getAttribute(ATTRIBUTE_FORM_URL); - - if ( - !node.hasAttribute(ATTRIBUTE_FORM_RELOAD) || - node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow" - ) { - node.removeAttribute(ATTRIBUTE_FORM_URL); - } - - const options = this.getOption("fetch", {}); - const filter = undefined; - loadAndAssignContent(node, url, options, filter) - .then(() => { - fireCustomEvent(this, "monster-tab-changed", { - reference, - }); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } else { - fireCustomEvent(this, "monster-tab-changed", { - reference, - data, - }); - } - } else { - node.classList.remove("active"); - } - } - - const standardButtons = this.getOption("buttons.standard"); - for (const index in standardButtons) { - const button = standardButtons[index]; - const state = button["reference"] === reference ? "active" : "inactive"; - this.setOption(`buttons.standard.${index}.state`, state); - } - - const popperButton = this.getOption("buttons.popper"); - for (const index in popperButton) { - const button = popperButton[index]; - const state = button["reference"] === reference ? "active" : "inactive"; - this.setOption(`buttons.popper.${index}.state`, state); - } - - hidePopper.call(this); -} - -/** - * @private - */ -function initEventHandler() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - /** - * @param {Event} event - */ - this[changeTabEventHandler] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button"); - - if (element instanceof HTMLButtonElement && element.disabled !== true) { - show.call(this, element); - } - }; - - /** - * event:monster-tab-remove - * @event Monster.Components.Form.event:monster-tab-remove - */ - - /** - * @param {Event} event - * @fires Monster.Components.Form.event:monster-tab-remove - */ - this[removeTabEventHandler] = (event) => { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "remove-tab", - ); - - if (element instanceof HTMLElement) { - const button = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "button", - ); - - if (button instanceof HTMLButtonElement && button.disabled !== true) { - const reference = button.getAttribute( - `${ATTRIBUTE_PREFIX}tab-reference`, - ); - if (reference) { - const container = this.querySelector(`[id=${reference}]`); - if (container instanceof HTMLElement) { - container.remove(); - initTabButtons.call(this); - fireCustomEvent(this, "monster-tab-remove", { - reference, - }); - } - } - } - } - }; - - this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]); - this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]); - - this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]); - this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]); - - /** - * @param {Event} event - */ - this[closeEventHandler] = (event) => { - const path = event.composedPath(); - - for (const [, element] of Object.entries(path)) { - if (element === this) { - return; - } - } - - hidePopper.call(this); - }; - - return this; -} - -/** - * @private - * @param observedNode - */ -function attachTabMutationObserver(observedNode) { - const self = this; - - if (hasObjectLink(observedNode, mutationObserverSymbol)) { - return; - } - - /** - * this construct monitors a node whether it is disabled or modified - * @type {MutationObserver} - */ - const observer = new MutationObserver(function (mutations) { - if (isArray(mutations)) { - const mutation = mutations.pop(); - if (mutation instanceof MutationRecord) { - initTabButtons.call(self); - } - } - }); - - observer.observe(observedNode, { - childList: false, - attributes: true, - subtree: false, - attributeFilter: [ - "disabled", - ATTRIBUTE_BUTTON_LABEL, - `${ATTRIBUTE_PREFIX}button-icon`, - ], - }); - - addToObjectLink(observedNode, mutationObserverSymbol, observer); -} - -/** - * @private - * @return {Select} - * @throws {Error} no shadow-root is defined - */ -function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - this[controlElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=control]`, - ); - this[navElementSymbol] = this.shadowRoot.querySelector( - `nav[${ATTRIBUTE_ROLE}=nav]`, - ); - this[popperElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper]`, - ); - this[popperNavElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper-nav]`, - ); -} - -/** - * @private - * @return {Promise<unknown>} - * @throws {Error} no shadow-root is defined - * - */ -function initTabButtons() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - let activeReference; - - const dimensionsCalculated = this[dimensionsSymbol].getVia( - "data.calculated", - false, - ); - - const buttons = []; - const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots - - for (const node of nodes) { - if (!(node instanceof HTMLElement)) continue; - let label = getButtonLabel.call(this, node); - - let reference; - if (node.hasAttribute("id")) { - reference = node.getAttribute("id"); - } - - let disabled; - if (node.hasAttribute("disabled") || node.disabled === true) { - disabled = true; - } - - if (!reference) { - reference = new ID("tab").toString(); - node.setAttribute("id", reference); - } - - if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) { - label = `<span part="label">${label}</span><img part="icon" src="${node.getAttribute( - `${ATTRIBUTE_PREFIX}button-icon`, - )}">`; - } - - let remove = false; - if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) { - remove = true; - } - - if (node.matches(".active") === true && disabled !== true) { - node.classList.remove("active"); - activeReference = reference; - } - - const state = ""; - const classes = dimensionsCalculated ? "" : "invisible"; - - buttons.push({ - reference, - label, - state, - class: classes, - disabled, - remove, - }); - - attachTabMutationObserver.call(this, node); - } - - this.setOption("buttons.standard", clone(buttons)); - this.setOption("buttons.popper", []); - this.setOption("marker", random()); - - return adjustButtonVisibility.call(this).then(() => { - if (activeReference) { - return new Processing(() => { - const button = this.shadowRoot.querySelector( - `[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`, - ); - if (button instanceof HTMLButtonElement && button.disabled !== true) { - show.call(this, button); - } - }) - .run(undefined) - .then(() => {}) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - - return Promise.resolve(); - }); -} - -function checkAndRearrangeButtons() { - if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) { - calculateNavigationButtonsDimensions.call(this); - } - - rearrangeButtons.call(this); -} - -/** - * @private - * @return {Promise<unknown>} - */ -function adjustButtonVisibility() { - const self = this; - - return new Promise((resolve) => { - const observer = new MutationObserver(function (mutations) { - const defCount = self.getOption("buttons.standard").length; - const domCount = self[navElementSymbol].querySelectorAll( - 'button[data-monster-role="button"]', - ).length; - - // in drawing - if (defCount !== domCount) return; - - observer.disconnect(); - - checkAndRearrangeButtons.call(self); - - resolve(); - }); - - observer.observe(self[navElementSymbol], { - attributes: true, - }); - }); -} - -/** - * @private - * @param {string} value - * @return {number} - */ -function getDimValue(value) { - if ([undefined, null].indexOf(value) !== -1) { - return 0; - } - - const valueAsInt = parseInt(value, 10); - - if (isNaN(valueAsInt)) { - return 0; - } - - return valueAsInt; -} - -/** - * @private - * @param {HTMLElement} node - * @return {number} - */ -function calcBoxWidth(node) { - const dim = getGlobal("window").getComputedStyle(node); - const bounding = node.getBoundingClientRect(); - - return ( - getDimValue(dim["border-left-width"]) + - getDimValue(dim["padding-left"]) + - getDimValue(dim["margin-left"]) + - getDimValue(bounding["width"]) + - getDimValue(dim["border-right-width"]) + - getDimValue(dim["margin-right"]) + - getDimValue(dim["padding-left"]) - ); -} - -/** - * @private - * @return {Object} - */ -function rearrangeButtons() { - const standardButtons = []; - const popperButtons = []; - - let sum = 0; - const space = this[dimensionsSymbol].getVia("data.space"); - - const buttons = this.getOption("buttons.standard"); - for (const [, button] of buttons.entries()) { - const ref = button?.reference; - - sum += this[dimensionsSymbol].getVia(`data.button.${ref}`); - - if (sum > space) { - popperButtons.push(clone(button)); - } else { - standardButtons.push(clone(button)); - } - } - - this.setOption("buttons.standard", standardButtons); - this.setOption("buttons.popper", popperButtons); - - if (this[switchElementSymbol]) { - if (popperButtons.length > 0) { - this[switchElementSymbol].classList.remove("hidden"); - } else { - this[switchElementSymbol].classList.add("hidden"); - } - } -} - -/** - * @private - * @return {Object} - */ -function calculateNavigationButtonsDimensions() { - const width = this[navElementSymbol].getBoundingClientRect().width; - - let startEndWidth = 0; - - getSlottedElements.call(this, undefined, "start").forEach((node) => { - startEndWidth += calcBoxWidth.call(this, node); - }); - - getSlottedElements.call(this, undefined, "end").forEach((node) => { - startEndWidth += calcBoxWidth.call(this, node); - }); - - this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2); - this[dimensionsSymbol].setVia("data.visible", !(width === 0)); - - const buttons = this.getOption("buttons.standard").concat( - this.getOption("buttons.popper"), - ); - - for (const [i, button] of buttons.entries()) { - const ref = button?.reference; - const element = this[navElementSymbol].querySelector( - `:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`, - ); - if (!(element instanceof HTMLButtonElement)) continue; - - this[dimensionsSymbol].setVia( - `data.button.${ref}`, - calcBoxWidth.call(this, element), - ); - button["class"] = new TokenList(button["class"]) - .remove("invisible") - .toString(); - } - - const slots = this[controlElementSymbol].querySelectorAll( - `nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`, - ); - for (const [, slot] of slots.entries()) { - slot.classList.remove("invisible"); - } - - this[dimensionsSymbol].setVia("data.calculated", true); - this.setOption("buttons.standard", clone(buttons)); -} - -/** - * @private - * @param {HTMLElement} node - * @return {string} - */ -function getButtonLabel(node) { - let label; - let setLabel = false; - if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) { - label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL); - } else { - label = node.innerText; - setLabel = true; - } - - if (!isString(label)) { - label = ""; - } - - label = label.trim(); - - if (label === "") { - label = this.getOption("labels.new-tab-label", "New Tab"); - } - - if (label.length > 100) { - label = `${label.substring(0, 99)}…`; - } - - if (setLabel === true) { - node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label); - } - - return label; -} - -/** - * @private - * @return {string} - */ -function getTemplate() { - // language=HTML - return ` - <template id="buttons"> - <button part="button" - data-monster-role="button" - data-monster-attributes=" - class path:classes.button, - data-monster-state path:buttons.state, - disabled path:buttons.disabled | if:true, - data-monster-tab-reference path:buttons.reference"><span - data-monster-replace="path:buttons.label"></span><span part="remove-tab" - data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden " - data-monster-role="remove-tab" - tabindex="-1"></span></button> - </template> - <div data-monster-role="control" part="control"> - <nav data-monster-role="nav" part="nav" - data-monster-attributes="data-monster-marker path:marker" - data-monster-insert="buttons path:buttons.standard"> - <slot name="start" class="invisible"></slot> - <div data-monster-role="popper" part="popper" tabindex="-1" - data-monster-attributes="class path:classes.popper"> - <div data-popper-arrow></div> - +class Tabs extends NewTabs { - <div part="popper-nav" data-monster-role="popper-nav" - data-monster-insert="buttons path:buttons.popper" - tabindex="-1"></div> - </div> - <slot name="popper" class="invisible"></slot> - <slot name="end" class="invisible"></slot> - </nav> - <slot data-monster-role="slot" class="invisible"></slot> - </div>`; } registerCustomElement(Tabs); diff --git a/source/components/host/overlay.mjs b/source/components/host/overlay.mjs index 08e38ba23..ab6e618bc 100644 --- a/source/components/host/overlay.mjs +++ b/source/components/host/overlay.mjs @@ -106,7 +106,7 @@ class Overlay extends CustomElement { * @returns {symbol} */ static get [instanceSymbol]() { - return Symbol.for("@schukai/component-host/overlay@@instance"); + return Symbol.for("@schukai/monster/components/host/overlay@@instance"); } /** diff --git a/source/components/host/viewer.mjs b/source/components/host/viewer.mjs index 8581aef06..7139b567c 100644 --- a/source/components/host/viewer.mjs +++ b/source/components/host/viewer.mjs @@ -39,18 +39,10 @@ const viewerElementSymbol = Symbol("viewerElement"); * Or you can create this CustomControl directly in Javascript: * * ```js - * import '@schukai/component-state/source/viewer.mjs'; + * import '@schukai/monster/components/host/viewer.mjs'; * document.createElement('monster-viewer'); * ``` * - * The Body should have a class "hidden" to ensure that the styles are applied correctly. - * - * ```css - * body.hidden { - * visibility: hidden; - * } - * ``` - * * @startuml viewer.png * skinparam monochrome true * skinparam shadowing false @@ -62,10 +54,6 @@ const viewerElementSymbol = Symbol("viewerElement"); * @copyright schukai GmbH * @memberOf Monster.Components.Host * @summary A simple viewer component - * @fires Monster.Components.Host.Viewer.event:monster-viewer-before-open - * @fires Monster.Components.Host.Viewer.event:monster-viewer-open - * @fires Monster.Components.Host.Viewer.event:monster-viewer-before-close - * @fires Monster.Components.Host.Viewer.event:monster-viewer-closed */ class Viewer extends CustomElement { /** @@ -73,7 +61,7 @@ class Viewer extends CustomElement { * @returns {symbol} */ static get [instanceSymbol]() { - return Symbol.for("@schukai/component-host/viewer@@instance"); + return Symbol.for("@schukai/monster/components/host/viewer@@instance"); } /** diff --git a/source/components/layout/namespace.mjs b/source/components/layout/namespace.mjs new file mode 100644 index 000000000..23450cb6d --- /dev/null +++ b/source/components/layout/namespace.mjs @@ -0,0 +1,13 @@ +/** + * Copyright 2023 schukai GmbH + * SPDX-License-Identifier: AGPL-3.0 + */ + +/** + * Namespace for all layout related functions. + * + * @namespace Monster.Components.Layout + * @memberOf Monster + * @author schukai GmbH + */ +const ns = {}; diff --git a/source/components/layout/split-screen.mjs b/source/components/layout/split-screen.mjs new file mode 100644 index 000000000..3375a9f5c --- /dev/null +++ b/source/components/layout/split-screen.mjs @@ -0,0 +1,341 @@ +/** + * Copyright 2023 schukai GmbH + * SPDX-License-Identifier: AGPL-3.0 + */ + +import { + assembleMethodSymbol, + CustomElement, + registerCustomElement, +} from "../../dom/customelement.mjs"; +import "../notify/notify.mjs"; +import {Observer} from "../../types/observer.mjs"; +import {SplitScreenStyleSheet} from "./stylesheet/split-screen.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {internalSymbol} from "../../constants.mjs"; + +export {SplitScreen, TYPE_VERTICAL, TYPE_HORIZONTAL}; + +/** + * @private + * @type {symbol} + */ +const splitScreenElementSymbol = Symbol("splitScreenElement"); + +/** + * @private + * @type {symbol} + */ +const draggerElementSymbol = Symbol("draggerElement"); +/** + * @private + * @type {symbol} + */ +const startPanelElementSymbol = Symbol("startPanelElement"); +/** + * @private + * @type {symbol} + */ +const endPanelElementSymbol = Symbol("endPanelElement"); +/** + * @private + * @type {symbol} + */ +const handleElementSymbol = Symbol("handleElement"); + +/** + * + * @type {string} + */ +const TYPE_VERTICAL = "vertical"; +/** + * + * @type {string} + */ +const TYPE_HORIZONTAL = "horizontal"; + + +/** + * The Viewer component is used to show a PDF, HTML or Image. + * + * <img src="./images/splitscreen.png"> + * + * You can create this control either by specifying the HTML tag <monster-splitscreen />` directly in the HTML or using + * Javascript via the `document.createElement('monster-split-screen');` method. + * + * ```html + * <monster-split-screen></monster-split-screen> + * ``` + * + * Or you can create this CustomControl directly in Javascript: + * + * ```js + * import '@schukai/monster/components/layout/split-screen.mjs'; + * document.createElement('monster-split-screen'); + * ``` + * + * @startuml splitscreen.png + * skinparam monochrome true + * skinparam shadowing false + * HTMLElement <|-- CustomElement + * CustomElement <|-- CustomControl + * CustomControl <|-- SplitScreen + * @enduml + * + * @copyright schukai GmbH + * @memberOf Monster.Components.Layout + * @summary A simple split screen layout + */ +class SplitScreen extends CustomElement { + + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/layout/splitscreen"); + } + + /** + * 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} classes Css classes + * @property {Object} features Feature definitions + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + splitType: TYPE_VERTICAL, + dimension: "20%", + classes: {}, + features: {}, + }); + } + + setContent(html) { + this.setOption("content", html); + return this; + } + + /** + * + * @returns {Monster.Components.Host.Viewer} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + initControlReferences.call(this); + initEventHandler.call(this); + applyPanelDimensions.call(this); + } + + setDimension(dimension) { + // check if percent and greater than100 + if (dimension.includes("%")) { + if (parseInt(dimension) > 100) { + throw new Error("dimension must be less than 100%"); + } else if (parseInt(dimension) < 0) { + throw new Error("dimension must be greater than 0%"); + } + } + + this.setOption("dimension", dimension); + return this; + } + + + /** + * + * @return {string} + */ + static getTag() { + return "monster-split-screen"; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [SplitScreenStyleSheet]; + } +} + + +/** + * Set the dimensions of the panel based on the split type. + */ +function applyPanelDimensions() { + + const splitType = this.getOption("splitType"); + const dimension = this.getOption("dimension"); + + if (splitType === TYPE_VERTICAL) { + this[startPanelElementSymbol].style.width = dimension; + this[endPanelElementSymbol].style.width = `calc(100% - ${dimension} - 5px)`; + this[draggerElementSymbol].style.cursor = "ew-resize"; + this[splitScreenElementSymbol].classList.add("vertical"); + this[splitScreenElementSymbol].classList.remove("horizontal"); + + } else { + this[startPanelElementSymbol].style.height = dimension; + this[endPanelElementSymbol].style.height = `calc(100% - ${dimension} - 5px)`; + this[draggerElementSymbol].style.cursor = "ns-resize"; + this[splitScreenElementSymbol].classList.add("horizontal"); + this[splitScreenElementSymbol].classList.remove("vertical"); + + } +} + + +/** + * @private + * @return {Select} + * @throws {Error} no shadow-root is defined + */ +function initControlReferences() { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[splitScreenElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=split-screen]"); + this[draggerElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=dragger]"); + this[handleElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=handle]"); + + this[startPanelElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=startPanel]"); + this[endPanelElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=endPanel]"); + +} + +/** + * @private + */ +function initEventHandler() { + const self = this; + + this[internalSymbol].getSubject().isDragging = false; + + this[draggerElementSymbol].addEventListener('dblclick', () => { + self[internalSymbol].getSubject().isDragging = false; + applyPanelDimensions.call(this); + }); + + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + console.log(entry.contentRect.width); + } + }); + + resizeObserver.observe(this[splitScreenElementSymbol]); + + + this[draggerElementSymbol].addEventListener('mousedown', () => { + self[internalSymbol].getSubject().isDragging = true; + + document.addEventListener('mousemove', (e) => { + e.preventDefault(); + if (!self[internalSymbol].getSubject().isDragging) { + return; + } + + if (self.getOption("splitType") === TYPE_HORIZONTAL) { + const containerOffsetTop = self[splitScreenElementSymbol].offsetTop; + const topPanel = self[startPanelElementSymbol]; + const bottomPanel = self[endPanelElementSymbol]; + const newTopHeight = e.clientY - containerOffsetTop; + topPanel.style.height = `${newTopHeight}px`; + bottomPanel.style.height = `calc(100% - ${newTopHeight}px - 5px)`; // 5px is dragger height + + + } else { + + const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft; + const leftPanel = self[startPanelElementSymbol]; + const rightPanel = self[endPanelElementSymbol]; + const newLeftWidth = e.clientX - containerOffsetLeft; + leftPanel.style.width = `${newLeftWidth}px`; + rightPanel.style.width = `calc(100% - ${newLeftWidth}px - 5px)`; // 5px is dragger width + } + + }); + + document.addEventListener('mouseup', (e) => { + self[internalSymbol].getSubject().isDragging = false; + document.removeEventListener('mousemove', (e) => { + if (!self[internalSymbol].getSubject().isDragging) { + return; + } + + if (self.getOption("splitType") === TYPE_VERTICAL) { + const containerOffsetTop = self[splitScreenElementSymbol].offsetTop; + const topPanel = self[startPanelElementSymbol]; + const bottomPanel = self[endPanelElementSymbol]; + const newTopHeight = e.clientY - containerOffsetTop; + topPanel.style.height = `${newTopHeight}px`; + bottomPanel.style.height = `calc(100% - ${newTopHeight}px - 5px)`; // 5px is dragger height + + } else { + const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft; + const newLeftWidth = e.clientX - containerOffsetLeft; + const leftPanel = self[startPanelElementSymbol]; + const rightPanel = self[endPanelElementSymbol]; + leftPanel.style.width = `${newLeftWidth}px`; + rightPanel.style.width = `calc(100% - ${newLeftWidth}px - 5px)`; // 5px is dragger width + } + }); + document.removeEventListener('mouseup', (e) => { + self[internalSymbol].getSubject().isDragging = false; + }); + }); + }); + + let lastDimension = this.getOption("dimension"); + let lastType = this.getOption("splitType"); + this[internalSymbol].attachObserver( + new Observer(() => { + if (lastDimension !== this.getOption("dimension")) { + lastDimension = this.getOption("dimension"); + applyPanelDimensions.call(this); + } + + if (lastType !== this.getOption("splitType")) { + lastType = this.getOption("splitType"); + applyPanelDimensions.call(this); + } + + })); + + + return this; + +} + +/** + * @private + * @return {string} + */ +function getTemplate() { + // language=HTML + return ` + <div data-monster-role="split-screen" part="container"> + <div data-monster-role="startPanel" class="panel" part="startPanel"> + <slot name="start"></slot> + </div> + <div data-monster-role="dragger" part="dragger"> + <div data-monster-role="handle"></div> + </div> + <div data-monster-role="endPanel" class="panel" part="endPanel"> + <slot name="end"></slot> + </div> + + + </div>`; +} + +registerCustomElement(SplitScreen); diff --git a/source/components/layout/style/split-screen.pcss b/source/components/layout/style/split-screen.pcss new file mode 100644 index 000000000..786812cf4 --- /dev/null +++ b/source/components/layout/style/split-screen.pcss @@ -0,0 +1,59 @@ + + +[data-monster-role="split-screen"] { + + box-sizing: border-box; + display: flex; + width: 100%; + height: auto; + flex-direction: row; + margin: 0; + padding: 0; + + & .panel { + flex-grow: 1; + overflow: auto; + } + + [data-monster-role="dragger"] { + background-color: var(--monster-bg-color-primary-4); + color: var(--monster-color-primary-4); + width: var(--monster-border-width); + height: auto; + + position: relative; + + & [data-monster-role=handle] { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + cursor: pointer; + width: 5px; + height: 120px; + background-color: var(--monster-bg-color-primary-3); + color: var(--monster-color-primary-3); + z-index: var(--monster-z-index-outline); + } + } + + &.horizontal { + flex-direction: column; + + [data-monster-role="dragger"] { + width: 100%; + height: var(--monster-border-width); + + & [data-monster-role=handle] { + width: 120px; + height: 5px; + } + + } + + + } + +} + + diff --git a/source/components/form/style/tabs.pcss b/source/components/layout/style/tabs.pcss similarity index 99% rename from source/components/form/style/tabs.pcss rename to source/components/layout/style/tabs.pcss index de838d398..6c3b5b096 100644 --- a/source/components/form/style/tabs.pcss +++ b/source/components/layout/style/tabs.pcss @@ -27,9 +27,6 @@ nav[data-monster-role=nav] { border-bottom-style: var(--monster-border-style); box-shadow: var(--monster-box-shadow-1); border-color: var(--monster-bg-color-primary-2); - - flex-wrap: nowrap; - } [data-monster-role=nav] button .remove-tab { diff --git a/source/components/layout/stylesheet/split-screen.mjs b/source/components/layout/stylesheet/split-screen.mjs new file mode 100644 index 000000000..516f7ae58 --- /dev/null +++ b/source/components/layout/stylesheet/split-screen.mjs @@ -0,0 +1,27 @@ + +/** + * Copyright schukai GmbH and contributors 2024. All Rights Reserved. + * Node module: @schukai/monster + * This file is licensed under the AGPLv3 License. + * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html + */ + +import {addAttributeToken} from "../../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; + +export {SplitScreenStyleSheet} + +/** + * @private + * @type {CSSStyleSheet} + */ +const SplitScreenStyleSheet = new CSSStyleSheet(); + +try { + SplitScreenStyleSheet.insertRule(` +@layer splitscreen { +[data-monster-role=split-screen]{box-sizing:border-box;display:flex;flex-direction:row;height:auto;margin:0;padding:0;width:100%}[data-monster-role=split-screen] .panel{flex-grow:1;overflow:auto}[data-monster-role=split-screen] [data-monster-role=dragger]{background-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4);height:auto;position:relative;width:var(--monster-border-width)}[data-monster-role=split-screen] [data-monster-role=dragger] [data-monster-role=handle]{background-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-3);cursor:pointer;height:120px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px;z-index:var(--monster-z-index-outline)}.horizontal[data-monster-role=split-screen]{flex-direction:column}.horizontal[data-monster-role=split-screen] [data-monster-role=dragger]{height:var(--monster-border-width);width:100%}.horizontal[data-monster-role=split-screen] [data-monster-role=dragger] [data-monster-role=handle]{height:5px;width:120px} +}`, 0); +} catch (e) { + addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + ""); +} diff --git a/source/components/form/stylesheet/tabs.mjs b/source/components/layout/stylesheet/tabs.mjs similarity index 100% rename from source/components/form/stylesheet/tabs.mjs rename to source/components/layout/stylesheet/tabs.mjs diff --git a/source/components/layout/tabs.mjs b/source/components/layout/tabs.mjs new file mode 100644 index 000000000..75d9d07f6 --- /dev/null +++ b/source/components/layout/tabs.mjs @@ -0,0 +1,1075 @@ +/** + * Copyright schukai GmbH and contributors 2023. All Rights Reserved. + * Node module: @schukai/monster + * This file is licensed under the AGPLv3 License. + * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html + */ +import { instanceSymbol } from "../../constants.mjs"; +import { createPopper } from "@popperjs/core"; +import { extend } from "../../data/extend.mjs"; +import { Pathfinder } from "../../data/pathfinder.mjs"; +import { + addAttributeToken, + addToObjectLink, + hasObjectLink, +} from "../../dom/attributes.mjs"; +import { + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_PREFIX, + ATTRIBUTE_ROLE, +} from "../../dom/constants.mjs"; +import { + assembleMethodSymbol, + CustomElement, + getSlottedElements, + registerCustomElement, +} from "../../dom/customelement.mjs"; +import { + findTargetElementFromEvent, + fireCustomEvent, +} from "../../dom/events.mjs"; +import { getDocument } from "../../dom/util.mjs"; +import { random } from "../../math/random.mjs"; +import { getGlobal } from "../../types/global.mjs"; +import { ID } from "../../types/id.mjs"; +import { isArray, isString } from "../../types/is.mjs"; +import { TokenList } from "../../types/tokenlist.mjs"; +import { clone } from "../../util/clone.mjs"; +import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; +import { Processing } from "../../util/processing.mjs"; +import { + ATTRIBUTE_BUTTON_LABEL, + ATTRIBUTE_FORM_RELOAD, + ATTRIBUTE_FORM_URL, + STYLE_DISPLAY_MODE_BLOCK, +} from "../form/constants.mjs"; + +import { TabsStyleSheet } from "./stylesheet/tabs.mjs"; +import { loadAndAssignContent } from "../form/util/fetch.mjs"; +import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; +import { + popperInstanceSymbol, + setEventListenersModifiers, +} from "../form/util/popper.mjs"; + +export { Tabs }; + +/** + * @private + * @type {symbol} + */ +const popperElementSymbol = Symbol("popperElement"); + +/** + * @private + * @type {symbol} + */ +const popperNavElementSymbol = Symbol("popperNavElement"); + +/** + * @private + * @type {symbol} + */ +const controlElementSymbol = Symbol("controlElement"); + +/** + * @private + * @type {symbol} + */ +const navElementSymbol = Symbol("navElement"); +/** + * @private + * @type {symbol} + */ +const switchElementSymbol = Symbol("switchElement"); + +/** + * @private + * @type {symbol} + */ +const changeTabEventHandler = Symbol("changeTabEventHandler"); +/** + * @private + * @type {symbol} + */ +const removeTabEventHandler = Symbol("removeTabEventHandler"); + +/** + * @private + * @type {symbol} + */ +const popperSwitchEventHandler = Symbol("popperSwitchEventHandler"); + +/** + * local symbol + * @private + * @type {symbol} + */ +const closeEventHandler = Symbol("closeEventHandler"); + +/** + * @private + * @type {symbol} + */ +const mutationObserverSymbol = Symbol("mutationObserver"); + +/** + * @private + * @type {symbol} + */ +const dimensionsSymbol = Symbol("dimensions"); + +/** + * @private + * @type {symbol} + */ +const timerCallbackSymbol = Symbol("timerCallback"); + +/** + * local symbol + * @private + * @type {symbol} + */ +const resizeObserverSymbol = Symbol("resizeObserver"); + +/** + * This CustomControl creates a tab element with a variety of options. + * + * <img src="./images/tabs.png"> + * + * You can create this control either by specifying the HTML tag `<monster-tabs />` directly in the HTML or using + * Javascript via the `document.createElement('monster-tabs');` method. + * + * ```html + * <monster-tabs></monster-tabs> + * ``` + * + * Or you can create this CustomControl directly in Javascript: + * + * ```js + * import {Tabs} from '@schukai/monster/components/layout/tabs.mjs'; + * document.createElement('monster-tabs'); + * ``` + * + * @example <caption>Create a simple tab control</caption> + * <monster-tabs> + * <div id="tab1">Tab 1</div> + * <div id="tab2">Tab 2</div> + * </monster-tabs> + * + * @startuml tabs.png + * skinparam monochrome true + * skinparam shadowing false + * HTMLElement <|-- CustomElement + * CustomElement <|-- Tabs + * @enduml + * + * @since 1.10.0 + * @copyright schukai GmbH + * @memberOf Monster.Components.Layout + * @summary A configurable tab control + * @fires Monster.Components.Layout.event:monster-fetched + */ +class Tabs extends CustomElement { + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/layout/tabs"); + } + + /** + * 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 + * @property {string} labels.new-tab-label="New Tab" + * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) + * @property {String} fetch.redirect=error + * @property {String} fetch.method=GET + * @property {String} fetch.mode=same-origin + * @property {String} fetch.credentials=same-origin + * @property {Object} fetch.headers={"accept":"text/html"}} + * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) + * @property {string} popper.placement=bottom PopperJS placement + * @property {Object[]} modifiers={name:offset} PopperJS placement + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + labels: { + "new-tab-label": "New Tab", + }, + buttons: { + standard: [], + popper: [], + }, + fetch: { + redirect: "error", + method: "GET", + mode: "same-origin", + credentials: "same-origin", + headers: { + accept: "text/html", + }, + }, + + classes: { + button: "monster-theme-primary-1", + popper: "monster-theme-primary-1", + }, + + popper: { + placement: "bottom", + modifiers: [ + { + name: "offset", + options: { + offset: [0, 2], + }, + }, + + { + name: "eventListeners", + enabled: false, + }, + ], + }, + }); + } + + /** + * This method is called internal and should not be called directly. + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + initControlReferences.call(this); + + this[dimensionsSymbol] = new Pathfinder({ data: {} }); + + initEventHandler.call(this); + + // setup structure + initTabButtons.call(this).then(() => { + initPopperSwitch.call(this); + initPopper.call(this); + attachResizeObserver.call(this); + attachTabChangeObserver.call(this); + }); + } + + /** + * This method is called internal and should not be called directly. + * + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [TabsStyleSheet, ThemeStyleSheet]; + } + + /** + * This method is called internal and should not be called directly. + * + * @return {string} + */ + static getTag() { + return "monster-tabs"; + } + + /** + * This method is called by the dom and should not be called directly. + * + * @return {void} + */ + connectedCallback() { + super.connectedCallback(); + + const document = getDocument(); + + for (const [, type] of Object.entries(["click", "touch"])) { + // close on outside ui-events + document.addEventListener(type, this[closeEventHandler]); + } + } + + /** + * This method is called by the dom and should not be called directly. + * + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + + const document = getDocument(); + + // close on outside ui-events + for (const [, type] of Object.entries(["click", "touch"])) { + document.removeEventListener(type, this[closeEventHandler]); + } + } +} + +/** + * @private + */ +function initPopperSwitch() { + const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots + let switchButton; + if (nodes.size === 0) { + switchButton = document.createElement("button"); + switchButton.setAttribute(ATTRIBUTE_ROLE, "switch"); + switchButton.setAttribute("part", "switch"); + switchButton.classList.add("hidden"); + const classList = this.getOption("classes.button"); + if (classList) { + switchButton.classList.add(classList); + } + switchButton.innerHTML = + '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>'; + this[navElementSymbol].prepend(switchButton); + } else { + switchButton = nodes.next(); + } + + /** + * @param {Event} event + */ + this[popperSwitchEventHandler] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch"); + + if (element instanceof HTMLButtonElement) { + togglePopper.call(this); + } + }; + + for (const type of ["click", "touch"]) { + switchButton.addEventListener(type, this[popperSwitchEventHandler]); + } + + this[switchElementSymbol] = switchButton; +} + +/** + * @private + */ +function hidePopper() { + if (!this[popperInstanceSymbol]) { + return; + } + + this[popperElementSymbol].style.display = "none"; + // performance https://popper.js.org/docs/v2/tutorial/#performance + setEventListenersModifiers.call(this, false); +} + +/** + * @private + */ +function showPopper() { + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + return; + } + + this[popperElementSymbol].style.visibility = "hidden"; + this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; + // performance https://popper.js.org/docs/v2/tutorial/#performance + setEventListenersModifiers.call(this, true); + + this[popperInstanceSymbol].update(); + + new Processing(() => { + this[popperElementSymbol].style.removeProperty("visibility"); + }) + .run(undefined) + .then(() => {}) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); +} + +/** + * @private + */ +function togglePopper() { + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + hidePopper.call(this); + } else { + showPopper.call(this); + } +} + +/** + * @private + */ +function attachResizeObserver() { + // against flickering + this[resizeObserverSymbol] = new ResizeObserver((entries) => { + if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[timerCallbackSymbol].touch(); + return; + } catch (e) { + delete this[timerCallbackSymbol]; + } + } + + this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { + this[dimensionsSymbol].setVia("data.calculated", false); + checkAndRearrangeButtons.call(this); + }); + }); + + this[resizeObserverSymbol].observe(this[navElementSymbol]); +} + +/** + * @private + */ +function attachTabChangeObserver() { + // against flickering + new MutationObserver((mutations) => { + let runUpdate = false; + + for (const mutation of mutations) { + if (mutation.type === "childList") { + if ( + mutation.addedNodes.length > 0 || + mutation.removedNodes.length > 0 + ) { + runUpdate = true; + break; + } + } + } + + if (runUpdate === true) { + this[dimensionsSymbol].setVia("data.calculated", false); + initTabButtons.call(this); + } + }).observe(this, { + childList: true, + }); +} + +/** + * @private + * @return {Select} + * @external "external:createPopper" + * @see {@link Plugins} + */ +function initPopper() { + const self = this; + + const options = extend({}, self.getOption("popper")); + + self[popperInstanceSymbol] = createPopper( + self[switchElementSymbol], + self[popperElementSymbol], + options, + ); + + const observer1 = new MutationObserver(function (mutations) { + let runUpdate = false; + + for (const mutation of mutations) { + if (mutation.type === "childList") { + if ( + mutation.addedNodes.length > 0 || + mutation.removedNodes.length > 0 + ) { + runUpdate = true; + break; + } + } + } + + if (runUpdate === true) { + self[popperInstanceSymbol].update(); + } + }); + + observer1.observe(self[popperNavElementSymbol], { + childList: true, + subtree: true, + }); + + return self; +} + +/** + * @private + * @param {HTMLElement} element + */ +function show(element) { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`); + + const nodes = getSlottedElements.call(this); + for (const node of nodes) { + const id = node.getAttribute("id"); + + if (id === reference) { + node.classList.add("active"); + + // get all data- from button and filter out data-monster-attributes and data-monster-insert + const data = {}; + const mask = [ + "data-monster-attributes", + "data-monster-insert-reference", + "data-monster-state", + "data-monster-button-label", + "data-monster-objectlink", + "data-monster-role", + ]; + + for (const [, attr] of Object.entries(node.attributes)) { + if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) { + data[attr.name] = attr.value; + } + } + + if (node.hasAttribute(ATTRIBUTE_FORM_URL)) { + const url = node.getAttribute(ATTRIBUTE_FORM_URL); + + if ( + !node.hasAttribute(ATTRIBUTE_FORM_RELOAD) || + node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow" + ) { + node.removeAttribute(ATTRIBUTE_FORM_URL); + } + + const options = this.getOption("fetch", {}); + const filter = undefined; + loadAndAssignContent(node, url, options, filter) + .then(() => { + fireCustomEvent(this, "monster-tab-changed", { + reference, + }); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } else { + fireCustomEvent(this, "monster-tab-changed", { + reference, + data, + }); + } + } else { + node.classList.remove("active"); + } + } + + const standardButtons = this.getOption("buttons.standard"); + for (const index in standardButtons) { + const button = standardButtons[index]; + const state = button["reference"] === reference ? "active" : "inactive"; + this.setOption(`buttons.standard.${index}.state`, state); + } + + const popperButton = this.getOption("buttons.popper"); + for (const index in popperButton) { + const button = popperButton[index]; + const state = button["reference"] === reference ? "active" : "inactive"; + this.setOption(`buttons.popper.${index}.state`, state); + } + + hidePopper.call(this); +} + +/** + * @private + */ +function initEventHandler() { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + /** + * @param {Event} event + */ + this[changeTabEventHandler] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button"); + + if (element instanceof HTMLButtonElement && element.disabled !== true) { + show.call(this, element); + } + }; + + /** + * event:monster-tab-remove + * @event Monster.Components.Layout.event:monster-tab-remove + */ + + /** + * @param {Event} event + * @fires Monster.Components.Layout.event:monster-tab-remove + */ + this[removeTabEventHandler] = (event) => { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "remove-tab", + ); + + if (element instanceof HTMLElement) { + const button = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "button", + ); + + if (button instanceof HTMLButtonElement && button.disabled !== true) { + const reference = button.getAttribute( + `${ATTRIBUTE_PREFIX}tab-reference`, + ); + if (reference) { + const container = this.querySelector(`[id=${reference}]`); + if (container instanceof HTMLElement) { + container.remove(); + initTabButtons.call(this); + fireCustomEvent(this, "monster-tab-remove", { + reference, + }); + } + } + } + } + }; + + this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]); + this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]); + + this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]); + this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]); + + /** + * @param {Event} event + */ + this[closeEventHandler] = (event) => { + const path = event.composedPath(); + + for (const [, element] of Object.entries(path)) { + if (element === this) { + return; + } + } + + hidePopper.call(this); + }; + + return this; +} + +/** + * @private + * @param observedNode + */ +function attachTabMutationObserver(observedNode) { + const self = this; + + if (hasObjectLink(observedNode, mutationObserverSymbol)) { + return; + } + + /** + * this construct monitors a node whether it is disabled or modified + * @type {MutationObserver} + */ + const observer = new MutationObserver(function (mutations) { + if (isArray(mutations)) { + const mutation = mutations.pop(); + if (mutation instanceof MutationRecord) { + initTabButtons.call(self); + } + } + }); + + observer.observe(observedNode, { + childList: false, + attributes: true, + subtree: false, + attributeFilter: [ + "disabled", + ATTRIBUTE_BUTTON_LABEL, + `${ATTRIBUTE_PREFIX}button-icon`, + ], + }); + + addToObjectLink(observedNode, mutationObserverSymbol, observer); +} + +/** + * @private + * @return {Select} + * @throws {Error} no shadow-root is defined + */ +function initControlReferences() { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[controlElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=control]`, + ); + this[navElementSymbol] = this.shadowRoot.querySelector( + `nav[${ATTRIBUTE_ROLE}=nav]`, + ); + this[popperElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper]`, + ); + this[popperNavElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper-nav]`, + ); +} + +/** + * @private + * @return {Promise<unknown>} + * @throws {Error} no shadow-root is defined + * + */ +function initTabButtons() { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + let activeReference; + + const dimensionsCalculated = this[dimensionsSymbol].getVia( + "data.calculated", + false, + ); + + const buttons = []; + const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots + + for (const node of nodes) { + if (!(node instanceof HTMLElement)) continue; + let label = getButtonLabel.call(this, node); + + let reference; + if (node.hasAttribute("id")) { + reference = node.getAttribute("id"); + } + + let disabled; + if (node.hasAttribute("disabled") || node.disabled === true) { + disabled = true; + } + + if (!reference) { + reference = new ID("tab").toString(); + node.setAttribute("id", reference); + } + + if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) { + label = `<span part="label">${label}</span><img part="icon" src="${node.getAttribute( + `${ATTRIBUTE_PREFIX}button-icon`, + )}">`; + } + + let remove = false; + if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) { + remove = true; + } + + if (node.matches(".active") === true && disabled !== true) { + node.classList.remove("active"); + activeReference = reference; + } + + const state = ""; + const classes = dimensionsCalculated ? "" : "invisible"; + + buttons.push({ + reference, + label, + state, + class: classes, + disabled, + remove, + }); + + attachTabMutationObserver.call(this, node); + } + + this.setOption("buttons.standard", clone(buttons)); + this.setOption("buttons.popper", []); + this.setOption("marker", random()); + + return adjustButtonVisibility.call(this).then(() => { + if (activeReference) { + return new Processing(() => { + const button = this.shadowRoot.querySelector( + `[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`, + ); + if (button instanceof HTMLButtonElement && button.disabled !== true) { + show.call(this, button); + } + }) + .run(undefined) + .then(() => {}) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + return Promise.resolve(); + }); +} + +function checkAndRearrangeButtons() { + if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) { + calculateNavigationButtonsDimensions.call(this); + } + + rearrangeButtons.call(this); +} + +/** + * @private + * @return {Promise<unknown>} + */ +function adjustButtonVisibility() { + const self = this; + + return new Promise((resolve) => { + const observer = new MutationObserver(function (mutations) { + const defCount = self.getOption("buttons.standard").length; + const domCount = self[navElementSymbol].querySelectorAll( + 'button[data-monster-role="button"]', + ).length; + + // in drawing + if (defCount !== domCount) return; + + observer.disconnect(); + + checkAndRearrangeButtons.call(self); + + resolve(); + }); + + observer.observe(self[navElementSymbol], { + attributes: true, + }); + }); +} + +/** + * @private + * @param {string} value + * @return {number} + */ +function getDimValue(value) { + if ([undefined, null].indexOf(value) !== -1) { + return 0; + } + + const valueAsInt = parseInt(value, 10); + + if (isNaN(valueAsInt)) { + return 0; + } + + return valueAsInt; +} + +/** + * @private + * @param {HTMLElement} node + * @return {number} + */ +function calcBoxWidth(node) { + const dim = getGlobal("window").getComputedStyle(node); + const bounding = node.getBoundingClientRect(); + + return ( + getDimValue(dim["border-left-width"]) + + getDimValue(dim["padding-left"]) + + getDimValue(dim["margin-left"]) + + getDimValue(bounding["width"]) + + getDimValue(dim["border-right-width"]) + + getDimValue(dim["margin-right"]) + + getDimValue(dim["padding-left"]) + ); +} + +/** + * @private + * @return {Object} + */ +function rearrangeButtons() { + const standardButtons = []; + const popperButtons = []; + + let sum = 0; + const space = this[dimensionsSymbol].getVia("data.space"); + + const buttons = this.getOption("buttons.standard"); + for (const [, button] of buttons.entries()) { + const ref = button?.reference; + + sum += this[dimensionsSymbol].getVia(`data.button.${ref}`); + + if (sum > space) { + popperButtons.push(clone(button)); + } else { + standardButtons.push(clone(button)); + } + } + + this.setOption("buttons.standard", standardButtons); + this.setOption("buttons.popper", popperButtons); + + if (this[switchElementSymbol]) { + if (popperButtons.length > 0) { + this[switchElementSymbol].classList.remove("hidden"); + } else { + this[switchElementSymbol].classList.add("hidden"); + } + } +} + +/** + * @private + * @return {Object} + */ +function calculateNavigationButtonsDimensions() { + const width = this[navElementSymbol].getBoundingClientRect().width; + + let startEndWidth = 0; + + getSlottedElements.call(this, undefined, "start").forEach((node) => { + startEndWidth += calcBoxWidth.call(this, node); + }); + + getSlottedElements.call(this, undefined, "end").forEach((node) => { + startEndWidth += calcBoxWidth.call(this, node); + }); + + this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2); + this[dimensionsSymbol].setVia("data.visible", !(width === 0)); + + const buttons = this.getOption("buttons.standard").concat( + this.getOption("buttons.popper"), + ); + + for (const [i, button] of buttons.entries()) { + const ref = button?.reference; + const element = this[navElementSymbol].querySelector( + `:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`, + ); + if (!(element instanceof HTMLButtonElement)) continue; + + this[dimensionsSymbol].setVia( + `data.button.${ref}`, + calcBoxWidth.call(this, element), + ); + button["class"] = new TokenList(button["class"]) + .remove("invisible") + .toString(); + } + + const slots = this[controlElementSymbol].querySelectorAll( + `nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`, + ); + for (const [, slot] of slots.entries()) { + slot.classList.remove("invisible"); + } + + this[dimensionsSymbol].setVia("data.calculated", true); + this.setOption("buttons.standard", clone(buttons)); +} + +/** + * @private + * @param {HTMLElement} node + * @return {string} + */ +function getButtonLabel(node) { + let label; + let setLabel = false; + if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) { + label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL); + } else { + label = node.innerText; + setLabel = true; + } + + if (!isString(label)) { + label = ""; + } + + label = label.trim(); + + if (label === "") { + label = this.getOption("labels.new-tab-label", "New Tab"); + } + + if (label.length > 100) { + label = `${label.substring(0, 99)}…`; + } + + if (setLabel === true) { + node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label); + } + + return label; +} + +/** + * @private + * @return {string} + */ +function getTemplate() { + // language=HTML + return ` + <template id="buttons"> + <button part="button" + data-monster-role="button" + data-monster-attributes=" + class path:classes.button, + data-monster-state path:buttons.state, + disabled path:buttons.disabled | if:true, + data-monster-tab-reference path:buttons.reference"><span + data-monster-replace="path:buttons.label"></span><span part="remove-tab" + data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden " + data-monster-role="remove-tab" + tabindex="-1"></span></button> + </template> + <div data-monster-role="control" part="control"> + <nav data-monster-role="nav" part="nav" + data-monster-attributes="data-monster-marker path:marker" + data-monster-insert="buttons path:buttons.standard"> + <slot name="start" class="invisible"></slot> + <div data-monster-role="popper" part="popper" tabindex="-1" + data-monster-attributes="class path:classes.popper"> + <div data-popper-arrow></div> + + + <div part="popper-nav" data-monster-role="popper-nav" + data-monster-insert="buttons path:buttons.popper" + tabindex="-1"></div> + </div> + <slot name="popper" class="invisible"></slot> + <slot name="end" class="invisible"></slot> + </nav> + <slot data-monster-role="slot" class="invisible"></slot> + </div>`; +} + +registerCustomElement(Tabs); -- GitLab