Enhance map components and improve build processes
- Updated Makefile to include `--build` flag for `docker-compose.dev.yml` and `--no-cache` for `docker-compose.prod.yml` to ensure fresh builds. - Added new CSS styles for Leaflet tooltips and popups to utilize theme colors, enhancing visual consistency. - Enhanced MapView component with new props for markers and current zoom level, improving marker management and zoom functionality. - Introduced new icons for copy and info actions to improve user interface clarity. - Updated MapBookmarks and MapControls components to support new features and improve user experience with bookmarks and zoom controls. - Refactored MapSearch to display coordinates and improve marker search functionality.
This commit is contained in:
4
Makefile
4
Makefile
@@ -3,10 +3,10 @@
|
|||||||
TOOLS_COMPOSE = docker compose -f docker-compose.tools.yml
|
TOOLS_COMPOSE = docker compose -f docker-compose.tools.yml
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
docker compose -f docker-compose.dev.yml up
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker compose -f docker-compose.prod.yml build
|
docker compose -f docker-compose.prod.yml build --no-cache
|
||||||
|
|
||||||
test: test-backend test-frontend
|
test: test-backend test-frontend
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,39 @@
|
|||||||
.leaflet-tile.tile-fresh {
|
.leaflet-tile.tile-fresh {
|
||||||
animation: tile-fresh-glow 0.4s ease-out;
|
animation: tile-fresh-glow 0.4s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Leaflet tooltip: use theme colors (dark/light) */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
color: var(--color-base-content);
|
||||||
|
border-color: var(--color-base-300);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
border-top-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
border-bottom-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
border-left-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
border-right-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaflet popup: use theme colors (dark/light) */
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: var(--color-base-100);
|
||||||
|
color: var(--color-base-content);
|
||||||
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
color: var(--color-base-content);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,12 +100,15 @@
|
|||||||
:maps="maps"
|
:maps="maps"
|
||||||
:quest-givers="questGivers"
|
:quest-givers="questGivers"
|
||||||
:players="players"
|
:players="players"
|
||||||
|
:markers="allMarkers"
|
||||||
|
:current-zoom="currentZoom"
|
||||||
:current-map-id="mapLogic.state.mapid.value"
|
:current-map-id="mapLogic.state.mapid.value"
|
||||||
:current-coords="mapLogic.state.displayCoords.value"
|
:current-coords="mapLogic.state.displayCoords.value"
|
||||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
||||||
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
||||||
@reset-view="mapLogic.resetView(leafletMap)"
|
@reset-view="mapLogic.resetView(leafletMap)"
|
||||||
|
@set-zoom="onSetZoom"
|
||||||
@jump-to-marker="mapLogic.state.selectedMarkerId.value = $event"
|
@jump-to-marker="mapLogic.state.selectedMarkerId.value = $event"
|
||||||
/>
|
/>
|
||||||
<MapContextMenu
|
<MapContextMenu
|
||||||
@@ -147,7 +150,7 @@ import { useMapNavigate } from '~/composables/useMapNavigate'
|
|||||||
import { useFullscreen } from '~/composables/useFullscreen'
|
import { useFullscreen } from '~/composables/useFullscreen'
|
||||||
import { startMapUpdates, type UseMapUpdatesReturn, type SseConnectionState } from '~/composables/useMapUpdates'
|
import { startMapUpdates, type UseMapUpdatesReturn, type SseConnectionState } from '~/composables/useMapUpdates'
|
||||||
import { createMapLayers, type MapLayersManager } from '~/composables/useMapLayers'
|
import { createMapLayers, type MapLayersManager } from '~/composables/useMapLayers'
|
||||||
import type { MapInfo, ConfigResponse, MeResponse } from '~/types/api'
|
import type { MapInfo, ConfigResponse, MeResponse, Marker as ApiMarker } from '~/types/api'
|
||||||
import type L from 'leaflet'
|
import type L from 'leaflet'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -252,6 +255,10 @@ const maps = ref<MapInfo[]>([])
|
|||||||
const mapsLoaded = ref(false)
|
const mapsLoaded = ref(false)
|
||||||
const questGivers = ref<Array<{ id: number; name: string }>>([])
|
const questGivers = ref<Array<{ id: number; name: string }>>([])
|
||||||
const players = ref<Array<{ id: number; name: string }>>([])
|
const players = ref<Array<{ id: number; name: string }>>([])
|
||||||
|
/** All markers from API for search suggestions (updated when markers load or on merge). */
|
||||||
|
const allMarkers = ref<ApiMarker[]>([])
|
||||||
|
/** Current map zoom level (1–6) for zoom slider. Updated on zoomend. */
|
||||||
|
const currentZoom = ref(HnHDefaultZoom)
|
||||||
/** Single source of truth: layout updates me, we derive auths for context menu. */
|
/** Single source of truth: layout updates me, we derive auths for context menu. */
|
||||||
const me = useState<MeResponse | null>('me', () => null)
|
const me = useState<MeResponse | null>('me', () => null)
|
||||||
const auths = computed(() => me.value?.auths ?? [])
|
const auths = computed(() => me.value?.auths ?? [])
|
||||||
@@ -347,6 +354,10 @@ function reloadPage() {
|
|||||||
if (import.meta.client) window.location.reload()
|
if (import.meta.client) window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetZoom(z: number) {
|
||||||
|
if (leafletMap) leafletMap.setZoom(z)
|
||||||
|
}
|
||||||
|
|
||||||
function onKeydown(e: KeyboardEvent) {
|
function onKeydown(e: KeyboardEvent) {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
const inInput = /^(INPUT|TEXTAREA|SELECT)$/.test(target?.tagName ?? '')
|
const inInput = /^(INPUT|TEXTAREA|SELECT)$/.test(target?.tagName ?? '')
|
||||||
@@ -462,6 +473,16 @@ onMounted(async () => {
|
|||||||
getTrackingCharacterId: () => mapLogic.state.trackingCharacterId.value,
|
getTrackingCharacterId: () => mapLogic.state.trackingCharacterId.value,
|
||||||
setTrackingCharacterId: (id: number) => { mapLogic.state.trackingCharacterId.value = id },
|
setTrackingCharacterId: (id: number) => { mapLogic.state.trackingCharacterId.value = id },
|
||||||
onMarkerContextMenu: mapLogic.openMarkerContextMenu,
|
onMarkerContextMenu: mapLogic.openMarkerContextMenu,
|
||||||
|
onAddMarkerToBookmark: (markerId, getMarkerById) => {
|
||||||
|
const m = getMarkerById(markerId)
|
||||||
|
if (!m) return
|
||||||
|
openBookmarkModal(m.name, 'Add bookmark', {
|
||||||
|
kind: 'add',
|
||||||
|
mapId: m.map,
|
||||||
|
x: Math.floor(m.position.x / TileSize),
|
||||||
|
y: Math.floor(m.position.y / TileSize),
|
||||||
|
})
|
||||||
|
},
|
||||||
resolveIconUrl: (path) => resolvePath(path),
|
resolveIconUrl: (path) => resolvePath(path),
|
||||||
fallbackIconUrl: FALLBACK_MARKER_ICON,
|
fallbackIconUrl: FALLBACK_MARKER_ICON,
|
||||||
})
|
})
|
||||||
@@ -479,7 +500,9 @@ onMounted(async () => {
|
|||||||
layersManager!.changeMap(mapTo)
|
layersManager!.changeMap(mapTo)
|
||||||
api.getMarkers().then((body) => {
|
api.getMarkers().then((body) => {
|
||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
const list = Array.isArray(body) ? body : []
|
||||||
|
allMarkers.value = list
|
||||||
|
layersManager!.updateMarkers(list)
|
||||||
questGivers.value = layersManager!.getQuestGivers()
|
questGivers.value = layersManager!.getQuestGivers()
|
||||||
})
|
})
|
||||||
leafletMap!.setView(latLng, leafletMap!.getZoom())
|
leafletMap!.setView(latLng, leafletMap!.getZoom())
|
||||||
@@ -530,7 +553,9 @@ onMounted(async () => {
|
|||||||
// Markers load asynchronously after map is visible.
|
// Markers load asynchronously after map is visible.
|
||||||
api.getMarkers().then((body) => {
|
api.getMarkers().then((body) => {
|
||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
const list = Array.isArray(body) ? body : []
|
||||||
|
allMarkers.value = list
|
||||||
|
layersManager!.updateMarkers(list)
|
||||||
questGivers.value = layersManager!.getQuestGivers()
|
questGivers.value = layersManager!.getQuestGivers()
|
||||||
updateSelectedMarkerForBookmark()
|
updateSelectedMarkerForBookmark()
|
||||||
})
|
})
|
||||||
@@ -650,6 +675,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
leafletMap.on('moveend', () => mapLogic.updateDisplayCoords(leafletMap))
|
leafletMap.on('moveend', () => mapLogic.updateDisplayCoords(leafletMap))
|
||||||
mapLogic.updateDisplayCoords(leafletMap)
|
mapLogic.updateDisplayCoords(leafletMap)
|
||||||
|
currentZoom.value = leafletMap.getZoom()
|
||||||
|
leafletMap.on('zoomend', () => {
|
||||||
|
if (leafletMap) currentZoom.value = leafletMap.getZoom()
|
||||||
|
})
|
||||||
leafletMap.on('drag', () => {
|
leafletMap.on('drag', () => {
|
||||||
mapLogic.state.trackingCharacterId.value = -1
|
mapLogic.state.trackingCharacterId.value = -1
|
||||||
})
|
})
|
||||||
|
|||||||
6
frontend-nuxt/components/icons/IconCopy.vue
Normal file
6
frontend-nuxt/components/icons/IconCopy.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconInfo.vue
Normal file
7
frontend-nuxt/components/icons/IconInfo.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 16v-4" />
|
||||||
|
<path d="M12 8h.01" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="flex flex-col gap-1 max-h-40 overflow-y-auto">
|
<div class="flex flex-col gap-1 max-h-40 overflow-y-auto">
|
||||||
<template v-if="bookmarks.length === 0">
|
<template v-if="bookmarks.length === 0">
|
||||||
<p class="text-xs text-base-content/60 py-1">No saved locations.</p>
|
<p class="text-xs text-base-content/60 py-1">No saved locations.</p>
|
||||||
<p class="text-xs text-base-content/50 py-0">Add current location or a selected quest giver below.</p>
|
<p class="text-xs text-base-content/50 py-0">Add your first location below.</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
class="btn btn-primary btn-sm w-full"
|
class="btn btn-primary btn-sm w-full"
|
||||||
:class="touchFriendly ? 'min-h-11' : ''"
|
:class="touchFriendly ? 'min-h-11' : ''"
|
||||||
:disabled="!selectedMarkerForBookmark"
|
:disabled="!selectedMarkerForBookmark"
|
||||||
title="Add selected quest giver as bookmark"
|
:title="selectedMarkerForBookmark ? 'Add selected quest giver as bookmark' : 'Select a quest giver from the list above to add it as a bookmark.'"
|
||||||
@click="onAddSelectedMarker"
|
@click="onAddSelectedMarker"
|
||||||
>
|
>
|
||||||
<icons-icon-plus class="size-4" />
|
<icons-icon-plus class="size-4" />
|
||||||
|
|||||||
@@ -49,12 +49,15 @@
|
|||||||
:maps="maps"
|
:maps="maps"
|
||||||
:quest-givers="questGivers"
|
:quest-givers="questGivers"
|
||||||
:players="players"
|
:players="players"
|
||||||
|
:markers="markers"
|
||||||
|
:current-zoom="currentZoom"
|
||||||
:current-map-id="currentMapId ?? undefined"
|
:current-map-id="currentMapId ?? undefined"
|
||||||
:current-coords="currentCoords"
|
:current-coords="currentCoords"
|
||||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
@zoom-in="$emit('zoomIn')"
|
@zoom-in="$emit('zoomIn')"
|
||||||
@zoom-out="$emit('zoomOut')"
|
@zoom-out="$emit('zoomOut')"
|
||||||
@reset-view="$emit('resetView')"
|
@reset-view="$emit('resetView')"
|
||||||
|
@set-zoom="$emit('setZoom', $event)"
|
||||||
@jump-to-marker="$emit('jumpToMarker', $event)"
|
@jump-to-marker="$emit('jumpToMarker', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +143,8 @@
|
|||||||
:maps="maps"
|
:maps="maps"
|
||||||
:quest-givers="questGivers"
|
:quest-givers="questGivers"
|
||||||
:players="players"
|
:players="players"
|
||||||
|
:markers="markers"
|
||||||
|
:current-zoom="currentZoom"
|
||||||
:current-map-id="currentMapId ?? undefined"
|
:current-map-id="currentMapId ?? undefined"
|
||||||
:current-coords="currentCoords"
|
:current-coords="currentCoords"
|
||||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
@@ -147,6 +152,7 @@
|
|||||||
@zoom-in="$emit('zoomIn')"
|
@zoom-in="$emit('zoomIn')"
|
||||||
@zoom-out="$emit('zoomOut')"
|
@zoom-out="$emit('zoomOut')"
|
||||||
@reset-view="$emit('resetView')"
|
@reset-view="$emit('resetView')"
|
||||||
|
@set-zoom="$emit('setZoom', $event)"
|
||||||
@jump-to-marker="$emit('jumpToMarker', $event)"
|
@jump-to-marker="$emit('jumpToMarker', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +175,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo, Marker as ApiMarker } from '~/types/api'
|
||||||
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
||||||
import MapControlsContent from '~/components/map/MapControlsContent.vue'
|
import MapControlsContent from '~/components/map/MapControlsContent.vue'
|
||||||
|
|
||||||
@@ -188,6 +194,8 @@ const props = withDefaults(
|
|||||||
maps: MapInfo[]
|
maps: MapInfo[]
|
||||||
questGivers: QuestGiver[]
|
questGivers: QuestGiver[]
|
||||||
players: Player[]
|
players: Player[]
|
||||||
|
markers?: ApiMarker[]
|
||||||
|
currentZoom?: number
|
||||||
currentMapId?: number | null
|
currentMapId?: number | null
|
||||||
currentCoords?: { x: number; y: number; z: number } | null
|
currentCoords?: { x: number; y: number; z: number } | null
|
||||||
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
||||||
@@ -196,6 +204,8 @@ const props = withDefaults(
|
|||||||
maps: () => [],
|
maps: () => [],
|
||||||
questGivers: () => [],
|
questGivers: () => [],
|
||||||
players: () => [],
|
players: () => [],
|
||||||
|
markers: () => [],
|
||||||
|
currentZoom: 1,
|
||||||
currentMapId: null,
|
currentMapId: null,
|
||||||
currentCoords: null,
|
currentCoords: null,
|
||||||
selectedMarkerForBookmark: null,
|
selectedMarkerForBookmark: null,
|
||||||
@@ -206,6 +216,7 @@ defineEmits<{
|
|||||||
zoomIn: []
|
zoomIn: []
|
||||||
zoomOut: []
|
zoomOut: []
|
||||||
resetView: []
|
resetView: []
|
||||||
|
setZoom: [level: number]
|
||||||
jumpToMarker: [id: number]
|
jumpToMarker: [id: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
v-if="currentMapId != null && currentCoords != null"
|
v-if="currentMapId != null && currentCoords != null"
|
||||||
:maps="maps"
|
:maps="maps"
|
||||||
:quest-givers="questGivers"
|
:quest-givers="questGivers"
|
||||||
|
:markers="markers"
|
||||||
|
:overlay-map-id="props.overlayMapId"
|
||||||
:current-map-id="currentMapId"
|
:current-map-id="currentMapId"
|
||||||
:current-coords="currentCoords"
|
:current-coords="currentCoords"
|
||||||
:touch-friendly="touchFriendly"
|
:touch-friendly="touchFriendly"
|
||||||
@@ -48,6 +50,19 @@
|
|||||||
<icons-icon-home />
|
<icons-icon-home />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
:min="zoomMin"
|
||||||
|
:max="zoomMax"
|
||||||
|
:value="currentZoom"
|
||||||
|
class="range range-primary range-sm flex-1"
|
||||||
|
:class="touchFriendly ? 'range-lg' : ''"
|
||||||
|
aria-label="Zoom level"
|
||||||
|
@input="onZoomSliderInput($event)"
|
||||||
|
>
|
||||||
|
<span class="text-xs font-mono w-6 text-right" aria-hidden="true">{{ currentZoom }}</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- Display -->
|
<!-- Display -->
|
||||||
<section class="flex flex-col gap-2">
|
<section class="flex flex-col gap-2">
|
||||||
@@ -78,7 +93,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<label class="label py-0"><span>Overlay Map</span></label>
|
<label class="label py-0 flex items-center gap-1.5">
|
||||||
|
<span>Overlay Map</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex text-base-content/60 cursor-help"
|
||||||
|
title="Overlay shows markers from another map on top of the current one."
|
||||||
|
aria-label="Overlay shows markers from another map on top of the current one."
|
||||||
|
>
|
||||||
|
<icons-icon-info class="size-3.5" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
v-model="overlayMapId"
|
v-model="overlayMapId"
|
||||||
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
||||||
@@ -89,22 +113,43 @@
|
|||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<label class="label py-0"><span>Jump to Quest Giver</span></label>
|
<label class="label py-0"><span>Jump to</span></label>
|
||||||
|
<div class="join w-full flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm join-item flex-1 touch-manipulation"
|
||||||
|
:class="[jumpToTab === 'quest' ? 'btn-active' : 'btn-ghost', touchFriendly ? 'min-h-11 text-base' : '']"
|
||||||
|
aria-pressed="jumpToTab === 'quest'"
|
||||||
|
@click="jumpToTab = 'quest'"
|
||||||
|
>
|
||||||
|
Quest giver
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm join-item flex-1 touch-manipulation"
|
||||||
|
:class="[jumpToTab === 'player' ? 'btn-active' : 'btn-ghost', touchFriendly ? 'min-h-11 text-base' : '']"
|
||||||
|
aria-pressed="jumpToTab === 'player'"
|
||||||
|
@click="jumpToTab = 'player'"
|
||||||
|
>
|
||||||
|
Player
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
|
v-if="jumpToTab === 'quest'"
|
||||||
v-model="selectedMarkerIdSelect"
|
v-model="selectedMarkerIdSelect"
|
||||||
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation mt-1"
|
||||||
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
||||||
|
aria-label="Select quest giver"
|
||||||
>
|
>
|
||||||
<option value="">Select quest giver</option>
|
<option value="">Select quest giver</option>
|
||||||
<option v-for="q in questGivers" :key="q.id" :value="String(q.id)">{{ q.name }}</option>
|
<option v-for="q in questGivers" :key="q.id" :value="String(q.id)">{{ q.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<label class="label py-0"><span>Jump to Player</span></label>
|
|
||||||
<select
|
<select
|
||||||
|
v-else
|
||||||
v-model="selectedPlayerIdSelect"
|
v-model="selectedPlayerIdSelect"
|
||||||
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation mt-1"
|
||||||
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
||||||
|
aria-label="Select player"
|
||||||
>
|
>
|
||||||
<option value="">Select player</option>
|
<option value="">Select player</option>
|
||||||
<option v-for="p in players" :key="p.id" :value="String(p.id)">{{ p.name }}</option>
|
<option v-for="p in players" :key="p.id" :value="String(p.id)">{{ p.name }}</option>
|
||||||
@@ -126,9 +171,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo, Marker as ApiMarker } from '~/types/api'
|
||||||
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
||||||
import MapBookmarks from '~/components/map/MapBookmarks.vue'
|
import MapBookmarks from '~/components/map/MapBookmarks.vue'
|
||||||
|
import { HnHMinZoom, HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
||||||
|
|
||||||
interface QuestGiver {
|
interface QuestGiver {
|
||||||
id: number
|
id: number
|
||||||
@@ -140,27 +186,33 @@ interface Player {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zoomMin = HnHMinZoom
|
||||||
|
const zoomMax = HnHMaxZoom
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
maps: MapInfo[]
|
maps: MapInfo[]
|
||||||
questGivers: QuestGiver[]
|
questGivers: QuestGiver[]
|
||||||
players: Player[]
|
players: Player[]
|
||||||
|
markers?: ApiMarker[]
|
||||||
touchFriendly?: boolean
|
touchFriendly?: boolean
|
||||||
selectedMapIdSelect: string
|
selectedMapIdSelect: string
|
||||||
overlayMapId: number
|
overlayMapId: number
|
||||||
selectedMarkerIdSelect: string
|
selectedMarkerIdSelect: string
|
||||||
selectedPlayerIdSelect: string
|
selectedPlayerIdSelect: string
|
||||||
|
currentZoom?: number
|
||||||
currentMapId?: number
|
currentMapId?: number
|
||||||
currentCoords?: { x: number; y: number; z: number } | null
|
currentCoords?: { x: number; y: number; z: number } | null
|
||||||
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
||||||
}>(),
|
}>(),
|
||||||
{ touchFriendly: false, currentMapId: 0, currentCoords: null, selectedMarkerForBookmark: null }
|
{ touchFriendly: false, markers: () => [], currentZoom: 1, currentMapId: 0, currentCoords: null, selectedMarkerForBookmark: null }
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
zoomIn: []
|
zoomIn: []
|
||||||
zoomOut: []
|
zoomOut: []
|
||||||
resetView: []
|
resetView: []
|
||||||
|
setZoom: [level: number]
|
||||||
jumpToMarker: [id: number]
|
jumpToMarker: [id: number]
|
||||||
'update:hideMarkers': [v: boolean]
|
'update:hideMarkers': [v: boolean]
|
||||||
'update:selectedMapIdSelect': [v: string]
|
'update:selectedMapIdSelect': [v: string]
|
||||||
@@ -169,8 +221,16 @@ const emit = defineEmits<{
|
|||||||
'update:selectedPlayerIdSelect': [v: string]
|
'update:selectedPlayerIdSelect': [v: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function onZoomSliderInput(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value
|
||||||
|
const level = Number(value)
|
||||||
|
if (!Number.isNaN(level)) emit('setZoom', level)
|
||||||
|
}
|
||||||
|
|
||||||
const hideMarkers = defineModel<boolean>('hideMarkers', { required: true })
|
const hideMarkers = defineModel<boolean>('hideMarkers', { required: true })
|
||||||
|
|
||||||
|
const jumpToTab = ref<'quest' | 'player'>('quest')
|
||||||
|
|
||||||
const selectedMapIdSelect = computed({
|
const selectedMapIdSelect = computed({
|
||||||
get: () => props.selectedMapIdSelect,
|
get: () => props.selectedMapIdSelect,
|
||||||
set: (v) => emit('update:selectedMapIdSelect', v),
|
set: (v) => emit('update:selectedMapIdSelect', v),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="displayCoords"
|
v-if="displayCoords"
|
||||||
class="absolute bottom-2 right-2 z-[501] rounded-lg px-3 py-2 font-mono text-sm bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow cursor-pointer select-none transition-all hover:border-primary/50 hover:bg-base-100"
|
class="absolute bottom-2 right-2 z-[501] rounded-lg px-3 py-2 font-mono text-base bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow cursor-pointer select-none transition-all hover:border-primary/50 hover:bg-base-100 flex items-center gap-2"
|
||||||
aria-label="Current grid position and zoom — click to copy share link"
|
aria-label="Current grid position and zoom — click to copy share link"
|
||||||
:title="copied ? 'Copied!' : 'Click to copy share link'"
|
:title="copied ? 'Copied!' : 'Click to copy share link'"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
Copied!
|
Copied!
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<icons-icon-copy class="size-4 shrink-0 opacity-70" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -74,11 +74,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs text-base-content/50 mt-0.5">Coords: x, y — or type a marker name.</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo, Marker as ApiMarker } from '~/types/api'
|
||||||
|
import { TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
import { useMapNavigate } from '~/composables/useMapNavigate'
|
import { useMapNavigate } from '~/composables/useMapNavigate'
|
||||||
import { useRecentLocations } from '~/composables/useRecentLocations'
|
import { useRecentLocations } from '~/composables/useRecentLocations'
|
||||||
|
|
||||||
@@ -86,11 +88,13 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
maps: MapInfo[]
|
maps: MapInfo[]
|
||||||
questGivers: Array<{ id: number; name: string }>
|
questGivers: Array<{ id: number; name: string }>
|
||||||
|
markers?: ApiMarker[]
|
||||||
|
overlayMapId?: number
|
||||||
currentMapId: number
|
currentMapId: number
|
||||||
currentCoords: { x: number; y: number; z: number } | null
|
currentCoords: { x: number; y: number; z: number } | null
|
||||||
touchFriendly?: boolean
|
touchFriendly?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{ touchFriendly: false }
|
{ touchFriendly: false, markers: () => [], overlayMapId: -1 }
|
||||||
)
|
)
|
||||||
|
|
||||||
const { goToCoords } = useMapNavigate()
|
const { goToCoords } = useMapNavigate()
|
||||||
@@ -147,22 +151,37 @@ const suggestions = computed<Suggestion[]>(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const list: Suggestion[] = []
|
const list: Suggestion[] = []
|
||||||
for (const qg of props.questGivers) {
|
const overlayId = props.overlayMapId ?? -1
|
||||||
if (qg.name.toLowerCase().includes(q)) {
|
const visibleMarkers = (props.markers ?? []).filter(
|
||||||
|
(m) =>
|
||||||
|
!m.hidden &&
|
||||||
|
(m.map === props.currentMapId || (overlayId >= 0 && m.map === overlayId))
|
||||||
|
)
|
||||||
|
const qLower = q.toLowerCase()
|
||||||
|
for (const m of visibleMarkers) {
|
||||||
|
if (m.name.toLowerCase().includes(qLower)) {
|
||||||
|
const gridX = Math.floor(m.position.x / TileSize)
|
||||||
|
const gridY = Math.floor(m.position.y / TileSize)
|
||||||
list.push({
|
list.push({
|
||||||
key: `qg-${qg.id}`,
|
key: `marker-${m.id}`,
|
||||||
label: qg.name,
|
label: `${m.name} · ${gridX}, ${gridY}`,
|
||||||
mapId: props.currentMapId,
|
mapId: m.map,
|
||||||
x: 0,
|
x: gridX,
|
||||||
y: 0,
|
y: gridY,
|
||||||
zoom: undefined,
|
zoom: undefined,
|
||||||
markerId: qg.id,
|
markerId: m.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (list.length > 0) return list.slice(0, 8)
|
// Prefer quest givers (match by id in questGivers) so they appear first when query matches both
|
||||||
|
const qgIds = new Set(props.questGivers.map((qg) => qg.id))
|
||||||
return []
|
list.sort((a, b) => {
|
||||||
|
const aQg = a.markerId != null && qgIds.has(a.markerId) ? 1 : 0
|
||||||
|
const bQg = b.markerId != null && qgIds.has(b.markerId) ? 1 : 0
|
||||||
|
if (bQg !== aQg) return bQg - aQg
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
return list.slice(0, 8)
|
||||||
})
|
})
|
||||||
|
|
||||||
function scheduleCloseDropdown() {
|
function scheduleCloseDropdown() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type L from 'leaflet'
|
import type L from 'leaflet'
|
||||||
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
import { HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
import { createMarker, type MapMarker, type MarkerData, type MapViewRef } from '~/lib/Marker'
|
import { createMarker, type MapMarker, type MarkerData, type MapViewRef } from '~/lib/Marker'
|
||||||
import { createCharacter, type MapCharacter, type CharacterData, type CharacterMapViewRef } from '~/lib/Character'
|
import { createCharacter, type MapCharacter, type CharacterData, type CharacterMapViewRef } from '~/lib/Character'
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,15 @@ import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api
|
|||||||
|
|
||||||
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
export interface MapLayersOptions {
|
export interface MapLayersOptions {
|
||||||
/** Leaflet API (from dynamic import). Required for creating markers and characters without static leaflet import. */
|
/** Leaflet API (from dynamic import). Required for creating markers and characters without static leaflet import. */
|
||||||
L: typeof import('leaflet')
|
L: typeof import('leaflet')
|
||||||
@@ -28,6 +37,8 @@ export interface MapLayersOptions {
|
|||||||
getTrackingCharacterId: () => number
|
getTrackingCharacterId: () => number
|
||||||
setTrackingCharacterId: (id: number) => void
|
setTrackingCharacterId: (id: number) => void
|
||||||
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => 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. */
|
/** Resolves relative marker icon path to absolute URL. If omitted, relative paths are used. */
|
||||||
resolveIconUrl?: (path: string) => string
|
resolveIconUrl?: (path: string) => string
|
||||||
/** Fallback icon URL when a marker image fails to load. */
|
/** Fallback icon URL when a marker image fails to load. */
|
||||||
@@ -62,6 +73,7 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|||||||
getTrackingCharacterId,
|
getTrackingCharacterId,
|
||||||
setTrackingCharacterId,
|
setTrackingCharacterId,
|
||||||
onMarkerContextMenu,
|
onMarkerContextMenu,
|
||||||
|
onAddMarkerToBookmark,
|
||||||
resolveIconUrl,
|
resolveIconUrl,
|
||||||
fallbackIconUrl,
|
fallbackIconUrl,
|
||||||
} = options
|
} = options
|
||||||
@@ -112,7 +124,30 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|||||||
(marker: MapMarker) => {
|
(marker: MapMarker) => {
|
||||||
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
|
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
|
||||||
marker.setClickCallback(() => {
|
marker.setClickCallback(() => {
|
||||||
if (marker.leafletMarker) map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
|
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) => {
|
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
||||||
mev.originalEvent.preventDefault()
|
mev.originalEvent.preventDefault()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type L from 'leaflet'
|
import type L from 'leaflet'
|
||||||
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
|
import { HnHMaxZoom, ImageIcon, TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
|
|
||||||
export interface MarkerData {
|
export interface MarkerData {
|
||||||
id: number
|
id: number
|
||||||
@@ -104,7 +104,15 @@ export function createMarker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const position = mapview.map.unproject([marker.position.x, marker.position.y], HnHMaxZoom)
|
const position = mapview.map.unproject([marker.position.x, marker.position.y], HnHMaxZoom)
|
||||||
leafletMarker = L.marker(position, { icon, title: marker.name })
|
leafletMarker = L.marker(position, { icon })
|
||||||
|
const gridX = Math.floor(marker.position.x / TileSize)
|
||||||
|
const gridY = Math.floor(marker.position.y / TileSize)
|
||||||
|
const tooltipContent = `${marker.name} · ${gridX}, ${gridY}`
|
||||||
|
leafletMarker.bindTooltip(tooltipContent, {
|
||||||
|
direction: 'top',
|
||||||
|
permanent: false,
|
||||||
|
offset: L.point(0, -14),
|
||||||
|
})
|
||||||
leafletMarker.addTo(mapview.markerLayer)
|
leafletMarker.addTo(mapview.markerLayer)
|
||||||
const markerEl = (leafletMarker as unknown as { getElement?: () => HTMLElement }).getElement?.()
|
const markerEl = (leafletMarker as unknown as { getElement?: () => HTMLElement }).getElement?.()
|
||||||
if (markerEl) markerEl.setAttribute('aria-label', marker.name)
|
if (markerEl) markerEl.setAttribute('aria-label', marker.name)
|
||||||
@@ -125,6 +133,9 @@ export function createMarker(
|
|||||||
if (leafletMarker) {
|
if (leafletMarker) {
|
||||||
const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
||||||
leafletMarker.setLatLng(position)
|
leafletMarker.setLatLng(position)
|
||||||
|
const gridX = Math.floor(updated.position.x / TileSize)
|
||||||
|
const gridY = Math.floor(updated.position.y / TileSize)
|
||||||
|
leafletMarker.setTooltipContent(`${marker.name} · ${gridX}, ${gridY}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ vi.mock('leaflet', () => {
|
|||||||
addTo: vi.fn().mockReturnThis(),
|
addTo: vi.fn().mockReturnThis(),
|
||||||
setLatLng: vi.fn().mockReturnThis(),
|
setLatLng: vi.fn().mockReturnThis(),
|
||||||
remove: vi.fn().mockReturnThis(),
|
remove: vi.fn().mockReturnThis(),
|
||||||
|
bindTooltip: vi.fn().mockReturnThis(),
|
||||||
|
setTooltipContent: vi.fn().mockReturnThis(),
|
||||||
|
openPopup: vi.fn().mockReturnThis(),
|
||||||
|
closePopup: vi.fn().mockReturnThis(),
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
@@ -19,11 +23,13 @@ vi.mock('leaflet', () => {
|
|||||||
|
|
||||||
vi.mock('~/lib/LeafletCustomTypes', () => ({
|
vi.mock('~/lib/LeafletCustomTypes', () => ({
|
||||||
HnHMaxZoom: 6,
|
HnHMaxZoom: 6,
|
||||||
|
TileSize: 100,
|
||||||
ImageIcon: class {
|
ImageIcon: class {
|
||||||
constructor(_opts: Record<string, unknown>) {}
|
constructor(_opts: Record<string, unknown>) {}
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
import L from 'leaflet'
|
||||||
import { createMarker, type MarkerData, type MapViewRef } from '../Marker'
|
import { createMarker, type MarkerData, type MapViewRef } from '../Marker'
|
||||||
|
|
||||||
function makeMarkerData(overrides: Partial<MarkerData> = {}): MarkerData {
|
function makeMarkerData(overrides: Partial<MarkerData> = {}): MarkerData {
|
||||||
@@ -57,7 +63,7 @@ describe('createMarker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('creates a marker with correct properties', () => {
|
it('creates a marker with correct properties', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
expect(marker.id).toBe(1)
|
expect(marker.id).toBe(1)
|
||||||
expect(marker.name).toBe('Tower')
|
expect(marker.name).toBe('Tower')
|
||||||
expect(marker.position).toEqual({ x: 100, y: 200 })
|
expect(marker.position).toEqual({ x: 100, y: 200 })
|
||||||
@@ -69,46 +75,46 @@ describe('createMarker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('detects quest type', () => {
|
it('detects quest type', () => {
|
||||||
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bush' }))
|
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bush' }), undefined, L)
|
||||||
expect(marker.type).toBe('quest')
|
expect(marker.type).toBe('quest')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('detects quest type for bumling', () => {
|
it('detects quest type for bumling', () => {
|
||||||
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bumling' }))
|
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bumling' }), undefined, L)
|
||||||
expect(marker.type).toBe('quest')
|
expect(marker.type).toBe('quest')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('detects custom type', () => {
|
it('detects custom type', () => {
|
||||||
const marker = createMarker(makeMarkerData({ image: 'custom' }))
|
const marker = createMarker(makeMarkerData({ image: 'custom' }), undefined, L)
|
||||||
expect(marker.type).toBe('custom')
|
expect(marker.type).toBe('custom')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('extracts type from gfx path', () => {
|
it('extracts type from gfx path', () => {
|
||||||
const marker = createMarker(makeMarkerData({ image: 'gfx/terobjs/mm/village' }))
|
const marker = createMarker(makeMarkerData({ image: 'gfx/terobjs/mm/village' }), undefined, L)
|
||||||
expect(marker.type).toBe('village')
|
expect(marker.type).toBe('village')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('starts with null leaflet marker', () => {
|
it('starts with null leaflet marker', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
expect(marker.leafletMarker).toBeNull()
|
expect(marker.leafletMarker).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('add creates a leaflet marker for non-hidden markers', () => {
|
it('add creates a leaflet marker for non-hidden markers', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
const mapview = makeMapViewRef()
|
const mapview = makeMapViewRef()
|
||||||
marker.add(mapview)
|
marker.add(mapview)
|
||||||
expect(mapview.map.unproject).toHaveBeenCalled()
|
expect(mapview.map.unproject).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('add does nothing for hidden markers', () => {
|
it('add does nothing for hidden markers', () => {
|
||||||
const marker = createMarker(makeMarkerData({ hidden: true }))
|
const marker = createMarker(makeMarkerData({ hidden: true }), undefined, L)
|
||||||
const mapview = makeMapViewRef()
|
const mapview = makeMapViewRef()
|
||||||
marker.add(mapview)
|
marker.add(mapview)
|
||||||
expect(mapview.map.unproject).not.toHaveBeenCalled()
|
expect(mapview.map.unproject).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('update changes position and name', () => {
|
it('update changes position and name', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
const mapview = makeMapViewRef()
|
const mapview = makeMapViewRef()
|
||||||
|
|
||||||
marker.update(mapview, {
|
marker.update(mapview, {
|
||||||
@@ -122,7 +128,7 @@ describe('createMarker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('setClickCallback and setContextMenu work', () => {
|
it('setClickCallback and setContextMenu work', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
const clickCb = vi.fn()
|
const clickCb = vi.fn()
|
||||||
const contextCb = vi.fn()
|
const contextCb = vi.fn()
|
||||||
|
|
||||||
@@ -131,7 +137,7 @@ describe('createMarker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('remove on a marker without leaflet marker does nothing', () => {
|
it('remove on a marker without leaflet marker does nothing', () => {
|
||||||
const marker = createMarker(makeMarkerData())
|
const marker = createMarker(makeMarkerData(), undefined, L)
|
||||||
const mapview = makeMapViewRef()
|
const mapview = makeMapViewRef()
|
||||||
marker.remove(mapview) // should not throw
|
marker.remove(mapview) // should not throw
|
||||||
expect(marker.leafletMarker).toBeNull()
|
expect(marker.leafletMarker).toBeNull()
|
||||||
|
|||||||
Reference in New Issue
Block a user