feat: split leaflet-icon into image Icon and DivIcon, event-only marker↔icon handshake
- Strip leaflet-icon.ts to image-only (new Icon(), remove div/html/bgPos) - Create leaflet-div-icon.ts with DivIcon, innerHTML content, MutationObserver - Convert marker↔icon from querySelector to event-only protocol: marker iterates children dispatching leaflet-request-icon on each, icons listen on themselves and respond with icon-changed - Extract emitIconChanged() helper into register.ts to eliminate repeated CustomEvent construction across both icon components - Type icon-changed and leaflet-request-icon in HTMLElementEventMap - Update marker's #onIconChanged to use typed event instead of cast - Export LeafletDivIcon from index.ts - Add DivIcon demo markers to custom-icon.html and index.html
This commit is contained in:
parent
0855851504
commit
35190bacc5
@ -300,6 +300,17 @@
|
||||
></leaflet-icon>
|
||||
<leaflet-popup>I am a orange leaf.</leaflet-popup>
|
||||
</leaflet-marker>
|
||||
|
||||
<leaflet-marker lat="51.48" lng="-0.095">
|
||||
<leaflet-div-icon
|
||||
class-name="my-div-icon"
|
||||
icon-size="[24, 24]"
|
||||
icon-anchor="[12, 12]"
|
||||
>
|
||||
<div style="width:24px;height:24px;background:#e74c3c;border:2px solid #c0392b;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;font-size:14px;">★</div>
|
||||
</leaflet-div-icon>
|
||||
<leaflet-popup>I use a DivIcon! Content comes from innerHTML.</leaflet-popup>
|
||||
</leaflet-marker>
|
||||
</leaflet-map>
|
||||
|
||||
|
||||
|
||||
35
index.html
35
index.html
@ -303,6 +303,13 @@
|
||||
|
||||
<leaflet-feature-group>
|
||||
<leaflet-marker lat="51.513" lng="-0.07" title="Feature Group Marker">
|
||||
<leaflet-div-icon
|
||||
class-name="my-div-icon"
|
||||
icon-size="[24, 24]"
|
||||
icon-anchor="[12, 12]"
|
||||
>
|
||||
<div style="width:24px;height:24px;background:#e74c3c;border:2px solid #c0392b;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;font-size:14px;">★</div>
|
||||
</leaflet-div-icon>
|
||||
<leaflet-tooltip>Part of a feature group</leaflet-tooltip>
|
||||
</leaflet-marker>
|
||||
<leaflet-circle lat="51.513" lng="-0.07" radius="100" color="purple" fill-color="purple" fill-opacity="0.2">
|
||||
@ -353,7 +360,7 @@
|
||||
'leaflet-polygon': ['leaflet-line', 'leaflet-popup'],
|
||||
'leaflet-polyline': ['leaflet-line', 'leaflet-popup'],
|
||||
'leaflet-rectangle': ['leaflet-popup'],
|
||||
'leaflet-marker': ['leaflet-popup', 'leaflet-tooltip'],
|
||||
'leaflet-marker': ['leaflet-popup', 'leaflet-tooltip', 'leaflet-icon', 'leaflet-div-icon'],
|
||||
'leaflet-circle': ['leaflet-popup', 'leaflet-tooltip'],
|
||||
'leaflet-circle-marker': ['leaflet-popup', 'leaflet-tooltip'],
|
||||
'leaflet-image-overlay': ['leaflet-popup', 'leaflet-tooltip'],
|
||||
@ -630,6 +637,32 @@
|
||||
},
|
||||
'leaflet-layer-group': {},
|
||||
'leaflet-feature-group': {},
|
||||
'leaflet-icon': {
|
||||
Attributes: [
|
||||
{ attr: 'icon-url', kind: 'text' },
|
||||
{ attr: 'icon-retina-url', kind: 'text' },
|
||||
{ attr: 'icon-size', kind: 'text' },
|
||||
{ attr: 'icon-anchor', kind: 'text' },
|
||||
{ attr: 'popup-anchor', kind: 'text' },
|
||||
{ attr: 'tooltip-anchor', kind: 'text' },
|
||||
{ attr: 'shadow-url', kind: 'text' },
|
||||
{ attr: 'shadow-retina-url', kind: 'text' },
|
||||
{ attr: 'shadow-size', kind: 'text' },
|
||||
{ attr: 'shadow-anchor', kind: 'text' },
|
||||
{ attr: 'class-name', kind: 'text' },
|
||||
],
|
||||
},
|
||||
'leaflet-div-icon': {
|
||||
Attributes: [
|
||||
{ attr: 'icon-size', kind: 'text' },
|
||||
{ attr: 'icon-anchor', kind: 'text' },
|
||||
{ attr: 'popup-anchor', kind: 'text' },
|
||||
{ attr: 'tooltip-anchor', kind: 'text' },
|
||||
{ attr: 'class-name', kind: 'text' },
|
||||
{ attr: 'html', kind: 'text' },
|
||||
{ attr: 'bg-pos', kind: 'text' },
|
||||
],
|
||||
},
|
||||
'leaflet-popup': {
|
||||
Position: [
|
||||
{ attr: 'lat', kind: 'num', step: 'any' },
|
||||
|
||||
74
src/components/leaflet-div-icon.ts
Normal file
74
src/components/leaflet-div-icon.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { DivIcon, DivIconOptions, Icon } from 'leaflet';
|
||||
import { emitIconChanged } from '../core/register.ts';
|
||||
|
||||
const PROPS = {
|
||||
iconSize: { attr: 'icon-size' },
|
||||
iconAnchor: { attr: 'icon-anchor' },
|
||||
popupAnchor: { attr: 'popup-anchor' },
|
||||
tooltipAnchor: { attr: 'tooltip-anchor' },
|
||||
className: { attr: 'class-name' },
|
||||
html: { attr: 'html' },
|
||||
bgPos: { attr: 'bg-pos' },
|
||||
} as const;
|
||||
|
||||
const JSON_KEYS = new Set(['iconSize', 'iconAnchor', 'popupAnchor', 'tooltipAnchor', 'bgPos']);
|
||||
|
||||
export class LeafletDivIcon extends HTMLElement {
|
||||
#obj?: Icon;
|
||||
#observer?: MutationObserver;
|
||||
#onRequest = () => {
|
||||
if (!this.#obj) return;
|
||||
emitIconChanged(this, this.#obj);
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
this.#applyIcon();
|
||||
emitIconChanged(this, this.#obj);
|
||||
this.addEventListener('leaflet-request-icon', this.#onRequest);
|
||||
|
||||
this.#observer = new MutationObserver(() => {
|
||||
this.#applyIcon();
|
||||
emitIconChanged(this, this.#obj);
|
||||
});
|
||||
this.#observer.observe(this, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('leaflet-request-icon', this.#onRequest);
|
||||
this.#observer?.disconnect();
|
||||
this.#observer = undefined;
|
||||
emitIconChanged(this, null);
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return Object.values(PROPS).map((s) => s.attr);
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.#applyIcon();
|
||||
if (!this.#obj) return;
|
||||
emitIconChanged(this, this.#obj);
|
||||
}
|
||||
|
||||
get leafletObject() {
|
||||
return this.#obj;
|
||||
}
|
||||
|
||||
#applyIcon() {
|
||||
const opts: Record<string, unknown> = {};
|
||||
for (const [key, spec] of Object.entries(PROPS)) {
|
||||
const v = this.getAttribute(spec.attr);
|
||||
if (v === null) continue;
|
||||
opts[key] = JSON_KEYS.has(key) ? JSON.parse(v) : v;
|
||||
}
|
||||
const content = this.innerHTML;
|
||||
if (content) opts.html = content;
|
||||
this.#obj = new DivIcon(opts as DivIconOptions) as unknown as Icon;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('leaflet-div-icon', LeafletDivIcon);
|
||||
@ -1,23 +1,19 @@
|
||||
import { Icon, icon, divIcon, IconOptions, DivIconOptions } from 'leaflet';
|
||||
import { defineProps, str, on } from '../core/props.ts';
|
||||
import { WithProps } from '../core/utils.ts';
|
||||
import { Icon, IconOptions } from 'leaflet';
|
||||
import { emitIconChanged } from '../core/register.ts';
|
||||
|
||||
const PROPS = defineProps({
|
||||
iconUrl: str(),
|
||||
iconRetinaUrl: str(),
|
||||
iconSize: str(),
|
||||
iconAnchor: str(),
|
||||
popupAnchor: str(),
|
||||
tooltipAnchor: str(),
|
||||
shadowUrl: str(),
|
||||
shadowRetinaUrl: str(),
|
||||
shadowSize: str(),
|
||||
shadowAnchor: str(),
|
||||
className: str(),
|
||||
div: on(),
|
||||
html: str(),
|
||||
bgPos: str(),
|
||||
});
|
||||
const PROPS = {
|
||||
iconUrl: { attr: 'icon-url' },
|
||||
iconRetinaUrl: { attr: 'icon-retina-url' },
|
||||
iconSize: { attr: 'icon-size' },
|
||||
iconAnchor: { attr: 'icon-anchor' },
|
||||
popupAnchor: { attr: 'popup-anchor' },
|
||||
tooltipAnchor: { attr: 'tooltip-anchor' },
|
||||
shadowUrl: { attr: 'shadow-url' },
|
||||
shadowRetinaUrl: { attr: 'shadow-retina-url' },
|
||||
shadowSize: { attr: 'shadow-size' },
|
||||
shadowAnchor: { attr: 'shadow-anchor' },
|
||||
className: { attr: 'class-name' },
|
||||
} as const;
|
||||
|
||||
const JSON_KEYS = new Set([
|
||||
'iconSize',
|
||||
@ -26,40 +22,34 @@ const JSON_KEYS = new Set([
|
||||
'tooltipAnchor',
|
||||
'shadowSize',
|
||||
'shadowAnchor',
|
||||
'bgPos',
|
||||
]);
|
||||
|
||||
export class LeafletIcon extends WithProps(HTMLElement, PROPS) {
|
||||
export class LeafletIcon extends HTMLElement {
|
||||
#obj?: Icon;
|
||||
#onRequest = () => {
|
||||
if (!this.#obj) return;
|
||||
emitIconChanged(this, this.#obj);
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
this.#applyIcon();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('icon-changed', {
|
||||
bubbles: true,
|
||||
detail: { icon: this.#obj },
|
||||
}),
|
||||
);
|
||||
emitIconChanged(this, this.#obj);
|
||||
this.addEventListener('leaflet-request-icon', this.#onRequest);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('icon-changed', {
|
||||
bubbles: true,
|
||||
detail: { icon: null },
|
||||
}),
|
||||
);
|
||||
this.removeEventListener('leaflet-request-icon', this.#onRequest);
|
||||
emitIconChanged(this, null);
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return Object.values(PROPS).map((s) => s.attr);
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.#applyIcon();
|
||||
if (!this.#obj) return;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('icon-changed', {
|
||||
bubbles: true,
|
||||
detail: { icon: this.#obj },
|
||||
}),
|
||||
);
|
||||
emitIconChanged(this, this.#obj);
|
||||
}
|
||||
|
||||
get leafletObject() {
|
||||
@ -69,15 +59,12 @@ export class LeafletIcon extends WithProps(HTMLElement, PROPS) {
|
||||
#applyIcon() {
|
||||
const opts: Record<string, unknown> = {};
|
||||
for (const [key, spec] of Object.entries(PROPS)) {
|
||||
if (key === 'div') continue;
|
||||
const v = this.getAttribute(spec.attr);
|
||||
if (v === null) continue;
|
||||
opts[key] = JSON_KEYS.has(key) ? JSON.parse(v) : v;
|
||||
}
|
||||
if (this.hasAttribute('div')) {
|
||||
this.#obj = divIcon(opts as unknown as DivIconOptions) as unknown as Icon;
|
||||
} else if (opts.iconUrl) {
|
||||
this.#obj = icon(opts as unknown as IconOptions) as unknown as Icon;
|
||||
if (opts.iconUrl) {
|
||||
this.#obj = new Icon(opts as unknown as IconOptions);
|
||||
} else {
|
||||
this.#obj = undefined;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Icon, Marker } from 'leaflet';
|
||||
import { defineProps, num, str, on } from '../core/props.ts';
|
||||
import { registerChildren, unregisterChildren } from '../core/register.ts';
|
||||
import { LeafletIconChangedEvent, registerChildren, unregisterChildren } from '../core/register.ts';
|
||||
import { WithProps, buildAttrMap, buildOptions, numAttr, setLayerAttr } from '../core/utils.ts';
|
||||
|
||||
const PROPS = defineProps({
|
||||
@ -27,10 +27,9 @@ export class LeafletMarker extends WithProps(HTMLElement, PROPS) {
|
||||
this.#obj.on('dragend move', this.#onChange, this);
|
||||
registerChildren(this, this.#obj);
|
||||
this.addEventListener('icon-changed', this.#onIconChanged);
|
||||
const iconEl = this.querySelector(':scope > leaflet-icon') as unknown as {
|
||||
leafletObject: Icon;
|
||||
} | null;
|
||||
if (iconEl?.leafletObject) this.#obj.setIcon(iconEl.leafletObject);
|
||||
for (const child of this.children) {
|
||||
child.dispatchEvent(new CustomEvent('leaflet-request-icon'));
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -72,10 +71,9 @@ export class LeafletMarker extends WithProps(HTMLElement, PROPS) {
|
||||
this.#syncing = false;
|
||||
}
|
||||
|
||||
#onIconChanged(e: Event) {
|
||||
#onIconChanged(e: LeafletIconChangedEvent) {
|
||||
if (!this.#obj) return;
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail?.icon) this.#obj.setIcon(detail.icon);
|
||||
if (e.detail.icon) this.#obj.setIcon(e.detail.icon);
|
||||
else this.#obj.setIcon(new Icon.Default());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
|
||||
import { Icon, Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
|
||||
import { registerWithParent } from './utils.ts';
|
||||
|
||||
// Custom event type for the bubbling registration protocol. Carries the
|
||||
@ -11,14 +11,29 @@ export type LeafletRegisterEvent = CustomEvent<{
|
||||
|
||||
export type LeafletLayerEvent = CustomEvent<{ layer: Layer }>;
|
||||
|
||||
export type LeafletIconChangedEvent = CustomEvent<{ icon: Icon | null }>;
|
||||
|
||||
export type LeafletRequestIconEvent = CustomEvent<Record<string, never>>;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'leaflet-register': LeafletRegisterEvent;
|
||||
'leaflet-add-layer': LeafletLayerEvent;
|
||||
'leaflet-remove-layer': LeafletLayerEvent;
|
||||
'icon-changed': LeafletIconChangedEvent;
|
||||
'leaflet-request-icon': LeafletRequestIconEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export function emitIconChanged(el: HTMLElement, icon: Icon | null | undefined) {
|
||||
el.dispatchEvent(
|
||||
new CustomEvent('icon-changed', {
|
||||
bubbles: true,
|
||||
detail: { icon: icon ?? null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Tracks whether a child registered as a plain layer, popup, or tooltip --
|
||||
// used by the controls panel and by cleanup logic.
|
||||
type ChildEntry = {
|
||||
|
||||
@ -24,3 +24,4 @@ export { LeafletControlScale } from './components/leaflet-control-scale.ts';
|
||||
export { LeafletPopup } from './components/leaflet-popup.ts';
|
||||
export { LeafletTooltip } from './components/leaflet-tooltip.ts';
|
||||
export { LeafletIcon } from './components/leaflet-icon.ts';
|
||||
export { LeafletDivIcon } from './components/leaflet-div-icon.ts';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user