daily: 2021-09-26

This commit is contained in:
2021-09-27 00:40:21 +08:00
parent 93e19c05af
commit fb671cd62a
22 changed files with 2295 additions and 64 deletions
+275
View File
@@ -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);