MapView
MoleculeLeaflet-based interactive map. Tooltip-enabled markers, predefined zones (polygon), route lines (polyline), and click-to-add marker mode.
<%- include('modules/ui/MapView/MapView', {
center: [41.015, 28.979],
zoom: 6,
height: 400,
markers: CITIES,
zones: ZONES,
routes: ROUTES,
}) %>
<%- include('modules/ui/MapView/MapView', {
center: [39.5, 35.0],
zoom: 5,
height: 360,
}) %>
<%- include('modules/ui/MapView/MapView', {
center: [39.5, 35.0],
zoom: 5,
height: 360,
zones: ZONES,
routes: ROUTES,
}) %>
<%
// MapView — provider-agnostic interactive map (EJS).
// Mirror of NextJS modules/ui/MapView/index.tsx.
//
// M1: leaflet provider, token-aware keyless Esri tiles (Light Gray / Dark Gray Canvas),
// IntersectionObserver-driven lazy load, fit-to-bounds.
//
// Locals:
// id — DOM id base (auto-generated when omitted)
// provider — 'leaflet' | 'mapbox' | 'google' (default 'leaflet')
// center, zoom — view start
// markers, zones, routes
// fitBoundsPadding — number → enables fit-to-bounds
// tiles — { url, attribution } (both themes) or { light, dark };
// default Esri Light Gray / Dark Gray Canvas with place names
// minZoom, maxZoom, cluster, clusterRadius, showLabels, tooltipTrigger,
// scrollWheelZoom, zoomControl, controlsPosition, fitMaxZoom
// — display options, see partials/_display.ejs; markers as MapCanvas
// (shape 'image' with `image`, `draggable`)
// onMarkerMove — NAME of a global function called with (id, position) when a draggable marker is dropped; the
// DOM event 'kui:map-marker-move' (detail: { id, position }) fires too
// loadingLabel — text shown until the map is built (default 'Loading map…')
// height — px or CSS string
// className — extra root classes
//
// TODO M1+: providers/mapbox + google adapters.
// TODO M2: activeMarkerId.
// TODO M3: search, geocode, route.
// TODO M4: draw, locate, layer toggle.
// TODO M5: a11y polish, i18n messages, telemetry.
var _id = locals.id || ('map-' + Math.random().toString(36).substr(2, 9));
var _provider = locals.provider || 'leaflet';
var _center = locals.center || [39.9334, 32.8597];
var _zoom = locals.zoom || 6;
var _height = locals.height || 480;
var _markers = locals.markers || [];
var _zones = locals.zones || [];
var _routes = locals.routes || [];
var _fitBoundsPadding = locals.fitBoundsPadding;
var _cls = locals.className || '';
var _tiles = locals.tiles || null;
var _loadingLabel = locals.loadingLabel || 'Loading map…';
var _onMove = typeof locals.onMarkerMove === 'string' ? locals.onMarkerMove : '';
var D = {};
include('./partials/_display', { D: D, src: locals });
if (_provider !== 'leaflet') {
throw new Error('MapView provider "' + _provider + '" is not yet implemented — TODO M1+');
}
// Button base classes — kept in sync with Button.ejs `xs` size + primary/outline variants.
var btnBase = 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus disabled:opacity-50 disabled:cursor-not-allowed px-2 py-1 text-xs';
var btnPrimary = 'bg-primary text-primary-fg hover:bg-primary-hover';
var btnOutline = 'border border-border text-text-primary hover:bg-surface-overlay';
// CSS height
var _cssHeight = (typeof _height === 'number') ? (_height + 'px') : _height;
%>
<%/*
Inline Card(variant="raised") wrapper with -mx-6 -my-4 cancel-padding inner
so toolbar + map render edge-to-edge inside Card's rounded border.
*/%>
<div class="rounded-xl border border-border overflow-hidden text-left bg-surface-raised shadow-sm<%= _cls ? ' ' + _cls : '' %>"
style="isolation:isolate">
<div class="px-6 py-4">
<div class="-mx-6 -my-4 flex flex-col">
<%- include('./partials/_controls', {
_id: _id,
_zones: _zones,
_routes: _routes,
btnBase: btnBase,
btnPrimary: btnPrimary,
btnOutline: btnOutline
}) %>
<!-- Map canvas -->
<div id="<%= _id %>-map" style="height:<%= _cssHeight %>">
<div data-map-loading class="w-full h-full flex items-center justify-center bg-surface-raised">
<span class="text-sm text-text-secondary"><%= _loadingLabel %></span>
</div>
</div>
</div>
</div>
</div>
<script>
<%- include('./scripts/map-parts.js') %>
<%- include('./scripts/map-leaflet.js') %>
<%- include('./scripts/markers.js') %>
(function () {
var opts = {
id: '<%- _id %>',
center: [<%- _center[0] %>, <%- _center[1] %>],
zoom: <%- _zoom %>,
markers: <%- include('../_script-json', { value: _markers }) %>,
zones: <%- include('../_script-json', { value: _zones }) %>,
routes: <%- include('../_script-json', { value: _routes }) %>,
fitBoundsPadding: <%- _fitBoundsPadding != null ? _fitBoundsPadding : 'undefined' %>,
tiles: <%- include('../_script-json', { value: _tiles }) %>,
display: <%- include('../_script-json', { value: D.value }) %>,
onReady: function (ctx) { window.__MapViewMarkers.attach({ id: '<%- _id %>', ctx: ctx }); },
onMarkerMove: function (markerId, position) {
var el = document.getElementById(<%- include('../_script-json', { value: _id + '-map' }) %>);
if (el) el.dispatchEvent(new CustomEvent('kui:map-marker-move', { bubbles: true, detail: { id: markerId, position: position } }));
var name = <%- include('../_script-json', { value: _onMove }) %>;
if (name && typeof window[name] === 'function') window[name](markerId, position);
}
};
opts = Object.assign(opts, opts.display);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { window.__MapViewLeaflet.init(opts); });
} else {
window.__MapViewLeaflet.init(opts);
}
})();
</script>