diff --git a/demos/custom-icon.html b/demos/custom-icon.html
index 076e88e..1448d5e 100644
--- a/demos/custom-icon.html
+++ b/demos/custom-icon.html
@@ -300,6 +300,17 @@
>
I am a orange leaf.
+
+
+
+ ★
+
+ I use a DivIcon! Content comes from innerHTML.
+
diff --git a/index.html b/index.html
index a5f82bd..bbfdda2 100644
--- a/index.html
+++ b/index.html
@@ -303,6 +303,13 @@
+
+ ★
+
Part of a feature group
@@ -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' },
diff --git a/src/components/leaflet-div-icon.ts b/src/components/leaflet-div-icon.ts
new file mode 100644
index 0000000..9c6ca9d
--- /dev/null
+++ b/src/components/leaflet-div-icon.ts
@@ -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 = {};
+ 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);
diff --git a/src/components/leaflet-icon.ts b/src/components/leaflet-icon.ts
index 42023b0..0ba2ddc 100644
--- a/src/components/leaflet-icon.ts
+++ b/src/components/leaflet-icon.ts
@@ -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 = {};
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;
}
diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts
index 3454555..73b296f 100644
--- a/src/components/leaflet-marker.ts
+++ b/src/components/leaflet-marker.ts
@@ -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());
}
}
diff --git a/src/core/register.ts b/src/core/register.ts
index 4f3e340..68cb5a1 100644
--- a/src/core/register.ts
+++ b/src/core/register.ts
@@ -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>;
+
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 = {
diff --git a/src/index.ts b/src/index.ts
index ba8b88b..d2f6b77 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';