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
type: map-editor
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>
<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 MarkerList from '../map/MarkerList.vue';
import { useMarkersApi } from '../../composables/useMarkersApi.js';
@ -191,6 +191,7 @@ export default {
// Watch for changes in the form fields
const form = document.querySelector('.k-form');
if (form) {
// Listen to input events
form.addEventListener('input', (e) => {
if (e.target.name && (e.target.name.includes('latitude') || e.target.name.includes('longitude'))) {
const newCoords = getCoordinatesFromForm();
@ -198,6 +199,39 @@ export default {
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') {
// Center map on new position if valid
if (!isNaN(lat) && !isNaN(lon) && lat !== null && lon !== null && lat !== 0 && lon !== 0) {
if (mapPreview.value && mapPreview.value.centerOnPosition) {
mapPreview.value.centerOnPosition(lat, lon);
}
// Force immediate reactivity
nextTick(() => {
if (mapPreview.value && mapPreview.value.centerOnPosition) {
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);
transition: border-color 0.2s;
color: #000;
background: var(--input-color-back);
}
.search-input:focus {

View file

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

View file

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