Compare commits

...

3 commits

Author SHA1 Message Date
isUnknown
bad465406d fix: resolve marker positioning bug and integrate Kirby design system
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 17s
- Fix marker positioning issue where markers would glide and misalign during zoom
- Implement two-wrapper structure to isolate CSS transforms from MapLibre positioning
- Outer .custom-marker: MapLibre handles positioning via translate3d()
- Inner .marker-inner: Visual transforms (rotate, scale) isolated from MapLibre
- Remove debug console.log statements
- Integrate Kirby design system in MarkerList and GeocodeSearch components
- Use Kirby CSS variables (--input-color-back, --color-border, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 15:10:32 +01:00
isUnknown
63dc136309 change help message 2026-01-29 14:35:51 +01:00
isUnknown
818506fcfa fix: add polling and reset handling for single mode coordinates
Enhanced coordinate synchronization in single mode to handle Panel
actions like "Supprimer" (reset to saved values).

Issues Fixed:
- Marker not updating when clicking "Supprimer" button in Panel
- Panel "Supprimer" restores saved coordinates but marker didn't move
- No detection of programmatic field value changes

Solution:
- Add MutationObserver to detect attribute changes on input fields
- Add 500ms polling as fallback for value detection
- Add nextTick() for reactive updates to ensure proper timing
- Handle coordinate reset: when invalid, return to default center
- Proper cleanup with onBeforeUnmount for observers and intervals

Behavior:
- User changes field → marker updates immediately
- User drags marker → fields update immediately
- User clicks "Supprimer" → marker returns to saved position
- Fields cleared → marker disappears, map resets to default center

Technical Details:
- MutationObserver watches 'value' attribute on lat/lon inputs
- Polling checks every 500ms for changes missed by events
- Watcher uses nextTick() to ensure DOM updates complete
- All event listeners and observers properly cleaned up on unmount

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 14:34:28 +01:00
7 changed files with 245 additions and 190 deletions

View file

@ -58,4 +58,4 @@ tabs:
label: Position sur la carte label: Position sur la carte
type: map-editor type: map-editor
mode: single mode: single
help: Déplacez le marqueur ou recherchez une adresse help: Déplacez le marqueur

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
</template> </template>
<script> <script>
import { ref, computed, watch, onMounted, nextTick } from 'vue'; import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MapPreview from '../map/MapPreview.vue'; import MapPreview from '../map/MapPreview.vue';
import MarkerList from '../map/MarkerList.vue'; import MarkerList from '../map/MarkerList.vue';
import { useMarkersApi } from '../../composables/useMarkersApi.js'; import { useMarkersApi } from '../../composables/useMarkersApi.js';
@ -191,6 +191,7 @@ export default {
// Watch for changes in the form fields // Watch for changes in the form fields
const form = document.querySelector('.k-form'); const form = document.querySelector('.k-form');
if (form) { if (form) {
// Listen to input events
form.addEventListener('input', (e) => { form.addEventListener('input', (e) => {
if (e.target.name && (e.target.name.includes('latitude') || e.target.name.includes('longitude'))) { if (e.target.name && (e.target.name.includes('latitude') || e.target.name.includes('longitude'))) {
const newCoords = getCoordinatesFromForm(); const newCoords = getCoordinatesFromForm();
@ -198,6 +199,39 @@ export default {
singleLon.value = newCoords.lon; singleLon.value = newCoords.lon;
} }
}); });
// Also use MutationObserver to detect value changes (e.g., from "Supprimer" button)
const latInput = form.querySelector('input[name*="latitude"]');
const lonInput = form.querySelector('input[name*="longitude"]');
if (latInput && lonInput) {
const observer = new MutationObserver(() => {
const newCoords = getCoordinatesFromForm();
if (newCoords.lat !== singleLat.value || newCoords.lon !== singleLon.value) {
singleLat.value = newCoords.lat;
singleLon.value = newCoords.lon;
}
});
// Observe attribute changes (value attribute)
observer.observe(latInput, { attributes: true, attributeFilter: ['value'] });
observer.observe(lonInput, { attributes: true, attributeFilter: ['value'] });
// Also poll periodically as a fallback
const pollInterval = setInterval(() => {
const newCoords = getCoordinatesFromForm();
if (newCoords.lat !== singleLat.value || newCoords.lon !== singleLon.value) {
singleLat.value = newCoords.lat;
singleLon.value = newCoords.lon;
}
}, 500);
// Cleanup on unmount
onBeforeUnmount(() => {
observer.disconnect();
clearInterval(pollInterval);
});
}
} }
} }
@ -228,9 +262,23 @@ export default {
if (props.mode === 'single') { if (props.mode === 'single') {
// Center map on new position if valid // Center map on new position if valid
if (!isNaN(lat) && !isNaN(lon) && lat !== null && lon !== null && lat !== 0 && lon !== 0) { if (!isNaN(lat) && !isNaN(lon) && lat !== null && lon !== null && lat !== 0 && lon !== 0) {
// Force immediate reactivity
nextTick(() => {
if (mapPreview.value && mapPreview.value.centerOnPosition) { if (mapPreview.value && mapPreview.value.centerOnPosition) {
mapPreview.value.centerOnPosition(lat, lon); mapPreview.value.centerOnPosition(lat, lon);
} }
});
} else {
// Coordinates are invalid/cleared - reset to default center
center.value = {
lat: props.defaultCenter[0],
lon: props.defaultCenter[1]
};
nextTick(() => {
if (mapPreview.value && mapPreview.value.centerOnPosition) {
mapPreview.value.centerOnPosition(center.value.lat, center.value.lon);
}
});
} }
} }
} }

