preview map editor 60%

This commit is contained in:
taDuc
2026-04-13 21:47:28 +07:00
parent 3023fa947c
commit 458de8dadc
16 changed files with 1664 additions and 1149 deletions

View File

@@ -2,11 +2,12 @@ import maplibregl from "maplibre-gl";
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
// Khởi tạo engine chọn feature và context menu edit/delete.
export function initSelect(
map: maplibregl.Map,
getMode: ModeGetter,
onDelete: (id: string | number) => void,
onEdit: (feature: maplibregl.MapGeoJSONFeature) => void,
onDelete?: (id: string | number) => void,
onEdit?: (feature: maplibregl.MapGeoJSONFeature) => void,
onSelectId?: (id: string | number | null) => void
) {
const SELECTABLE_LAYERS = [
@@ -17,12 +18,11 @@ export function initSelect(
"places-symbol",
] as const;
const selectedIds = new Set<number | string>();
const hasContextActions = Boolean(onDelete || onEdit);
let contextMenu: HTMLDivElement | null = null;
let docClickHandler: ((ev: MouseEvent) => void) | null = null;
/**
* Clear feature-state highlight for all selected features.
*/
// Bỏ highlight feature-state của toàn bộ đối tượng đang chọn.
function clearSelection() {
if (!selectedIds.size) return;
selectedIds.forEach((id) => {
@@ -32,9 +32,7 @@ export function initSelect(
onSelectId?.(null);
}
/**
* Select (or toggle) a feature. Holding Alt enables additive/toggle selection.
*/
// Chọn hoặc toggle đối tượng; giữ Alt để chọn cộng dồn/tắt chọn.
function selectFeature(feature: maplibregl.MapGeoJSONFeature, additive: boolean) {
const id = feature.id ?? feature.properties?.id;
if (id === undefined || id === null) return;
@@ -56,6 +54,7 @@ export function initSelect(
onSelectId?.(selectedIds.size === 1 ? id : null);
}
// Chọn feature theo click trái, hỗ trợ additive bằng Alt.
function onClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return;
@@ -72,9 +71,8 @@ export function initSelect(
selectFeature(features[0], additive);
}
/**
* Show context menu (edit/delete) on right click.
*/
// Hiển thị menu ngữ cảnh (sửa/xóa) khi click chuột phải.
// Mở menu thao tác khi click phải lên feature.
function onRightClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return;
@@ -103,6 +101,7 @@ export function initSelect(
);
}
// Đổi cursor pointer khi hover lên đối tượng có thể chọn.
function onMove(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return;
@@ -115,15 +114,20 @@ export function initSelect(
map.on("click", onClick);
map.on("mousemove", onMove);
map.on("contextmenu", onRightClick);
if (hasContextActions) {
map.on("contextmenu", onRightClick);
}
return () => {
map.off("click", onClick);
map.off("mousemove", onMove);
map.off("contextmenu", onRightClick);
if (hasContextActions) {
map.off("contextmenu", onRightClick);
}
hideContextMenu();
};
// Ẩn và dọn dẹp context menu hiện tại.
function hideContextMenu() {
if (contextMenu) {
contextMenu.remove();
@@ -135,9 +139,7 @@ export function initSelect(
}
}
/**
* Render a minimal context menu near cursor.
*/
// Render menu ngữ cảnh tối giản gần vị trí con trỏ.
function showContextMenu(
x: number,
y: number,
@@ -159,6 +161,7 @@ export function initSelect(
menu.style.fontSize = "14px";
menu.style.padding = "4px 0";
// Tạo một item thao tác trong context menu.
const createItem = (label: string, onClick: () => void) => {
const item = document.createElement("div");
item.textContent = label;
@@ -174,30 +177,38 @@ export function initSelect(
};
const selectedCount = selectedIds.size || 1;
let hasMenuItems = false;
if (selectedCount === 1 && clickedFeature.geometry?.type === "Polygon") {
if (selectedCount === 1 && clickedFeature.geometry?.type === "Polygon" && onEdit) {
const single = clickedFeature;
menu.appendChild(createItem("Chỉnh sửa", () => onEdit(single)));
hasMenuItems = true;
}
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();
}
)
);
if (onDelete) {
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();
}
)
);
hasMenuItems = true;
}
if (!hasMenuItems) return;
document.body.appendChild(menu);
contextMenu = menu;
// Đóng menu khi click ra ngoài vùng menu.
const onDocClick = (ev: MouseEvent) => {
if (!menu.contains(ev.target as Node)) {
hideContextMenu();