/** * 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. * * SPDX-License-Identifier: AGPL-3.0 */ import { assembleMethodSymbol, CustomElement, registerCustomElement, } from "../../dom/customelement.mjs"; import "../notify/notify.mjs"; import { fireCustomEvent } from "../../dom/events.mjs"; import { Observer } from "../../types/observer.mjs"; import { SplitPanelStyleSheet } from "./stylesheet/split-panel.mjs"; import { instanceSymbol } from "../../constants.mjs"; import { internalSymbol } from "../../constants.mjs"; export { SplitPanel, 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"; /** * A SplitPanel Control * * @fragments /fragments/components/layout/split-panel/ * * @example /examples/components/layout/split-panel-simple * * @since 3.54.0 * @copyright schukai GmbH * @summary The SplitPanel control is a simple layout control that allows you to split the screen * into two parts. The split can be either vertical or horizontal. The control provides a * draggable handle that allows you to adjust the size of the two panels. */ class SplitPanel extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/layout/split-panel"); } /** * 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 {string} splitType Split type (vertical or horizontal) * @property {string} dimension Dimension * @property {string} dimension.initial Initial dimension of the start panel * @property {string} dimension.max Maximum dimension of the start panel (in percentage) * @property {string} dimension.min Minimum dimension of the start panel (in percentage) */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, splitType: TYPE_VERTICAL, dimension: { initial: "60%", max: "80%", min: "20%", }, }); } fullStartScreen() { this.setDimension("100%"); return this; } fullEndScreen() { this.setDimension("0%"); return this; } isFullStartScreen() { return this[internalSymbol].getSubject().currentDimension === "100%"; } isFullEndScreen() { return this[internalSymbol].getSubject().currentDimension === "0%"; } isInitialScreen() { return ( this[internalSymbol].getSubject().currentDimension === this.getOption("dimension").initial ); } resetScreen() { this.setDimension(this.getOption("dimension").initial); return this; } setContent(html) { this.setOption("content", html); return this; } /** * * @return {Monster.Components.Host.Viewer} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); //applyPanelDimensions.call(this); this.setDimension(this.getOption("dimension").initial); } /** * Check if the dimension is a percentage and within a valid range, then set the dimension option. * * @param {string} dimension - The dimension to be set, can be in percentage or absolute value. * @return {Object} - Returns the current object instance for chaining. */ 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[internalSymbol].getSubject().currentDimension = dimension; return this; } /** * * @return {string} */ static getTag() { return "monster-split-panel"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [SplitPanelStyleSheet]; } } /** * Set the dimensions of the panel based on the split type. * @fires monster-dimension-changed */ function applyPanelDimensions() { const splitType = this.getOption("splitType"); const dimension = this[internalSymbol].getSubject().currentDimension; 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"); } fireCustomEvent(this, "monster-dimension-changed", { controller: this, dimension: dimension, }); } /** * @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-panel]", ); 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; let lastDimension = this[internalSymbol].getSubject().currentDimension; let lastType = this.getOption("splitType"); this[internalSymbol].getSubject().isDragging = false; // @todo: add better touch support const eventTypes = ["dblclick", "touchstart"]; for (const eventType of eventTypes) { this[draggerElementSymbol].addEventListener(eventType, () => { self[internalSymbol].getSubject().isDragging = false; lastDimension = undefined; let currentDimension; if (self.getOption("splitType") === TYPE_VERTICAL) { const topPanel = self[startPanelElementSymbol]; currentDimension = topPanel.style.width; } else { const topPanel = self[startPanelElementSymbol]; currentDimension = topPanel.style.height; } if (currentDimension === self.getOption("dimension").initial) { self.setDimension(self.getOption("dimension").max); } else if (currentDimension === self.getOption("dimension").max) { self.setDimension(self.getOption("dimension").min); } else if (currentDimension === self.getOption("dimension").min) { self.setDimension(self.getOption("dimension").initial); } else { self.setDimension(self.getOption("dimension").initial); } }); } this[draggerElementSymbol].addEventListener("mousedown", () => { self[internalSymbol].getSubject().isDragging = true; const eventListener = (e) => { e.preventDefault(); // the 5px are wrong and must be calc from css property --monster-dragger-width let draggerWidth = getComputedStyle( self[draggerElementSymbol], ).getPropertyValue("--monster-dragger-width"); if ( draggerWidth === "" || draggerWidth === undefined || draggerWidth === null ) { draggerWidth = "0"; } 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]; let newTopHeight = e.clientY - containerOffsetTop; const min = this.getOption("dimension").min; const max = this.getOption("dimension").max; const topAsPercent = (newTopHeight / this[splitScreenElementSymbol].offsetHeight) * 100; if (parseInt(min) > topAsPercent) { newTopHeight = min; } else if (parseInt(max) < topAsPercent) { newTopHeight = max; } else { newTopHeight = topAsPercent + "%"; } // calc new top height to pixel const newTopHeightPx = (parseInt(newTopHeight) / 100) * this[splitScreenElementSymbol].offsetHeight; topPanel.style.height = `${newTopHeightPx}px`; bottomPanel.style.height = `calc(100% - ${newTopHeightPx}px - ${draggerWidth})`; // 5px is dragger height } else { const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft; const leftPanel = self[startPanelElementSymbol]; const rightPanel = self[endPanelElementSymbol]; let newLeftWidth = e.clientX - containerOffsetLeft; const min = this.getOption("dimension").min; const max = this.getOption("dimension").max; const leftAsPercent = (newLeftWidth / this[splitScreenElementSymbol].offsetWidth) * 100; if (parseInt(min) > leftAsPercent) { newLeftWidth = min; } else if (parseInt(max) < leftAsPercent) { newLeftWidth = max; } else { newLeftWidth = leftAsPercent + "%"; } leftPanel.style.width = `${newLeftWidth}`; rightPanel.style.width = `calc(100% - ${newLeftWidth} - ${draggerWidth})`; // 5px is dragger width } }; const dragEventHandler = (e) => { self[internalSymbol].getSubject().isDragging = false; document.removeEventListener("mousemove", eventListener); document.removeEventListener("mouseup", eventListener); }; document.addEventListener("mousemove", eventListener); document.addEventListener("mouseup", dragEventHandler); }); this[internalSymbol].attachObserver( new Observer(() => { let apply = false; if ( lastDimension !== this[internalSymbol].getSubject().currentDimension ) { lastDimension = this[internalSymbol].getSubject().currentDimension; apply = true; } if (lastType !== this.getOption("splitType")) { lastType = this.getOption("splitType"); apply = true; } if (apply) { applyPanelDimensions.call(this); } }), ); return this; } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="split-panel" part="control"> <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" part="handle"></div> </div> <div data-monster-role="endPanel" class="panel" part="endPanel"> <slot name="end"></slot> </div> </div>`; } registerCustomElement(SplitPanel);