tons of feature
This commit is contained in:
198
lib/selectingEngine.ts
Normal file
198
lib/selectingEngine.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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<number | string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user