import maplibregl from "maplibre-gl"; type ModeGetter = () => "idle" | "draw" | "select" | "add-point"; export function initSelect( map: maplibregl.Map, getMode: ModeGetter, onDelete: (id: string | number) => void, onEdit: (feature: maplibregl.MapGeoJSONFeature) => void ) { const selectedIds = new Set(); let contextMenu: HTMLDivElement | null = null; let docClickHandler: ((ev: MouseEvent) => void) | null = null; /** * Clear feature-state highlight for all selected features. */ function clearSelection() { if (!selectedIds.size) return; selectedIds.forEach((id) => { map.setFeatureState({ source: "countries", id }, { selected: false }); }); selectedIds.clear(); } /** * Select (or toggle) a feature. Holding Alt enables additive/toggle selection. */ function selectFeature(feature: maplibregl.MapGeoJSONFeature, additive: boolean) { const id = feature.id ?? feature.properties?.id; if (id === undefined || id === null) return; if (!additive) { clearSelection(); } if (additive && selectedIds.has(id)) { // Alt + click on an already selected feature removes it from the selection map.setFeatureState({ source: "countries", id }, { selected: false }); selectedIds.delete(id); return; } map.setFeatureState({ source: "countries", id }, { selected: true }); selectedIds.add(id); } function onClick(e: maplibregl.MapLayerMouseEvent) { if (getMode() !== "select") return; const features = map.queryRenderedFeatures(e.point, { layers: ["countries-fill"], }) as maplibregl.MapGeoJSONFeature[]; if (!features.length) { clearSelection(); return; } const additive = !!e.originalEvent?.altKey; selectFeature(features[0], additive); } /** * Show context menu (edit/delete) on right click. */ function onRightClick(e: maplibregl.MapLayerMouseEvent) { if (getMode() !== "select") return; e.preventDefault(); // block browser menu const features = map.queryRenderedFeatures(e.point, { layers: ["countries-fill"], }) as maplibregl.MapGeoJSONFeature[]; if (!features.length) return; const feature = features[0]; const id = feature.id ?? feature.properties?.id; if (id === undefined || id === null) return; // if right-clicked item not selected, make it the sole selection if (!selectedIds.has(id)) { clearSelection(); selectFeature(feature, false); } showContextMenu( e.originalEvent?.clientX ?? e.point.x, e.originalEvent?.clientY ?? e.point.y, feature ); } function onMove(e: maplibregl.MapLayerMouseEvent) { if (getMode() !== "select") return; const features = map.queryRenderedFeatures(e.point, { layers: ["countries-fill"], }); map.getCanvas().style.cursor = features.length ? "pointer" : ""; } map.on("click", onClick); map.on("mousemove", onMove); map.on("contextmenu", onRightClick); return () => { map.off("click", onClick); map.off("mousemove", onMove); map.off("contextmenu", onRightClick); hideContextMenu(); }; function hideContextMenu() { if (contextMenu) { contextMenu.remove(); contextMenu = null; } if (docClickHandler) { document.removeEventListener("click", docClickHandler); docClickHandler = null; } } /** * Render a minimal context menu near cursor. */ function showContextMenu( x: number, y: number, clickedFeature: maplibregl.MapGeoJSONFeature ) { hideContextMenu(); const menu = document.createElement("div"); menu.style.position = "fixed"; menu.style.left = `${x}px`; menu.style.top = `${y}px`; menu.style.background = "#0f172a"; menu.style.color = "white"; menu.style.border = "1px solid #1f2937"; menu.style.borderRadius = "6px"; menu.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)"; menu.style.zIndex = "9999"; menu.style.minWidth = "120px"; menu.style.fontSize = "14px"; menu.style.padding = "4px 0"; const createItem = (label: string, onClick: () => void) => { const item = document.createElement("div"); item.textContent = label; item.style.padding = "8px 12px"; item.style.cursor = "pointer"; item.onmouseenter = () => (item.style.background = "#1f2937"); item.onmouseleave = () => (item.style.background = "transparent"); item.onclick = () => { onClick(); hideContextMenu(); }; return item; }; const selectedCount = selectedIds.size || 1; if (selectedCount === 1) { const single = clickedFeature; menu.appendChild(createItem("Chỉnh sửa", () => onEdit(single))); } menu.appendChild( createItem( selectedCount > 1 ? `Xóa ${selectedCount} mục` : "Xóa", () => { const ids = selectedIds.size ? Array.from(selectedIds) : [clickedFeature.id ?? clickedFeature.properties?.id]; ids.forEach((eachId) => { if (eachId !== undefined && eachId !== null) onDelete(eachId); }); clearSelection(); } ) ); document.body.appendChild(menu); contextMenu = menu; const onDocClick = (ev: MouseEvent) => { if (!menu.contains(ev.target as Node)) { hideContextMenu(); } }; docClickHandler = onDocClick; setTimeout(() => document.addEventListener("click", onDocClick), 0); } }