Skip to content
Snippets Groups Projects
Select Git revision
  • 2cea82d4aced6a1a2361c0a64bc4517472f7afee
  • master default protected
  • 1.31
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
  • 4.28.0
  • 4.27.0
  • 4.26.0
  • 4.25.5
  • 4.25.4
  • 4.25.3
  • 4.25.2
  • 4.25.1
23 results

split-panel.mjs

Blame
  • split-panel.mjs 11.81 KiB
    /**
     * 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);