@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
253 lines (218 loc) • 6.42 kB
JavaScript
/**
* 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 {
arrow,
autoPlacement,
autoUpdate,
detectOverflow,
computePosition,
offset,
flip,
size,
shift,
} from "@floating-ui/dom";
import { Processing } from "../../../util/processing.mjs";
import { getDocument } from "../../../dom/util.mjs";
import { isString, isArray, isObject, isFunction } from "../../../types/is.mjs";
export { positionPopper };
/**
* @private
* @param controlElement
* @param popperElement
* @param options
* @return {Promise|*}
*/
function positionPopper(controlElement, popperElement, options) {
const body = getDocument().body;
return new Processing(() => {
const arrowElement = controlElement.querySelector(
"[data-monster-role=arrow]",
);
const config = Object.assign(
{},
{
placement: "top",
},
options,
);
const middleware = config?.["middleware"];
if (!isArray(middleware)) {
if (isString(middleware)) {
config["middleware"] = middleware.split(",").filter((line) => {
return line.trim().length > 0;
});
} else {
config["middleware"] = [];
}
}
for (const key in middleware) {
const line = middleware[key];
if (isFunction(line)) {
continue;
}
if (isObject(line)) {
continue;
}
if (!isString(line)) {
throw new Error(
`Middleware must be a string, a function or an object. Got ${typeof line}`,
);
}
const kv = line.split(":");
const fn = kv.shift();
switch (fn) {
case "flip":
config["middleware"][key] = flip();
break;
case "shift":
config["middleware"][key] = shift();
break;
case "autoPlacement":
let defaultAllowedPlacements = ["top", "bottom", "left", "right"];
const defPlacement = kv?.shift();
if (isString(defPlacement) && defPlacement.trim().length > 0) {
defaultAllowedPlacements = defPlacement
.split(",")
.filter((line) => {
return line.trim().length > 0;
});
}
if (defaultAllowedPlacements.includes(config.placement)) {
defaultAllowedPlacements.splice(
defaultAllowedPlacements.indexOf(config.placement),
1,
);
}
defaultAllowedPlacements.unshift(config.placement);
config["middleware"][key] = autoPlacement({
crossAxis: true,
autoAlignment: true,
allowedPlacements: defaultAllowedPlacements,
});
break;
case "detectOverflow":
config["middleware"][key] = detectOverflow();
break;
case "arrow":
if (arrowElement) {
config["middleware"][key] = arrow({ element: arrowElement });
}
break;
case "size":
config["middleware"][key] = size({
apply({ availableWidth, availableHeight, elements }) {
const maxWidth = body.clientWidth;
const maxHeight = body.clientHeight;
if (availableWidth < 0) {
availableWidth = 0;
}
if (availableHeight < 0) {
availableHeight = 0;
}
if (availableWidth > maxWidth) {
availableWidth = maxWidth;
}
if (availableHeight > maxHeight) {
availableHeight = maxHeight;
}
Object.assign(elements.floating.style, {
boxSizing: "border-box",
maxWidth: `${availableWidth}px`,
maxHeight: `${availableHeight}px`,
});
},
});
break;
case "offset":
const o = kv?.shift();
config["middleware"][key] = offset(parseInt(o) || 10);
break;
case "hide":
config["middleware"][key] = hide();
break;
default:
throw new Error(`Unknown function: ${fn}`);
}
}
popperElement.style.removeProperty("visibility");
popperElement.style.display = "block";
autoUpdate(controlElement, popperElement, () => {
computePosition(controlElement, popperElement, config).then(
({ x, y, placement, middlewareData }) => {
Object.assign(popperElement.style, {
top: "0",
left: "0",
transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`,
});
if (middlewareData.arrow) {
const side = placement.split("-")[0];
const staticSide = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
}[side];
// monster-border-width = + 4 (2*2) (should come from css)
const arrowLen = arrowElement.offsetWidth + 4;
const borderStyle = {
borderLeft: "transparent",
borderRight: "transparent",
borderBottom: "transparent",
borderTop: "transparent",
};
const defaultBorder =
"var(--monster-border-width) var(--monster-border-style) var(--monster-bg-color-primary-4)";
switch (side) {
case "top":
borderStyle.borderRight = defaultBorder;
borderStyle.borderBottom = defaultBorder;
break;
case "bottom":
borderStyle.borderTop = defaultBorder;
borderStyle.borderLeft = defaultBorder;
break;
case "left":
borderStyle.borderRight = defaultBorder;
borderStyle.borderTop = defaultBorder;
break;
case "right":
borderStyle.borderBottom = defaultBorder;
borderStyle.borderLeft = defaultBorder;
break;
}
const { x, y } = middlewareData.arrow;
Object.assign(
arrowElement.style,
{
left: x != null ? `${x}px` : "",
top: y != null ? `${y}px` : "",
// Ensure the static side gets unset when
// flipping to other placements' axes.
right: "",
bottom: "",
[staticSide]: `${-arrowLen / 2}px`,
transform: "rotate(45deg)",
},
borderStyle,
);
}
},
);
});
}).run();
}
function roundByDPR(value) {
const dpr = window.devicePixelRatio || 1;
return Math.round(value * dpr) / dpr;
}