void) | null = null;
let lastSelectedRowAt = 0;
let hoverLayerIds = getHoverLayerIds(map);
@@ -98,14 +99,23 @@ export function useMapHoverPopup({
selectedRowIndex = wrapIndex(selectedRowIndex, rows.length);
lastSelectedRowClick = rows[selectedRowIndex]?.onClick || null;
lastSelectedRowAt = Date.now();
- updatePopupRowSelection(popup, selectedRowIndex, selectionVisible);
+ updatePopupRowSelection(popup, selectedRowIndex, selectionVisible, selectionDirection);
};
const cyclePopupRow = (direction: "next" | "prev") => {
const rows = getCurrentRows();
if (!rows.length) return;
- selectedRowIndex += direction === "prev" ? -1 : 1;
- selectedRowIndex = wrapIndex(selectedRowIndex, rows.length);
+ const currentIndex = wrapIndex(selectedRowIndex, rows.length);
+ const nextIndex = direction === "prev"
+ ? Math.max(0, currentIndex - 1)
+ : Math.min(rows.length - 1, currentIndex + 1);
+ selectionDirection = direction;
+ if (nextIndex === currentIndex) {
+ selectedRowIndex = currentIndex;
+ syncSelectedRow();
+ return;
+ }
+ selectedRowIndex = nextIndex;
syncSelectedRow();
};
@@ -153,7 +163,7 @@ export function useMapHoverPopup({
if (event.repeat) return;
if (isEditableEventTarget(event.target)) return;
selectionVisible = true;
- updatePopupRowSelection(popup, selectedRowIndex, selectionVisible);
+ updatePopupRowSelection(popup, selectedRowIndex, selectionVisible, selectionDirection);
requestPopupUpdateFromLastMouseEvent();
return;
}
@@ -488,7 +498,7 @@ function buildPopupNode(content: MapHoverPopupContent, selectedRowIndex: number,
grid.appendChild(card);
});
- updatePopupNodeRowSelection(root, selectedRowIndex, selectionVisible);
+ updatePopupNodeRowSelection(root, selectedRowIndex, selectionVisible, null);
return root;
}
@@ -598,11 +608,21 @@ function wrapIndex(index: number, length: number): number {
return ((index % length) + length) % length;
}
-function updatePopupRowSelection(popup: maplibregl.Popup, selectedRowIndex: number, selectionVisible: boolean) {
- updatePopupNodeRowSelection(popup.getElement() || null, selectedRowIndex, selectionVisible);
+function updatePopupRowSelection(
+ popup: maplibregl.Popup,
+ selectedRowIndex: number,
+ selectionVisible: boolean,
+ selectionDirection: "next" | "prev" | null
+) {
+ updatePopupNodeRowSelection(popup.getElement() || null, selectedRowIndex, selectionVisible, selectionDirection);
}
-function updatePopupNodeRowSelection(root: HTMLElement | null, selectedRowIndex: number, selectionVisible: boolean) {
+function updatePopupNodeRowSelection(
+ root: HTMLElement | null,
+ selectedRowIndex: number,
+ selectionVisible: boolean,
+ selectionDirection: "next" | "prev" | null
+) {
if (!root) return;
const cards = Array.from(root.querySelectorAll
("[data-hover-popup-row-index]"));
let selectedCard: HTMLElement | null = null;
@@ -616,8 +636,8 @@ function updatePopupNodeRowSelection(root: HTMLElement | null, selectedRowIndex:
}
}
if (selectedCard) {
- ensurePopupRowVisible(selectedCard);
- window.requestAnimationFrame(() => ensurePopupRowVisible(selectedCard));
+ ensurePopupRowVisible(selectedCard, selectionDirection);
+ window.requestAnimationFrame(() => ensurePopupRowVisible(selectedCard, selectionDirection));
}
}
@@ -627,20 +647,20 @@ function applyPopupRowStyle(card: HTMLElement, selected: boolean) {
card.style.boxShadow = selected ? "inset 2px 0 0 rgba(56, 189, 248, 0.95)" : "none";
}
-function ensurePopupRowVisible(card: HTMLElement) {
+function ensurePopupRowVisible(card: HTMLElement, direction: "next" | "prev" | null) {
const scrollRoot = card.closest("[data-hover-popup-scroll-root='true']");
if (!scrollRoot) return;
const padding = 8;
- const groupHeader = findPreviousGroupHeader(card);
+ const groupHeader = direction === "prev" ? findPreviousGroupHeader(card) : null;
const cardTop = card.offsetTop;
const cardBottom = cardTop + card.offsetHeight;
- const contextTop = groupHeader?.offsetTop ?? cardTop;
+ const targetTop = groupHeader?.offsetTop ?? cardTop;
const visibleTop = scrollRoot.scrollTop + padding;
const visibleBottom = scrollRoot.scrollTop + scrollRoot.clientHeight - padding;
- if (contextTop < visibleTop) {
- scrollRoot.scrollTop = Math.max(0, contextTop - padding);
+ if (targetTop < visibleTop) {
+ scrollRoot.scrollTop = Math.max(0, targetTop - padding);
return;
}
diff --git a/src/uhm/components/preview/PinnedWikiPopup.tsx b/src/uhm/components/preview/PinnedWikiPopup.tsx
deleted file mode 100644
index 2885ca6..0000000
--- a/src/uhm/components/preview/PinnedWikiPopup.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-"use client";
-
-import { useEffect, useRef } from "react";
-import type { Entity } from "@/uhm/api/entities";
-import type { Wiki } from "@/uhm/api/wikis";
-
-type PopupRow = {
- entity: Entity;
- wiki: Wiki | null;
- quote: string;
-};
-
-type Props = {
- rows: PopupRow[];
- featureId: string | number;
- top: number;
- left: number;
- onClose: () => void;
- onSelectRow: (entityId: string, wikiId?: string) => void;
-};
-
-export default function PinnedWikiPopup({
- rows,
- featureId,
- top,
- left,
- onClose,
- onSelectRow,
-}: Props) {
- const containerRef = useRef(null);
-
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "Escape") {
- onClose();
- }
- };
-
- const handlePointerDown = (event: PointerEvent) => {
- const target = event.target as Node | null;
- if (target && containerRef.current?.contains(target)) {
- return;
- }
- onClose();
- };
-
- window.addEventListener("keydown", handleKeyDown);
- window.addEventListener("pointerdown", handlePointerDown);
-
- return () => {
- window.removeEventListener("keydown", handleKeyDown);
- window.removeEventListener("pointerdown", handlePointerDown);
- };
- }, [onClose]);
-
- return (
-
-
-
-
- {rows.map(({ entity, wiki, quote }) => (
-
- ))}
-
-
-
-
- );
-}
diff --git a/src/uhm/components/preview/PreviewLayout.tsx b/src/uhm/components/preview/PreviewLayout.tsx
index 5654d09..612e515 100644
--- a/src/uhm/components/preview/PreviewLayout.tsx
+++ b/src/uhm/components/preview/PreviewLayout.tsx
@@ -10,7 +10,7 @@ import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerP
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
import TimelineBar from "@/uhm/components/ui/TimelineBar";
import RelatedEntityPopup from "./RelatedEntityPopup";
-import PinnedWikiPopup from "./PinnedWikiPopup";
+import WikiSelectionPanel from "./WikiSelectionPanel";
import { fitMapToFeatureCollection } from "@/uhm/components/map/mapUtils";
import { fetchWikiById, type Wiki } from "@/uhm/api/wikis";
@@ -104,7 +104,8 @@ const PreviewLayout = forwardRef(({
// Preview specific UI states
const [previewWikiError, setPreviewWikiError] = useState(null);
const [isPreviewWikiLoading, setIsPreviewWikiLoading] = useState(false);
- const [previewPinnedWikiPopupAnchor, setPreviewPinnedWikiPopupAnchor] = useState(null);
+ const [previewWikiSelectionPanelAnchor, setPreviewWikiSelectionPanelAnchor] = useState(null);
+ const [previewRightPanelMode, setPreviewRightPanelMode] = useState<"wiki" | "selection" | null>(null);
const [isPreviewEntitySidebarOpen, setIsPreviewEntitySidebarOpen] = useState(false);
const [previewLinkEntityPopup, setPreviewLinkEntityPopup] = useState<{
slug: string;
@@ -122,7 +123,8 @@ const PreviewLayout = forwardRef(({
setPreviewWikiCache({});
setPreviewWikiError(null);
setIsPreviewWikiLoading(false);
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
+ setPreviewRightPanelMode(null);
setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(false);
setPreviewLinkEntityPopup(null);
@@ -317,7 +319,9 @@ const PreviewLayout = forwardRef(({
: null;
- const isReplayPreviewWikiSidebarOpen = mode && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen);
+ const isPreviewWikiChooserOpen = previewRightPanelMode === "selection" && Boolean(previewWikiSelectionPanelAnchor);
+ const isReplayPreviewWikiSidebarOpen = mode && !isPreviewWikiChooserOpen && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen);
+ const isReplayPreviewRightPanelOpen = isReplayPreviewWikiSidebarOpen || isPreviewWikiChooserOpen;
// Handle replay preview entity selection
const selectReplayPreviewEntity = useCallback((
@@ -345,8 +349,9 @@ const PreviewLayout = forwardRef(({
setPreviewActiveEntityId(id);
setIsPreviewEntitySidebarOpen(true);
+ setPreviewRightPanelMode("wiki");
setPreviewWikiError(null);
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
setPreviewLinkEntityPopup(null);
if (options?.focusMap === true) {
@@ -368,6 +373,7 @@ const PreviewLayout = forwardRef(({
closeReplayPreviewWikiPanel();
setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(false);
+ setPreviewRightPanelMode(null);
setPreviewWikiError(null);
setPreviewLinkEntityPopup(null);
}, [closeReplayPreviewWikiPanel, setPreviewActiveEntityId]);
@@ -396,7 +402,8 @@ const PreviewLayout = forwardRef(({
setPreviewLinkEntityPopup(null);
if (!payload) {
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
+ setPreviewRightPanelMode(null);
return;
}
@@ -406,15 +413,12 @@ const PreviewLayout = forwardRef(({
if (!entity) return [];
const linkedWikis = previewRelations.entityWikisById[entity.id] || [];
- if (!linkedWikis.length) {
- return [{ entity, wiki: null as Wiki | null }];
- }
-
return linkedWikis.map((wiki) => ({ entity, wiki }));
});
if (!rows.length) {
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
+ setPreviewRightPanelMode(null);
return;
}
@@ -422,20 +426,28 @@ const PreviewLayout = forwardRef(({
const row = rows[0];
selectReplayPreviewEntity(row.entity.id, {
sourceFeatureId: payload.featureId,
- preferredWikiId: row.wiki?.id,
+ preferredWikiId: row.wiki.id,
focusMap: false,
selectGeometry: false,
});
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
+ setPreviewRightPanelMode("wiki");
return;
}
- setPreviewPinnedWikiPopupAnchor(payload);
+ closeReplayPreviewWikiPanel();
+ setPreviewActiveEntityId(null);
+ setIsPreviewEntitySidebarOpen(false);
+ setPreviewWikiError(null);
+ setPreviewWikiSelectionPanelAnchor(payload);
+ setPreviewRightPanelMode("selection");
}, [
+ closeReplayPreviewWikiPanel,
previewRelations.entitiesById,
previewRelations.entityWikisById,
previewRelations.geometryEntityIds,
selectReplayPreviewEntity,
+ setPreviewActiveEntityId,
]);
// Hover popup content provider
@@ -617,7 +629,7 @@ const PreviewLayout = forwardRef(({
setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(true);
setPreviewWikiError(null);
- setPreviewPinnedWikiPopupAnchor(null);
+ setPreviewWikiSelectionPanelAnchor(null);
setPreviewLinkEntityPopup(null);
openReplayPreviewWikiPanelById(wiki.id);
}
@@ -628,7 +640,7 @@ const PreviewLayout = forwardRef(({
}, [geometryVisibility]);
const computedTimelineStyle = useMemo(() => {
- const rightMargin = (isReplayPreviewWikiSidebarOpen && isLargeScreen)
+ const rightMargin = (isReplayPreviewRightPanelOpen && isLargeScreen)
? previewSidebarWidth + 32
: 18;
return {
@@ -636,31 +648,27 @@ const PreviewLayout = forwardRef(({
right: `${rightMargin}px`,
transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
};
- }, [isReplayPreviewWikiSidebarOpen, isLargeScreen, previewSidebarWidth]);
+ }, [isReplayPreviewRightPanelOpen, isLargeScreen, previewSidebarWidth]);
- // Popup PinnedWikiPopup rows
- const previewPinnedWikiPopupRows = useMemo(() => {
- if (!previewPinnedWikiPopupAnchor) return [];
+ // WikiSelectionPanel rows
+ const previewWikiSelectionPanelRows = useMemo(() => {
+ if (!previewWikiSelectionPanelAnchor) return [];
- const entityIds = previewRelations.geometryEntityIds[String(previewPinnedWikiPopupAnchor.featureId)] || [];
+ const entityIds = previewRelations.geometryEntityIds[String(previewWikiSelectionPanelAnchor.featureId)] || [];
return entityIds.flatMap((entityId) => {
const entity = previewRelations.entitiesById[entityId] || null;
if (!entity) return [];
const linkedWikis = previewRelations.entityWikisById[entity.id] || [];
- if (!linkedWikis.length) {
- return [{ entity, wiki: null as Wiki | null, quote: "" }];
- }
-
return linkedWikis.map((wiki) => ({
entity,
wiki,
- quote: extractWikiBlockquoteText(wiki.content),
+ quote: cleanWikiPreviewQuote(wiki.preview_quote) || extractWikiBlockquoteText(wiki.content),
}));
});
- }, [previewPinnedWikiPopupAnchor, previewRelations]);
+ }, [previewWikiSelectionPanelAnchor, previewRelations]);
useImperativeHandle(ref, () => ({
handleFeatureClick: handlePreviewMapFeatureClick,
@@ -730,6 +738,39 @@ const PreviewLayout = forwardRef(({
) : null}
+ {isPreviewWikiChooserOpen ? (
+
+ ) : null}
+
- {previewPinnedWikiPopupAnchor && previewPinnedWikiPopupRows.length > 0 ? (
-