Files
hnh-map/frontend-nuxt/composables/useMapLayers.ts
Nikolay Tatarinov fd624c2357 Refactor frontend components for improved functionality and accessibility
- Consolidated global error handling in app.vue to redirect users to the login page on API authentication failure.
- Enhanced MapView component by reintroducing event listeners for selected map and marker updates, improving interactivity.
- Updated PasswordInput and various modal components to ensure proper input handling and accessibility compliance.
- Refactored MapControls and MapControlsContent to streamline prop management and enhance user experience.
- Improved error handling in local storage operations within useMapBookmarks and useRecentLocations composables.
- Standardized input elements across forms for consistency in user interaction.
2026-03-04 14:06:27 +03:00

240 lines
8.5 KiB
TypeScript

import type L from 'leaflet'
import { HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
import { createMarker, type MapMarker, type MarkerData, type MapViewRef } from '~/lib/Marker'
import { createCharacter, type MapCharacter, type CharacterData, type CharacterMapViewRef } from '~/lib/Character'
import {
createUniqueList,
uniqueListUpdate,
uniqueListGetElements,
uniqueListById,
type UniqueList,
} from '~/lib/UniqueList'
import type { SmartTileLayer } from '~/lib/SmartTileLayer'
import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api'
type LeafletModule = L
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
export interface MapLayersOptions {
/** Leaflet API (from dynamic import). Required for creating markers and characters without static leaflet import. */
L: LeafletModule
map: L.Map
markerLayer: L.LayerGroup
layer: SmartTileLayerInstance
overlayLayer: SmartTileLayerInstance
getCurrentMapId: () => number
setCurrentMapId: (id: number) => void
setSelectedMapId: (id: number) => void
getAuths?: () => string[]
getTrackingCharacterId: () => number
setTrackingCharacterId: (id: number) => void
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => void
/** Called when user clicks "Add to saved locations" in marker popup. Receives marker id and getter to resolve marker. */
onAddMarkerToBookmark?: (markerId: number, getMarkerById: (id: number) => MapMarker | undefined) => void
/** Resolves relative marker icon path to absolute URL. If omitted, relative paths are used. */
resolveIconUrl?: (path: string) => string
/** Fallback icon URL when a marker image fails to load. */
fallbackIconUrl?: string
}
export interface MapLayersManager {
markers: UniqueList<MapMarker>
characters: UniqueList<MapCharacter>
changeMap: (id: number) => void
updateMarkers: (markersData: ApiMarker[]) => void
updateCharacters: (charactersData: ApiCharacter[]) => void
getQuestGivers: () => Array<{ id: number; name: string }>
getPlayers: () => Array<{ id: number; name: string }>
refreshMarkersVisibility: (hidden: boolean) => void
refreshOverlayMarkers: (overlayMapIdValue: number) => void
findMarkerById: (id: number) => MapMarker | undefined
findCharacterById: (id: number) => MapCharacter | undefined
}
export function createMapLayers(options: MapLayersOptions): MapLayersManager {
const {
L,
map,
markerLayer,
layer,
overlayLayer,
getCurrentMapId,
setCurrentMapId,
setSelectedMapId,
getAuths: _getAuths,
getTrackingCharacterId,
setTrackingCharacterId,
onMarkerContextMenu,
onAddMarkerToBookmark,
resolveIconUrl,
fallbackIconUrl,
} = options
const markers = createUniqueList<MapMarker>()
const characters = createUniqueList<MapCharacter>()
function markerCtx(): MapViewRef {
return { map, markerLayer, mapid: getCurrentMapId() }
}
function characterCtx(): CharacterMapViewRef {
return { map, mapid: getCurrentMapId() }
}
function changeMap(id: number) {
if (id === getCurrentMapId()) return
setCurrentMapId(id)
setSelectedMapId(id)
layer.map = id
layer.redraw()
overlayLayer.map = -1
overlayLayer.redraw()
const ctx = markerCtx()
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === id)
.forEach((it) => it.add(ctx))
const cCtx = characterCtx()
uniqueListGetElements(characters).forEach((it) => {
it.remove(cCtx)
it.add(cCtx)
})
}
function updateMarkers(markersData: ApiMarker[]) {
const list = Array.isArray(markersData) ? markersData : []
const ctx = markerCtx()
const iconOptions =
resolveIconUrl != null
? { resolveIconUrl, fallbackIconUrl }
: undefined
uniqueListUpdate(
markers,
list.map((it) => createMarker(it as MarkerData, iconOptions, L)),
(marker: MapMarker) => {
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
marker.setClickCallback(() => {
if (marker.leafletMarker) {
if (onAddMarkerToBookmark) {
const gridX = Math.floor(marker.position.x / TileSize)
const gridY = Math.floor(marker.position.y / TileSize)
const div = document.createElement('div')
div.className = 'map-marker-popup text-sm'
div.innerHTML = `
<p class="font-medium mb-1">${escapeHtml(marker.name)}</p>
<p class="text-base-content/70 text-xs mb-2 font-mono">${gridX}, ${gridY}</p>
<button type="button" class="btn btn-primary btn-xs w-full">Add to saved locations</button>
`
const btn = div.querySelector('button')
if (btn) {
btn.addEventListener('click', () => {
onAddMarkerToBookmark(marker.id, findMarkerById)
marker.leafletMarker?.closePopup()
})
}
marker.leafletMarker.unbindPopup()
marker.leafletMarker.bindPopup(div, { minWidth: 140, autoPan: true }).openPopup()
} else {
map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
}
}
})
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
mev.originalEvent.preventDefault()
mev.originalEvent.stopPropagation()
onMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
})
},
(marker: MapMarker) => marker.remove(ctx),
(marker: MapMarker, updated: MapMarker) => marker.update(ctx, updated)
)
}
function updateCharacters(charactersData: ApiCharacter[]) {
const list = Array.isArray(charactersData) ? charactersData : []
const ctx = characterCtx()
uniqueListUpdate(
characters,
list.map((it) => createCharacter(it as CharacterData, L)),
(character: MapCharacter) => {
character.add(ctx)
character.setClickCallback(() => setTrackingCharacterId(character.id))
},
(character: MapCharacter) => character.remove(ctx),
(character: MapCharacter, updated: MapCharacter) => {
if (getTrackingCharacterId() === updated.id) {
if (getCurrentMapId() !== updated.map) changeMap(updated.map)
const latlng = map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
map.setView(latlng, HnHMaxZoom)
}
character.update(ctx, updated)
}
)
}
function getQuestGivers(): Array<{ id: number; name: string }> {
return uniqueListGetElements(markers)
.filter((it) => it.type === 'quest')
.map((it) => ({ id: it.id, name: it.name }))
}
function getPlayers(): Array<{ id: number; name: string }> {
return uniqueListGetElements(characters).map((it) => ({ id: it.id, name: it.name }))
}
function refreshMarkersVisibility(hidden: boolean) {
const ctx = markerCtx()
if (hidden) {
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
} else {
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === getCurrentMapId() || it.map === overlayLayer.map)
.forEach((it) => it.add(ctx))
}
}
function refreshOverlayMarkers(overlayMapIdValue: number) {
overlayLayer.map = overlayMapIdValue
overlayLayer.redraw()
const ctx = markerCtx()
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === getCurrentMapId() || it.map === overlayMapIdValue)
.forEach((it) => it.add(ctx))
}
function findMarkerById(id: number): MapMarker | undefined {
return uniqueListById(markers, id)
}
function findCharacterById(id: number): MapCharacter | undefined {
return uniqueListById(characters, id)
}
return {
markers,
characters,
changeMap,
updateMarkers,
updateCharacters,
getQuestGivers,
getPlayers,
refreshMarkersVisibility,
refreshOverlayMarkers,
findMarkerById,
findCharacterById,
}
}