feat: add custom marker icons with configurable size

- Add markerIcon files field to marker.yml for custom JPG/PNG/SVG icons
- Add markerIconSize range field (20-500px, default 40px) with unit display
- Layout icon fields side-by-side (50/50 width) in marker blueprint
- Add markerIconUrl prop in index.php to auto-detect uploaded icon
- Add markerIconSize prop in index.php to read size from page data
- Update MapPreview.vue to display custom images instead of default pins
- Set icon dimensions dynamically based on markerIconSize value
- Icon size updates on save/reload (reactive implementation deferred)
- Remove custom tiles background functionality (not needed)

Note: Custom icons show uploaded image, may have white background on
transparent PNGs depending on image processing. Size is non-reactive
and requires save + reload to update in preview.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-29 16:14:33 +01:00
parent 925e98aea7
commit b19635f324
6 changed files with 161 additions and 58 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -28,6 +28,23 @@ Kirby::plugin('geoproject/map-editor', [
},
'longitude' => function ($longitude = null) {
return $longitude;
},
'markerIconUrl' => function ($markerIconUrl = null) {
// Auto-detect marker icon from page files
if ($markerIconUrl === null && $this->model()) {
$iconFile = $this->model()->markerIcon()->toFile();
if ($iconFile) {
return $iconFile->url();
}
}
return $markerIconUrl;
},
'markerIconSize' => function ($markerIconSize = 40) {
// Auto-detect marker icon size from page
if ($this->model() && $this->model()->markerIconSize()->isNotEmpty()) {
return (int) $this->model()->markerIconSize()->value();
}
return $markerIconSize;
}
]
]

View file

@ -79,6 +79,14 @@ export default {
type: [Number, String],
default: null,
},
markerIconUrl: {
type: String,
default: null,
},
markerIconSize: {
type: Number,
default: 40,
},
},
setup(props, { emit }) {
@ -138,6 +146,8 @@ export default {
id: 'single-marker',
position: { lat, lon },
title: 'Current position',
iconUrl: props.markerIconUrl,
iconSize: props.markerIconSize,
}];
}
return [];

View file

@ -181,19 +181,39 @@ export default {
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) {
inner.classList.add("selected");
}
// Check if custom icon is provided
if (markerData.iconUrl) {
// Use custom image
el.classList.add("custom-icon");
const img = document.createElement("img");
img.src = markerData.iconUrl;
img.className = "marker-icon-image";
// Add marker number
const numberEl = document.createElement("div");
numberEl.className = "marker-number";
numberEl.textContent = index + 1;
inner.appendChild(numberEl);
el.appendChild(inner);
// Set size from marker data or default to 40px
const size = markerData.iconSize || 40;
img.style.width = `${size}px`;
img.style.height = `${size}px`;
if (props.selectedMarkerId === markerData.id) {
img.classList.add("selected");
}
el.appendChild(img);
} else {
// Use default pin 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) {
inner.classList.add("selected");
}
// Add marker number
const numberEl = document.createElement("div");
numberEl.className = "marker-number";
numberEl.textContent = index + 1;
inner.appendChild(numberEl);
el.appendChild(inner);
}
try {
const coords = [markerData.position.lon, markerData.position.lat];
@ -252,6 +272,7 @@ export default {
function updateMarkerSelection(selectedId) {
markerElements.value.forEach(({ element }, markerId) => {
if (element) {
// Handle default pin marker
const inner = element.querySelector('.marker-inner');
if (inner) {
if (markerId === selectedId) {
@ -260,6 +281,16 @@ export default {
inner.classList.remove("selected");
}
}
// Handle custom icon marker
const img = element.querySelector('.marker-icon-image');
if (img) {
if (markerId === selectedId) {
img.classList.add("selected");
} else {
img.classList.remove("selected");
}
}
}
});
}
@ -399,6 +430,30 @@ export default {
pointer-events: none;
}
/* Custom icon marker */
.custom-marker.custom-icon {
cursor: grab;
}
.custom-marker.custom-icon:active {
cursor: grabbing;
}
.marker-icon-image {
object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
transition: all 0.2s;
}
.marker-icon-image:hover {
transform: scale(1.1);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
}
.marker-icon-image.selected {
filter: drop-shadow(0 4px 12px rgba(52, 152, 219, 0.8));
}
/* MapLibre controls styling */
.maplibregl-ctrl-group {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);