daily: 2021-09-26
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import Styles from "./index.module.css";
|
||||
|
||||
// ENHANCE: smart placement
|
||||
function PopoverOnSvg({
|
||||
children,
|
||||
renderContent,
|
||||
renderPopover,
|
||||
darkMode,
|
||||
globalClassName,
|
||||
offset,
|
||||
placement,
|
||||
style,
|
||||
title,
|
||||
trigger,
|
||||
visible,
|
||||
autoHide,
|
||||
// TODO: complete this
|
||||
showArrow,
|
||||
onVisibilityChange,
|
||||
}) {
|
||||
const containerRef = useRef();
|
||||
const triggerBoxRef = useRef({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
});
|
||||
const initFlagRef = useRef(false);
|
||||
const triggerRef = useRef();
|
||||
const popoverRef = useRef();
|
||||
// initially, always set innerVisible to false. If the prop *visible* is
|
||||
// true, we'll update it after dom mounted, because we cannot calculate
|
||||
// correct position of popover before react mounting it.
|
||||
const [innerVisible, setInnerVisible] = useState(false);
|
||||
const [, forceUpdate] = useState();
|
||||
const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean);
|
||||
const arrowClassNames = [Styles.arrow];
|
||||
const headerClassNames = [Styles.header];
|
||||
let offsetX = (offset && offset.x) || 0;
|
||||
let offsetY = (offset && offset.y) || 0;
|
||||
let mergedStyles = {
|
||||
transform: "",
|
||||
};
|
||||
const arrowStyles = {};
|
||||
placement = placement.toLowerCase();
|
||||
|
||||
const makeOffset = function (x, y = 0) {
|
||||
offsetX += x;
|
||||
offsetY += y;
|
||||
};
|
||||
const changeVisibility = useCallback(
|
||||
function (shouldShow) {
|
||||
shouldShow = shouldShow ?? !innerVisible;
|
||||
let newVisibility = visible === undefined ? shouldShow : visible;
|
||||
if (newVisibility) {
|
||||
if (!triggerRef.current) {
|
||||
setTimeout(changeVisibility, 0);
|
||||
return;
|
||||
}
|
||||
// re-get box of trigger element
|
||||
triggerBoxRef.current = triggerRef.current.getBoundingClientRect();
|
||||
}
|
||||
setInnerVisible(newVisibility);
|
||||
// FIXME: visible由false变为true时React Warning: Cannot update a component (`EditableContent`) while rendering a different component (`PopoverOnSvg`).
|
||||
onVisibilityChange && onVisibilityChange(shouldShow);
|
||||
},
|
||||
[visible, innerVisible, onVisibilityChange]
|
||||
);
|
||||
const handleContextMenu = useCallback(
|
||||
(ev) => {
|
||||
// TODO: working
|
||||
console.log(ev.pageX, ev.pageY)
|
||||
ev.preventDefault();
|
||||
changeVisibility(true);
|
||||
},
|
||||
[changeVisibility]
|
||||
);
|
||||
useEffect(() => {
|
||||
initFlagRef.current = true;
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (autoHide === false) {
|
||||
return;
|
||||
}
|
||||
const handler = (ev) => {
|
||||
if (ev.type === "keydown" && ev.key === "Escape") {
|
||||
changeVisibility(false);
|
||||
} else if (ev.type === "mouseup") {
|
||||
for (let target = ev.target; target; target = target.parentElement) {
|
||||
if (target === popoverRef.current) {
|
||||
return;
|
||||
} else if (target === triggerRef.current) {
|
||||
if (trigger === "context" && ev.button !== 2) {
|
||||
continue;
|
||||
} else if (trigger === "click" && ev.button !== 0) {
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
changeVisibility(false);
|
||||
}
|
||||
};
|
||||
if (innerVisible) {
|
||||
document.addEventListener("mouseup", handler);
|
||||
document.addEventListener("keydown", handler);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("mouseup", handler);
|
||||
document.removeEventListener("keydown", handler);
|
||||
};
|
||||
}, [innerVisible, autoHide, changeVisibility]);
|
||||
|
||||
if (visible !== undefined && visible !== innerVisible) {
|
||||
if (initFlagRef.current) {
|
||||
changeVisibility();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
// this is the first-time render, trigger dom has not mounted, so we
|
||||
// have to delay the rendering
|
||||
forceUpdate([]);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
if (!containerRef.current) {
|
||||
containerRef.current = document.body.querySelector(
|
||||
"body > div#svg-popover-container"
|
||||
);
|
||||
if (!containerRef.current) {
|
||||
containerRef.current = document.createElement("div");
|
||||
containerRef.current.id = "svg-popover-container";
|
||||
document.body.appendChild(containerRef.current);
|
||||
}
|
||||
}
|
||||
const box = triggerBoxRef.current;
|
||||
switch (true) {
|
||||
case /^(left|right)(top|center|bottom)?$/i.test(placement):
|
||||
mergedStyles.minHeight = "32px";
|
||||
if (placement.startsWith("right")) {
|
||||
mergedStyles.left = box.right + "px";
|
||||
} else {
|
||||
mergedStyles.left = box.left + "px";
|
||||
mergedStyles.transform += " translate(-100%, 0)";
|
||||
}
|
||||
if (placement.endsWith("top")) {
|
||||
mergedStyles.top = box.top + "px";
|
||||
mergedStyles.transform += " translate(0, 0)";
|
||||
arrowStyles.top = "16px";
|
||||
} else if (placement.endsWith("bottom")) {
|
||||
mergedStyles.top = box.bottom + "px";
|
||||
mergedStyles.transform += " translate(0, -100%)";
|
||||
arrowStyles.bottom = "0";
|
||||
} else {
|
||||
mergedStyles.top = box.y + box.height / 2 + "px";
|
||||
mergedStyles.transform += " translate(0, -50%)";
|
||||
arrowStyles.top = "50%";
|
||||
}
|
||||
break;
|
||||
case /^(top|bottom)(left|center|right)?$/i.test(placement):
|
||||
default:
|
||||
if (placement.startsWith("bottom")) {
|
||||
mergedStyles.top = box.bottom + "px";
|
||||
} else {
|
||||
mergedStyles.top = box.y + "px";
|
||||
mergedStyles.transform += " translate(0, -100%)";
|
||||
}
|
||||
if (placement.endsWith("left")) {
|
||||
mergedStyles.left = box.left + "px";
|
||||
arrowStyles.left = "16px";
|
||||
} else if (placement.endsWith("right")) {
|
||||
mergedStyles.left = box.right + "px";
|
||||
mergedStyles.transform += " translate(-100%, 0)";
|
||||
arrowStyles.right = "0";
|
||||
} else {
|
||||
mergedStyles.left = box.right - box.width / 2 + "px";
|
||||
mergedStyles.transform += " translateX(-50%)";
|
||||
arrowStyles.left = "50%";
|
||||
}
|
||||
break;
|
||||
}
|
||||
switch (true) {
|
||||
case !showArrow:
|
||||
break;
|
||||
case placement.startsWith("left"):
|
||||
makeOffset(-8);
|
||||
arrowClassNames.push("left");
|
||||
break;
|
||||
case placement.startsWith("right"):
|
||||
makeOffset(8);
|
||||
arrowClassNames.push("right");
|
||||
break;
|
||||
case placement.startsWith("bottom"):
|
||||
makeOffset(0, 8);
|
||||
arrowClassNames.push("bottom");
|
||||
break;
|
||||
case placement.startsWith("top"):
|
||||
default:
|
||||
makeOffset(0, -8);
|
||||
arrowClassNames.push("top");
|
||||
break;
|
||||
}
|
||||
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
|
||||
if (darkMode) {
|
||||
popoverClassNames.push("dark");
|
||||
arrowClassNames.push("dark");
|
||||
headerClassNames.push("dark");
|
||||
}
|
||||
mergedStyles = Object.assign(mergedStyles, style);
|
||||
return (
|
||||
<>
|
||||
[]
|
||||
<g
|
||||
type="noop"
|
||||
ref={triggerRef}
|
||||
onClick={trigger === "click" ? () => changeVisibility() : undefined}
|
||||
onMouseEnter={
|
||||
trigger === "hover" ? () => changeVisibility(true) : undefined
|
||||
}
|
||||
onMouseLeave={
|
||||
trigger === "hover" ? () => changeVisibility(false) : undefined
|
||||
}
|
||||
onContextMenu={trigger === "context" ? handleContextMenu : undefined}
|
||||
>
|
||||
{children}
|
||||
</g>
|
||||
{innerVisible &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className={popoverClassNames.join(" ")}
|
||||
style={mergedStyles}
|
||||
>
|
||||
{renderPopover ? (
|
||||
typeof renderPopover === "function" ? (
|
||||
renderPopover()
|
||||
) : (
|
||||
renderPopover
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={arrowClassNames.join(" ")}
|
||||
style={arrowStyles}
|
||||
></div>
|
||||
{title && (
|
||||
<header className={headerClassNames.join(" ")}>
|
||||
{title}
|
||||
</header>
|
||||
)}
|
||||
<div className={title ? Styles.content : null}>
|
||||
{typeof renderContent === "function"
|
||||
? renderContent()
|
||||
: renderContent ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
containerRef.current
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PopoverOnSvg.defaultProps = {
|
||||
trigger: "click",
|
||||
placement: "top",
|
||||
};
|
||||
|
||||
export default React.memo(PopoverOnSvg);
|
||||
Reference in New Issue
Block a user