ttrpg-tools/src/components/md-pin.tsx

126 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { customElement, noShadowDOM } from "solid-element";
import { createSignal, onMount, onCleanup, Show } from "solid-js";
customElement("md-pin", { x: 0, y: 0 }, (props, { element }) => {
noShadowDOM();
const [position, setPosition] = createSignal<{ top: string; left: string }>({ top: "0", left: "0" });
const [visible, setVisible] = createSignal(false);
const [transformStyle, setTransformStyle] = createSignal<string>("");
let pinContainer: HTMLSpanElement | undefined;
let targetImage: HTMLImageElement | undefined;
let resizeObserver: ResizeObserver | undefined;
// 从 element 的 textContent 获取 pin 标签内容
const label = element?.textContent?.trim() || "";
// 隐藏原始文本内容
if (element) {
element.textContent = "";
}
// 查找上方最近的图片
const findNearestImage = (): HTMLImageElement | null => {
if (!element) return null;
// 从当前元素向上查找
let current: Element | null = element;
while (current) {
// 在当前元素的之前兄弟节点中查找图片
let sibling: Element | null = current.previousElementSibling;
while (sibling) {
// 检查是否是图片元素
const img = sibling.querySelector('img');
if (img) return img;
// 检查元素本身是否有图片相关的类或标签
if (sibling.tagName === 'IMG') return sibling as HTMLImageElement;
sibling = sibling.previousElementSibling;
}
current = current.parentElement;
}
return null;
};
// 更新 pin 位置和容器样式
const updatePosition = () => {
if (!targetImage || !pinContainer) return;
const imgRect = targetImage.getBoundingClientRect();
const containerRect = pinContainer.parentElement.getBoundingClientRect();
// 计算图片左上角相对于容器原始位置的偏移
const offsetX = imgRect.left - containerRect.left;
const offsetY = imgRect.top - containerRect.top;
// 使用 transform 将容器移动到图片位置
setTransformStyle(`translate(${offsetX}px, ${offsetY}px)`);
// 计算 pin 在图片内的相对位置x/y 是百分比)
const x = typeof props.x === 'number' ? props.x : parseFloat(props.x) || 0;
const y = typeof props.y === 'number' ? props.y : parseFloat(props.y) || 0;
const left = (x / 100) * imgRect.width;
const top = (y / 100) * imgRect.height;
setPosition({
left: `${left}px`,
top: `${top}px`
});
};
onMount(() => {
// 查找目标图片
targetImage = findNearestImage();
if (targetImage) {
// 初始定位
updatePosition();
// 使用 ResizeObserver 监听图片大小变化
resizeObserver = new ResizeObserver(() => {
updatePosition();
});
resizeObserver.observe(targetImage);
// 延迟显示以等待位置计算完成
requestAnimationFrame(() => {
setVisible(true);
});
} else {
console.warn('md-pin: 未找到目标图片');
}
});
onCleanup(() => {
if (resizeObserver && targetImage) {
resizeObserver.unobserve(targetImage);
}
});
return (
<div
ref={pinContainer}
class="md-pin-container"
style={{ transform: transformStyle(), 'transform-origin': 'top left' }}
>
<Show when={visible() && targetImage}>
<span
class="md-pin absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto
bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6
flex items-center justify-center shadow-lg
z-10"
style={{
left: position().left,
top: position().top
}}
>
{label || '📍'}
</span>
</Show>
</div>
);
});