Data

MapView

A map (Leaflet's) with prices or dots for places on a list, one picked, and a pin to drag where something is.

#media#data#interactive

Demo

Preview

Click a price.

The pin is at 38.7139, -9.1394. Drag it, or click the map.

A label's text is escaped: the last one is written "<b>€45</b>".

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/map_view.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/map_view.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/map_view.qwe
let map_view_demo_places = [    { id: 1, lat: 38.7114, lng: -9.1305, text: "€95", title: "Alfama flat with river view" },    { id: 2, lat: 38.7171, lng: -9.1312, text: "€140", title: "Tiled house in Graça" },    { id: 3, lat: 38.7130, lng: -9.1460, text: "€70", title: "Bairro Alto studio" },    { id: 4, lat: 38.7145, lng: -9.1600, text: "<b>€45</b>", title: "Room by the Estrela garden" },]component MapViewDemoLive() {    state picked = null    state cheap = false    state spot = [38.7139, -9.1394]    let shown = cheap ? map_view_demo_places | filter((p) => p.id != 2) : map_view_demo_places    <div.demo-stack>        <MapView label="Places to stay" center={[38.7139, -9.1394]} zoom={14} markers={shown} picked={picked}                 height={300} on_pick={(id) => { picked = id }}/>        <div.demo-row>            <button type="button" class="map-view-demo-cheap" @click={() => { cheap = !cheap }}>{cheap ? "Show all" : "Under €100 only"}</button>            <p.demo-note>{picked == null                ? "Click a price."                : `Picked ${(find(map_view_demo_places, (p) => p.id == picked) ?? { title: "?" }).title}.`}</p>        </div>        <MapView label="Where it is" center={spot} zoom={15} pin={spot} height={220}                 markers={[{ id: "cafe", lat: 38.7105, lng: -9.1415, tone: "warning", title: "A café" }]}                 on_pin={(lat, lng) => { spot = [lat, lng] }}/>        <p.demo-note>The pin is at {round(spot[0], 4)}, {round(spot[1], 4)}. Drag it, or click the map.</p>    </div>}view MapViewDemo() =    <div.demo-stack>        <MapViewDemoLive/>        <p.demo-note>A label's text is escaped: the last one is written "&lt;b&gt;€45&lt;/b&gt;".</p>    </div>

Source

ui/map_view.qwe
// MapView: a map with markers on it, for places on a list: a price or a name on// each, one picked, and a pin that can be dragged to where something is. The map// is drawn by Leaflet, which this file takes with its `client` line, so a page loads// the script, and the component's own <link> its styles, only where a map is shown.////   <MapView center={[38.7139, -9.1394]} markers={[{ id: 1, lat: 38.7114, lng: -9.1305, text: "€95" }]}//            picked={chosen} on_pick={(id) => { chosen = id }}/>//   <MapView label="Where it is" center={spot} zoom={16} pin={spot} on_pin={(lat, lng) => { spot = [lat, lng] }}/>//// Asked for by app/stays (quillolang/quillo, FEEDBACK5.md; branch stays-components).//// center: [lat, lng]; the map moves there when it changes. zoom: 1 to 19 (13).// markers: a list of { id, lat, lng, text?, title?, tone? }. With text (a price, a// name) the marker is a label; without, a dot, coloured by tone ("accent" by// default, "success", "warning", "danger", "info" or "neutral"). title: said on// hover and to screen readers. Give new markers and the map draws them again.// picked: the id of the marker shown picked. fit: true zooms to show every marker.// pin: [lat, lng] of a pin that can be dragged, or moved by a click on the map.// height: in pixels (320). label: what screen readers call the map ("Map").// tiles, attribution: where the map's tiles come from (OpenStreetMap's) and its credit.// on_pick(id): a marker clicked, or chosen with the keyboard (Tab, then Enter).// on_pin(lat, lng): where the pin was put. Both inside one of your components.// Text given to Leaflet is escaped: a marker's text may come from your users.// If the script cannot load the map stays empty, and the console says why.import ui/theme.qwe use ui_clsclient "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" use Llet map_view_css = css`/* set elsewhere: leaflet- (Leaflet's script sets them) */.ui-map-view { position: relative; isolation: isolate; width: 100%; overflow: hidden; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-subtle); font: 14px/1.4 var(--ui-font); }.ui-map-view-map { width: 100%; height: 100%; }.ui-map-view .leaflet-container { font: 12px/1.4 var(--ui-font); background: var(--ui-subtle); }.ui-map-view-label, .ui-map-view-dot, .ui-map-view-pin { width: 0 !important; height: 0 !important; background: none; border: 0; }.ui-map-view-label > span {  position: absolute; transform: translate(-50%, -50%); padding: 3px 8px; border: 1px solid var(--ui-border); border-radius: 999px;  background: var(--ui-surface); color: var(--ui-fg); font: 600 12px/1.2 var(--ui-font); white-space: nowrap; box-shadow: var(--ui-shadow);  cursor: pointer; transition: transform .1s;}.ui-map-view-label:hover > span { transform: translate(-50%, -50%) scale(1.06); }.ui-map-view-label.ui-map-view-picked > span { background: var(--ui-fg); color: var(--ui-bg); border-color: var(--ui-fg); }.ui-map-view-dot > span {  position: absolute; width: 12px; height: 12px; transform: translate(-50%, -50%); border: 2px solid #fff; border-radius: 50%;  background: var(--ui-accent); box-shadow: 0 0 0 1px rgb(0 0 0 / .3); cursor: pointer;}.ui-map-view-dot-success > span { background: var(--ui-success); }.ui-map-view-dot-warning > span { background: var(--ui-warning); }.ui-map-view-dot-danger > span { background: var(--ui-danger); }.ui-map-view-dot-info > span { background: var(--ui-info); }.ui-map-view-dot-neutral > span { background: var(--ui-muted); }.ui-map-view-dot.ui-map-view-picked > span { width: 18px; height: 18px; box-shadow: 0 0 0 3px var(--ui-accent-soft), 0 0 0 1px rgb(0 0 0 / .3); }.ui-map-view-pin > span {  position: absolute; width: 22px; height: 22px; transform: translate(-50%, -50%); border: 3px solid #fff; border-radius: 50%;  background: var(--ui-accent); box-shadow: 0 0 0 1px rgb(0 0 0 / .4), var(--ui-shadow); cursor: grab;}.ui-map-view .leaflet-marker-icon:focus-visible { outline: none; }.ui-map-view .leaflet-marker-icon:focus-visible > span { box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-map-view-label > span { transition: none; } }`fn map_view_icon(m, picked) {    let on = picked != null && m.id == picked    m.text != null        ? L.divIcon({ className: ui_cls(["ui-map-view-label", on && "ui-map-view-picked"]),                      html: `<span>${escape(str(m.text))}</span>`, iconSize: null })        : L.divIcon({ className: ui_cls(["ui-map-view-dot", `ui-map-view-dot-${m.tone ?? "accent"}`, on && "ui-map-view-picked"]),                      html: "<span></span>", iconSize: null })}component MapView(center: list, zoom: int = 13, markers: list = [], picked = null, fit: bool = false,                  pin: list? = null, height: int = 320, label: str = "Map",                  tiles: str = "https://tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: str = "© OpenStreetMap",                  on_pick = null, on_pin = null, class: str? = null) {    style global map_view_css    state leaflet = null    state layer = null    state pin_marker = null    // A new center: the map goes there.    if leaflet != null { leaflet.setView(center, zoom) }    // New markers, or another one picked: drawn again.    if leaflet != null && layer != null {        layer.clearLayers()        for m in markers {            let mk = L.marker([m.lat, m.lng], { icon: map_view_icon(m, picked), title: str(m.title ?? m.text ?? ""),                                                      alt: str(m.title ?? m.text ?? ""), keyboard: true, riseOnHover: true,                                                      zIndexOffset: picked != null && m.id == picked ? 1000 : 0 })            mk.on("click", () => { on_pick && on_pick(m.id) })            layer.addLayer(mk)        }        if fit && len(markers) > 0 {            leaflet.fitBounds(markers | map((m) => [m.lat, m.lng]), { padding: [32, 32], maxZoom: 16 })        }    }    // The pin follows `pin` when it changes.    if pin_marker != null && pin != null { pin_marker.setLatLng(pin) }    <div class={ui_cls(["ui-map-view", class])} role="region" aria-label={label} style={`height: ${height}px`}>        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"/>        <div.ui-map-view-map @mount={(el) => {            let m = L.map(el, { scrollWheelZoom: false })            m.setView(center, zoom)            L.tileLayer(tiles, { maxZoom: 19, attribution: escape(attribution) }).addTo(m)            layer = L.layerGroup().addTo(m)            if pin != null {                let p = L.marker(pin, { draggable: true, keyboard: true, title: "Drag to move", alt: "Pin", autoPan: true,                                        zIndexOffset: 2000,                                        icon: L.divIcon({ className: "ui-map-view-pin", html: "<span></span>", iconSize: null }) })                p.addTo(m)                p.on("dragend", () => {                    let at = p.getLatLng()                    on_pin && on_pin(at.lat, at.lng)                })                m.on("click", (e) => {                    p.setLatLng(e.latlng)                    on_pin && on_pin(e.latlng.lat, e.latlng.lng)                })                pin_marker = p            }            leaflet = m            () => m.remove()        }}></div>    </div>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On MapView: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.