Something went wrong on our end
Select Git revision
split-panel.mjs
-
Volker Schukai authoredVolker Schukai authored
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);