View file

@ -215,6 +215,7 @@ export default {
background: var(--color-white); background: var(--color-white);
transition: border-color 0.2s; transition: border-color 0.2s;
color: #000; color: #000;
background: var(--input-color-back);
} }
.search-input:focus { .search-input:focus {

View file

@ -138,10 +138,11 @@ export default {
const isMarkerClick = target.closest('.custom-marker'); const isMarkerClick = target.closest('.custom-marker');
if (!isMarkerClick) { if (!isMarkerClick) {
emit("map-click", { const clickPos = {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lng: e.lngLat.lng lng: e.lngLat.lng
}); };
emit("map-click", clickPos);
} }
}); });
} catch (error) { } catch (error) {
@ -176,20 +177,27 @@ export default {
return; return;
} }
// Create marker element // Create marker element (outer wrapper for MapLibre positioning)
const el = document.createElement("div"); const el = document.createElement("div");
el.className = "custom-marker"; el.className = "custom-marker";
// Create inner wrapper for visual transforms (isolates from MapLibre transforms)
const inner = document.createElement("div");
inner.className = "marker-inner";
if (props.selectedMarkerId === markerData.id) { if (props.selectedMarkerId === markerData.id) {
el.classList.add("selected"); inner.classList.add("selected");
} }
// Add marker number // Add marker number
const numberEl = document.createElement("div"); const numberEl = document.createElement("div");
numberEl.className = "marker-number"; numberEl.className = "marker-number";
numberEl.textContent = index + 1; numberEl.textContent = index + 1;
el.appendChild(numberEl); inner.appendChild(numberEl);
el.appendChild(inner);
try { try {
const coords = [markerData.position.lon, markerData.position.lat];
// Create MapLibre marker // Create MapLibre marker
// Anchor at bottom-center (where the pin tip is) // Anchor at bottom-center (where the pin tip is)
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
@ -197,7 +205,7 @@ export default {
draggable: true, draggable: true,
anchor: 'bottom' anchor: 'bottom'
}) })
.setLngLat([markerData.position.lon, markerData.position.lat]) .setLngLat(coords)
.addTo(map.value); .addTo(map.value);
// Handle marker drag // Handle marker drag
@ -244,10 +252,13 @@ export default {
function updateMarkerSelection(selectedId) { function updateMarkerSelection(selectedId) {
markerElements.value.forEach(({ element }, markerId) => { markerElements.value.forEach(({ element }, markerId) => {
if (element) { if (element) {
const inner = element.querySelector('.marker-inner');
if (inner) {
if (markerId === selectedId) { if (markerId === selectedId) {
element.classList.add("selected"); inner.classList.add("selected");
} else { } else {
element.classList.remove("selected"); inner.classList.remove("selected");
}
} }
} }
}); });
@ -332,22 +343,27 @@ export default {
} }
} }
/* Custom marker styles */ /* Custom marker outer wrapper - NO transforms here, MapLibre handles positioning */
.custom-marker { .custom-marker {
width: 40px; /* MapLibre will position this element via transform: translate3d() */
height: 40px;
cursor: grab; cursor: grab;
display: flex;
align-items: center;
justify-content: center;
position: relative;
} }
.custom-marker:active { .custom-marker:active {
cursor: grabbing; cursor: grabbing;
} }
.custom-marker::before { /* Inner wrapper for visual styling - transforms are isolated here */
.marker-inner {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.marker-inner::before {
content: ""; content: "";
position: absolute; position: absolute;
width: 40px; width: 40px;
@ -360,12 +376,12 @@ export default {
transition: all 0.2s; transition: all 0.2s;
} }
.custom-marker:hover::before { .marker-inner:hover::before {
background: #c0392b; background: #c0392b;
transform: rotate(-45deg) scale(1.1); transform: rotate(-45deg) scale(1.1);
} }
.custom-marker.selected::before { .marker-inner.selected::before {
background: #3498db; background: #3498db;
border-color: #2980b9; border-color: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.6); box-shadow: 0 4px 12px rgba(52, 152, 219, 0.6);
@ -378,7 +394,7 @@ export default {
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;
line-height: 1; line-height: 1;
transform: translateY(-4px); /* transform removed - was causing positioning issues */
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
pointer-events: none; pointer-events: none;
} }

View file

@ -1,64 +1,65 @@
<template> <template>
<div class="marker-list-sidebar"> <aside class="k-map-markers-sidebar">
<!-- Header with add button --> <!-- Header with counter and add button -->
<div class="marker-list-header"> <header class="k-section-header">
<h3>Marqueurs ({{ markers.length }}/{{ maxMarkers }})</h3> <k-headline>
<button Marqueurs
type="button" <k-counter>{{ markers.length }}/{{ maxMarkers }}</k-counter>
class="k-button k-button-icon" </k-headline>
<k-button
icon="add"
size="xs"
variant="filled"
@click="$emit('add-marker')" @click="$emit('add-marker')"
:disabled="!canAddMarker" :disabled="!canAddMarker"
title="Ajouter un marqueur"
> >
<k-icon type="add" /> Ajouter
</button> </k-button>
</div> </header>
<!-- Geocode search --> <!-- Geocode search -->
<div class="geocode-search-container"> <div class="k-map-markers-search">
<GeocodeSearch @select-location="$emit('select-location', $event)" /> <GeocodeSearch @select-location="$emit('select-location', $event)" />
</div> </div>
<!-- Marker list items --> <!-- Marker list -->
<div class="marker-list-items"> <div class="k-map-markers-list">
<div <div
v-for="(marker, index) in markers" v-for="(marker, index) in markers"
:key="marker.id" :key="marker.id"
class="marker-item" class="k-map-marker-item"
:class="{ active: selectedMarkerId === marker.id }" :class="{ 'is-selected': selectedMarkerId === marker.id }"
@click="$emit('select-marker', marker.id)" @click="$emit('select-marker', marker.id)"
> >
<div class="marker-item-content"> <span class="k-map-marker-icon">
<span class="marker-number">{{ index + 1 }}</span> {{ index + 1 }}
<span class="marker-title"> </span>
<span class="k-map-marker-text">
{{ marker.title || `Marqueur ${index + 1}` }} {{ marker.title || `Marqueur ${index + 1}` }}
</span> </span>
</div> <span class="k-map-marker-options">
<div class="marker-item-actions"> <k-button
<button icon="open"
type="button" size="xs"
class="k-button k-button-small"
@click.stop="$emit('edit-marker', marker.id)" @click.stop="$emit('edit-marker', marker.id)"
title="Modifier le marqueur" />
> <k-button
<k-icon type="edit" /> icon="trash"
</button> size="xs"
<button
type="button"
class="k-button k-button-small"
@click.stop="$emit('delete-marker', marker.id)" @click.stop="$emit('delete-marker', marker.id)"
title="Supprimer le marqueur" />
> </span>
<k-icon type="trash" />
</button>
</div>
</div> </div>
<div v-if="markers.length === 0" class="marker-list-empty"> <div v-if="markers.length === 0" class="k-map-markers-empty">
Cliquez sur la carte pour ajouter des marqueurs <k-icon type="map-pin" />
</div> <p class="k-map-markers-empty-text">Aucun marqueur</p>
<p class="k-map-markers-empty-info">
Cliquez sur la carte ou sur "Ajouter" pour créer un marqueur
</p>
</div> </div>
</div> </div>
</aside>
</template> </template>
<script> <script>
@ -94,7 +95,9 @@ export default {
], ],
setup(props) { setup(props) {
const canAddMarker = computed(() => props.markers.length < props.maxMarkers); const canAddMarker = computed(
() => props.markers.length < props.maxMarkers
);
return { return {
canAddMarker, canAddMarker,
@ -104,143 +107,130 @@ export default {
</script> </script>
<style scoped> <style scoped>
.marker-list-sidebar { /* Sidebar container - uses Kirby's layout system */
width: var(--marker-list-width, 250px); .k-map-markers-sidebar {
width: var(--marker-list-width, 280px);
flex-shrink: 0; flex-shrink: 0;
border-right: 1px solid var(--color-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--color-background); border-right: 1px solid var(--color-border);
background: var(--color-white);
} }
.marker-list-header { /* Header - minimal override of k-section-header */
.k-section-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: var(--spacing-2);
padding: 0.75rem 1rem; padding: var(--spacing-3);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
background: var(--color-white); background: var(--panel-color-back);
margin-bottom: 0;
} }
.marker-list-header h3 { .k-section-header .k-headline {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
color: var(--color-text-light);
flex: 1;
}
.marker-list-header .k-button-icon {
width: 2rem;
height: 2rem;
min-width: 2rem;
padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: var(--spacing-2);
color: var(--color-black);
} }
.marker-list-header .k-button-icon .k-icon { /* Search container */
color: var(--color-black); .k-map-markers-search {
} padding: var(--spacing-3);
border-bottom: 1px solid var(--color-border);
.marker-list-header .k-button-icon:hover {
background: var(--color-background); background: var(--color-background);
} }
.marker-list-header .k-button-icon:disabled { /* List container */
opacity: 0.4; .k-map-markers-list {
cursor: not-allowed;
}
.geocode-search-container {
padding: 0.75rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-white);
}
.marker-list-items {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 0.5rem; padding: var(--spacing-2);
background: var(--color-background);
} }
.marker-item { /* Marker item - styled like Kirby's k-item */
.k-map-marker-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: var(--spacing-2);
padding: 0.75rem; padding: var(--spacing-2);
margin-bottom: 0.5rem; margin-bottom: var(--spacing-1);
background: var(--color-white); background: var(--color-white);
border: 1px solid var(--color-border); border-radius: var(--rounded);
border-radius: var(--rounded-sm);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
color: #000;
} }
.marker-item:hover { .k-map-marker-item:hover {
background: var(--color-background); background: var(--color-gray-200);
border-color: var(--color-focus);
color: #fff;
} }
.marker-item.active { .k-map-marker-item.is-selected {
background: var(--color-focus-outline); background: var(--color-blue-300);
border-color: var(--color-focus); color: var(--color-white);
} }
.marker-item-content { .k-map-marker-icon {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.marker-number {
display: inline-flex;
align-items: center;
justify-content: center; justify-content: center;
width: 24px; width: 2rem;
height: 24px; height: 2rem;
background: var(--color-gray-400);
color: var(--color-white);
border-radius: 50%; border-radius: 50%;
font-size: 0.75rem; background: var(--color-gray-300);
font-weight: 600; color: var(--color-white);
flex-shrink: 0; flex-shrink: 0;
} }
.marker-item.active .marker-number { .k-map-marker-item.is-selected .k-map-marker-icon {
background: var(--color-focus); background: var(--color-blue-600);
} }
.marker-title { .k-map-marker-text {
font-size: 0.875rem; flex: 1;
min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-size: var(--text-sm);
} }
.marker-item-actions { .k-map-marker-options {
display: flex; display: flex;
gap: 0.25rem; gap: var(--spacing-1);
flex-shrink: 0; flex-shrink: 0;
} }
.k-button-small { /* Empty state - styled like Kirby's k-empty */
padding: 0.25rem 0.5rem; .k-map-markers-empty {
min-height: auto; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-12) var(--spacing-6);
text-align: center;
color: var(--color-gray-600);
} }
.marker-list-empty { .k-map-markers-empty .k-icon {
padding: 2rem 1rem; width: 3rem;
text-align: center; height: 3rem;
color: var(--color-text-light); margin-bottom: var(--spacing-3);
font-size: 0.875rem; opacity: 0.25;
}
.k-map-markers-empty-text {
margin: 0 0 var(--spacing-2);
font-size: var(--text-base);
font-weight: 500;
color: var(--color-gray-800);
}
.k-map-markers-empty-info {
margin: 0;
font-size: var(--text-sm);
color: var(--color-gray-600);
} }
</style> </style>