@opensig/opendesign
Version:
420 lines (419 loc) • 13.1 kB
JavaScript
import { getElementSize, getOffsetElement, getScroll } from "../_utils/dom.mjs";
import { useOutClick } from "../hooks/use-out-click.mjs";
function getWrapperContentRect(wrapperEl, wrapperRect) {
const rect = wrapperRect || wrapperEl.getBoundingClientRect();
const left = rect.left + wrapperEl.clientLeft;
const top = rect.top + wrapperEl.clientTop;
return {
left,
top,
right: left + wrapperEl.clientWidth,
bottom: top + wrapperEl.clientHeight
};
}
const viewOffsetBuilders = {
top: (t, pSize, offset) => ({
left: t.left + t.width / 2 - pSize.width / 2,
top: t.top - offset - pSize.height
}),
bottom: (t, pSize, offset) => ({
left: t.left + t.width / 2 - pSize.width / 2,
top: t.bottom + offset
}),
left: (t, pSize, offset) => ({
left: t.left - offset - pSize.width,
top: t.top + t.height / 2 - pSize.height / 2
}),
right: (t, pSize, offset) => ({
left: t.right + offset,
top: t.top + t.height / 2 - pSize.height / 2
}),
tl: (t, pSize, offset) => ({
left: t.left,
top: t.top - offset - pSize.height
}),
tr: (t, pSize, offset) => ({
left: t.right - pSize.width,
top: t.top - offset - pSize.height
}),
bl: (t, _pSize, offset) => ({
left: t.left,
top: t.bottom + offset
}),
br: (t, pSize, offset) => ({
left: t.right - pSize.width,
top: t.bottom + offset
}),
lt: (t, pSize, offset) => ({
left: t.left - offset - pSize.width,
top: t.top
}),
lb: (t, pSize, offset) => ({
left: t.left - offset - pSize.width,
top: t.bottom - pSize.height
}),
rt: (t, _pSize, offset) => ({
left: t.right + offset,
top: t.top
}),
rb: (t, pSize, offset) => ({
left: t.right + offset,
top: t.bottom - pSize.height
})
};
function getPopupViewOffset(position, { t, pSize, offset = 0 }) {
return viewOffsetBuilders[position](t, pSize, offset);
}
function getWrapperViewEdge(popupSize, wrapperRect, edgeOffset = 0) {
const viewport = {
left: edgeOffset,
// 使用 document.documentElement.clientWidth 而非 window.innerWidth 是为了去除滚动条宽度
right: document.documentElement.clientWidth - popupSize.width - edgeOffset,
top: edgeOffset,
bottom: document.documentElement.clientHeight - popupSize.height - edgeOffset
};
if (!wrapperRect) {
return viewport;
}
return {
left: Math.max(viewport.left, wrapperRect.left),
top: Math.max(viewport.top, wrapperRect.top),
right: Math.min(viewport.right, wrapperRect.right - popupSize.width),
bottom: Math.min(viewport.bottom, wrapperRect.bottom - popupSize.height)
};
}
function getPopupWrapOffset(pos, { wrapperEl, wrapperContentRect }) {
if (!wrapperEl) {
return pos;
}
const cs = getScroll(wrapperEl);
const offsetX = cs.scrollLeft - ((wrapperContentRect == null ? void 0 : wrapperContentRect.left) ?? 0);
const offsetY = cs.scrollTop - ((wrapperContentRect == null ? void 0 : wrapperContentRect.top) ?? 0);
const tx = typeof pos.left === "number" ? pos.left + offsetX : 0;
const ty = typeof pos.top === "number" ? pos.top + offsetY : 0;
return { left: tx, top: ty };
}
const directionByPosition = {
tl: "top",
tr: "top",
top: "top",
bl: "bottom",
br: "bottom",
bottom: "bottom",
lt: "left",
lb: "left",
left: "left",
rt: "right",
rb: "right",
right: "right"
};
function getDirection(position) {
return directionByPosition[position];
}
const positionFlipMap = {
top: { bottom: "top", bl: "tl", br: "tr" },
bottom: { top: "bottom", tl: "bl", tr: "br" },
left: { right: "left", rt: "lt", rb: "lb" },
right: { left: "right", lt: "rt", lb: "rb" }
};
function adjustPosition(position, direction) {
return positionFlipMap[direction][position] ?? position;
}
function detectTopFlip({ pos, edge }) {
if (typeof pos.top !== "number") return null;
return edge.top > pos.top ? "bottom" : null;
}
function detectBottomFlip({ pos, edge }) {
if (typeof pos.top !== "number") return null;
return edge.bottom < pos.top ? "top" : null;
}
function detectLeftFlip({ pos, edge }) {
if (typeof pos.left !== "number") return null;
return edge.left > pos.left ? "right" : null;
}
function detectRightFlip({ pos, edge }) {
if (typeof pos.left !== "number") return null;
return edge.right < pos.left ? "left" : null;
}
const flipDetectors = {
top: detectTopFlip,
bottom: detectBottomFlip,
left: detectLeftFlip,
right: detectRightFlip
};
function maybeFlipDirection(position, ctx) {
const d = getDirection(position);
const style = { ...ctx.popupPosition };
const direction = flipDetectors[d]({
pos: ctx.popupPosition,
edge: ctx.edge,
popupSize: ctx.popupSize
});
if (direction === null) {
return { fixedPosition: position, style };
}
const fixedPosition = adjustPosition(position, direction);
return {
fixedPosition,
style: getPopupViewOffset(fixedPosition, { t: ctx.tRect, pSize: ctx.popupSize, offset: ctx.offset })
};
}
function clampLeft(left, edge) {
if (edge.left > left) {
return edge.left;
}
if (edge.right < left) {
return edge.right;
}
return left;
}
function clampTop(top, edge) {
if (edge.top > top) {
return edge.top;
}
if (edge.bottom < top) {
return edge.bottom;
}
return top;
}
function adjustOffset(position, { popupPosition, popupSize, tRect, wRect, offset, edgeOffset }) {
const edge = getWrapperViewEdge(popupSize, wRect, edgeOffset);
const flipped = maybeFlipDirection(position, {
popupPosition,
popupSize,
tRect,
edge,
offset
});
const style = { ...flipped.style };
if (typeof style.left === "number") {
style.left = clampLeft(style.left, edge);
}
if (typeof style.top === "number") {
style.top = clampTop(style.top, edge);
}
return {
position: flipped.fixedPosition,
popupStyle: style
};
}
const bottomEdgeRules = (x) => ({ left: `${x}px`, bottom: "0px" });
const topEdgeRules = (x) => ({ left: `${x}px`, top: "0px" });
const rightEdgeRules = (_x, y) => ({ top: `${y}px`, right: "0px" });
const leftEdgeRules = (_x, y) => ({ top: `${y}px`, left: "0px" });
const anchorRuleByPosition = {
top: bottomEdgeRules,
tl: bottomEdgeRules,
tr: bottomEdgeRules,
bottom: topEdgeRules,
bl: topEdgeRules,
br: topEdgeRules,
left: rightEdgeRules,
lt: rightEdgeRules,
lb: rightEdgeRules,
right: leftEdgeRules,
rt: leftEdgeRules,
rb: leftEdgeRules
};
function getAnchorOffset(position, { tRect, popupStyle, popupSize, anchorOffset = 8 }) {
const targetCenterX = tRect.left + tRect.width / 2;
const targetCenterY = tRect.top + tRect.height / 2;
const rawX = targetCenterX - (popupStyle.left ?? 0);
const rawY = targetCenterY - (popupStyle.top ?? 0);
const x = Math.min(Math.max(rawX, anchorOffset), popupSize.width - anchorOffset);
const y = Math.min(Math.max(rawY, anchorOffset), popupSize.height - anchorOffset);
return anchorRuleByPosition[position](x, y);
}
function resolveWrapperContext(popupEl) {
const wrapperEl = getOffsetElement(popupEl);
if (!wrapperEl) {
return { wrapperEl: null, isWrapperBounded: false };
}
const wrapperRect = wrapperEl.getBoundingClientRect();
const wrapperContentRect = wrapperEl.nodeName === "HTML" ? void 0 : getWrapperContentRect(wrapperEl, wrapperRect);
let isWrapperBounded = false;
if (wrapperContentRect && typeof window !== "undefined") {
const cs = window.getComputedStyle(wrapperEl);
isWrapperBounded = cs.overflowX !== "visible" || cs.overflowY !== "visible";
}
return { wrapperEl, wrapperContentRect, isWrapperBounded };
}
function calcPopupStyle({
popupEl,
targetEl,
position,
adaptive = true,
anchor = true,
anchorOffset = 8,
offset = 8,
edgeOffset = 0
}) {
const tRect = targetEl.getBoundingClientRect();
const popupSize = getElementSize(popupEl);
const { wrapperEl, wrapperContentRect, isWrapperBounded } = resolveWrapperContext(popupEl);
const emptyAnchor = {};
if (!wrapperEl) {
return {
popupStyle: getPopupViewOffset(position, { t: tRect, pSize: popupSize, offset }),
position,
anchorStyle: emptyAnchor
};
}
let popupStyle = getPopupViewOffset(position, { t: tRect, pSize: popupSize, offset });
let fixedPosition = position;
if (adaptive) {
const rlt = adjustOffset(position, {
popupPosition: popupStyle,
popupSize,
tRect,
wRect: isWrapperBounded ? wrapperContentRect : void 0,
offset,
edgeOffset
});
fixedPosition = rlt.position;
popupStyle = rlt.popupStyle;
}
const anchorStyle = anchor ? getAnchorOffset(fixedPosition, { tRect, popupStyle, popupSize, anchorOffset }) : emptyAnchor;
popupStyle = getPopupWrapOffset(popupStyle, { wrapperEl, wrapperContentRect });
return {
position: fixedPosition,
popupStyle,
anchorStyle
};
}
function isInsidePopup(event, popupRef) {
var _a;
return !!((_a = popupRef.value) == null ? void 0 : _a.contains(event.target));
}
function buildClickHandler({ el, autoHide, outClick, popupRef, updateFn, inner }) {
return () => {
inner();
if (autoHide) {
const onOutsideClick = () => {
updateFn(false);
outClick.removeListener(el, onOutsideClick);
};
outClick.addListener(el, onOutsideClick, {
exception: (e) => isInsidePopup(e, popupRef)
});
}
};
}
function bindClickTrigger(ctx, toggle) {
const { el, autoHide, outClick, listeners, popupRef, updateFn } = ctx;
const inner = toggle ? () => updateFn() : () => updateFn(true);
const fn = buildClickHandler({ el, autoHide, outClick, popupRef, updateFn, inner });
el.addEventListener("click", fn);
listeners.push(() => el.removeEventListener("click", fn));
}
function bindDomEvent({ el, type, handler, listeners }) {
el.addEventListener(type, handler);
listeners.push(() => el.removeEventListener(type, handler));
}
function bindHoverTrigger(ctx) {
const { el, autoHide, hoverDelay, listeners, updateFn } = ctx;
bindDomEvent({ el, type: "mouseenter", handler: () => updateFn(true, hoverDelay), listeners });
if (autoHide) {
bindDomEvent({ el, type: "mouseleave", handler: () => updateFn(false, hoverDelay), listeners });
}
}
function bindFocusTrigger(ctx) {
const { el, autoHide, listeners, updateFn } = ctx;
bindDomEvent({ el, type: "focusin", handler: () => updateFn(true), listeners });
if (autoHide) {
bindDomEvent({ el, type: "focusout", handler: () => updateFn(false), listeners });
}
}
function bindContextmenuTrigger(ctx) {
const { el, autoHide, outClick, popupRef, listeners, updateFn } = ctx;
bindDomEvent({
el,
type: "contextmenu",
handler: (e) => {
e.preventDefault();
updateFn(true);
},
listeners
});
if (autoHide) {
const hideFn = () => updateFn(false);
outClick.addListener(el, hideFn, {
exception: (e) => isInsidePopup(e, popupRef)
});
listeners.push(() => outClick.removeListener(el, hideFn));
}
}
function bindHoverOutclickTrigger(ctx) {
const { el, autoHide, hoverDelay, outClick, popupRef, listeners, updateFn } = ctx;
bindDomEvent({ el, type: "mouseenter", handler: () => updateFn(true, hoverDelay), listeners });
if (autoHide) {
const hideFn = () => updateFn(false);
outClick.addListener(el, hideFn, {
exception: (e) => isInsidePopup(e, popupRef)
});
listeners.push(() => outClick.removeListener(el, hideFn));
}
}
function toClickCtx(ctx) {
return {
el: ctx.el,
popupRef: ctx.popupRef,
autoHide: ctx.autoHide,
outClick: ctx.outClick,
listeners: ctx.listeners,
updateFn: ctx.updateFn
};
}
function noop() {
}
const triggerBinders = {
hover: bindHoverTrigger,
focus: bindFocusTrigger,
contextmenu: bindContextmenuTrigger,
none: noop,
"hover-outclick": bindHoverOutclickTrigger
};
const clickToggleMap = {
click: true,
"click-outclick": false
};
function bindTrigger({ el, popupRef, triggers, updateFn, hoverDelay = 100, autoHide = true }) {
if (!el) {
return [];
}
const outClick = useOutClick();
const listeners = [];
const ctx = { el, popupRef, autoHide, hoverDelay, outClick, listeners, updateFn };
const clickCtx = toClickCtx(ctx);
triggers.forEach((tr) => {
var _a;
if (tr === "click" || tr === "click-outclick") {
bindClickTrigger(clickCtx, clickToggleMap[tr]);
return;
}
(_a = triggerBinders[tr]) == null ? void 0 : _a.call(triggerBinders, ctx);
});
return listeners;
}
const TRANSFORM_ORIGIN_MAP = {
top: { left: "50%", top: "100%" },
tl: { left: "0px", top: "100%" },
tr: { left: "100%", top: "100%" },
bottom: { left: "50%", top: "0px" },
bl: { left: "0px", top: "0px" },
br: { left: "100%", top: "0px" },
left: { left: "100%", top: "50%" },
lt: { left: "100%", top: "100%" },
lb: { left: "100%", top: "100%" },
right: { left: "0px", top: "50%" },
rt: { left: "0px", top: "0px" },
rb: { left: "0px", top: "100%" }
};
function getTransformOrigin(position) {
return TRANSFORM_ORIGIN_MAP[position];
}
export {
bindTrigger,
calcPopupStyle,
getTransformOrigin
};