feat: Initial implementation of leaflet-components
Wraps Leaflet.js as native Web Components. Each element maps 1:1 to a Leaflet object with reactive attribute binding and proper lifecycle cleanup. Key design decisions: - LeafletElement base class handles attribute→option mapping, the leaflet-register bubble event for parent/child wiring, and disconnectedCallback cleanup tracking parent bindings. - LeafletControl base class for map controls (zoom, attribution, scale). - leaflet-map uses a PROPS table to drive observedAttributes, getters/setters, attributeChangedCallback, and map event listeners from a single source of truth. Property types are derived via a mapped type over the table; the mixin-base pattern (TypedBase) makes them visible to TypeScript without an interface merge. - Boolean map options that default to true use disable-* attributes. - Leaflet and its CSS are bundled into dist/index.js via esbuild. - JSR config included for @buddy/leaflet-components.main
commit
6b448ba092
@ -0,0 +1,32 @@
|
|||||||
|
/.idea/
|
||||||
|
/jspm_packages/
|
||||||
|
/node_modules/
|
||||||
|
/web_modules/
|
||||||
|
/dist/
|
||||||
|
*.pid
|
||||||
|
*.pid.lock
|
||||||
|
*.seed
|
||||||
|
*.tgz
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.local
|
||||||
|
.env.production.local
|
||||||
|
.env.test.local
|
||||||
|
.eslintcache
|
||||||
|
.node_repl_history
|
||||||
|
.npm
|
||||||
|
.pnp.*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
.stylelintcache
|
||||||
|
.yarn-integrity
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.yarn/unplugged
|
||||||
|
lerna-debug.log*
|
||||||
|
npm-debug.log*
|
||||||
|
pids
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # Type-check + emit .d.ts via tsc, then bundle to dist/index.js via esbuild
|
||||||
|
npm run lint # ESLint over src/**/*.{ts,js}
|
||||||
|
npm run format # Prettier formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no tests in this project. To preview components locally, open `index.html` in a browser with a dev server that supports TypeScript (e.g. `npx vite`).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This library wraps [Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object.
|
||||||
|
|
||||||
|
### Core base class: `src/core/LeafletElement.ts`
|
||||||
|
|
||||||
|
`LeafletElement` (extends `HTMLElement`) is the abstract base for all components except `leaflet-map`. Subclasses must implement `createLeafletObject()` returning an `L.Layer`. The base class:
|
||||||
|
|
||||||
|
- Reads `observedAttributes` to auto-populate `this.options` via `initOptions()` on connect, converting kebab-case attribute names to camelCase for Leaflet options.
|
||||||
|
- Parses attribute values to booleans, numbers, or JSON automatically (`parseAttributeValue`).
|
||||||
|
- On `attributeChangedCallback`, calls `updateLeafletObject()` which by default invokes the matching Leaflet setter (e.g. `setOpacity`, `setRadius`). Subclasses override this for attributes needing custom logic (e.g. lat/lng pairs).
|
||||||
|
- Uses a custom `leaflet-register` bubbling event to wire children into parents. When a child connects, it dispatches this event upward; each parent intercepts and either calls `addLayer`, `bindPopup`, or `bindTooltip` depending on the child's type.
|
||||||
|
|
||||||
|
### `leaflet-map`: `src/components/leaflet-map.ts`
|
||||||
|
|
||||||
|
`LeafletMap` does **not** extend `LeafletElement` — it extends `HTMLElement` directly and uses Shadow DOM. It is the root of the component tree and terminates all bubbling `leaflet-register` events by calling `layer.addTo(this.map)`.
|
||||||
|
|
||||||
|
### Child component pattern
|
||||||
|
|
||||||
|
All other components extend `LeafletElement`. To add a new component:
|
||||||
|
|
||||||
|
1. Extend `LeafletElement`, implement `createLeafletObject()`.
|
||||||
|
2. Declare `static get observedAttributes()` listing HTML attributes (kebab-case).
|
||||||
|
3. Override `updateLeafletObject()` only if the default setter-based update won't work (common for coordinate pairs).
|
||||||
|
4. Call `customElements.define('leaflet-foo', LeafletFoo)` at the bottom.
|
||||||
|
5. Export from `src/index.ts`.
|
||||||
|
|
||||||
|
### Special cases
|
||||||
|
|
||||||
|
- **`leaflet-polygon`** uses `<leaflet-line>` children for vertices. The polygon collects lat/lng from child `leaflet-line` elements rather than having them as direct attributes.
|
||||||
|
- **`leaflet-popup`** / **`leaflet-tooltip`**: content comes from `innerHTML`, not attributes. `leaflet-popup` watches for DOM mutations to keep Leaflet in sync.
|
||||||
|
- **`leaflet-layer-group`**: a passthrough container; children register themselves into it via the standard bubble mechanism.
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
`tsc` compiles `src/` → `dist/` with `.js` files and `.d.ts` declarations. Imports within the source use `.js` extensions (required for ESM `"moduleResolution": "bundler"`).
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tsPlugin,
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...tsPlugin.configs.recommended.rules,
|
||||||
|
...prettierConfig.rules,
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Leaflet Components Demo</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
leaflet-map {
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Leaflet Web Components</h1>
|
||||||
|
|
||||||
|
<leaflet-map lat="51.505" lng="-0.09" zoom="13">
|
||||||
|
<leaflet-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
attribution="© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors"
|
||||||
|
>
|
||||||
|
</leaflet-tile-layer>
|
||||||
|
|
||||||
|
<leaflet-marker lat="51.505" lng="-0.09" title="A Marker">
|
||||||
|
<leaflet-popup><b>Hello world!</b><br />I am a popup.</leaflet-popup>
|
||||||
|
<leaflet-tooltip>I am a tooltip</leaflet-tooltip>
|
||||||
|
</leaflet-marker>
|
||||||
|
|
||||||
|
<leaflet-circle
|
||||||
|
lat="51.508"
|
||||||
|
lng="-0.11"
|
||||||
|
color="red"
|
||||||
|
fill-color="#f03"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
radius="500"
|
||||||
|
>
|
||||||
|
<leaflet-popup>I am a circle.</leaflet-popup>
|
||||||
|
</leaflet-circle>
|
||||||
|
|
||||||
|
<leaflet-polygon color="blue" fill-color="cyan" fill-opacity="0.3">
|
||||||
|
<leaflet-line lat="51.509" lng="-0.08"></leaflet-line>
|
||||||
|
<leaflet-line lat="51.503" lng="-0.06"></leaflet-line>
|
||||||
|
<leaflet-line lat="51.51" lng="-0.047"></leaflet-line>
|
||||||
|
<leaflet-popup>I am a polygon.</leaflet-popup>
|
||||||
|
</leaflet-polygon>
|
||||||
|
|
||||||
|
<leaflet-feature-group>
|
||||||
|
<leaflet-marker lat="51.513" lng="-0.07" title="Feature Group Marker">
|
||||||
|
<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">
|
||||||
|
</leaflet-circle>
|
||||||
|
</leaflet-feature-group>
|
||||||
|
|
||||||
|
<leaflet-geojson
|
||||||
|
color="#e67e22"
|
||||||
|
weight="3"
|
||||||
|
fill-color="#f39c12"
|
||||||
|
fill-opacity="0.4"
|
||||||
|
data='{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-0.13,51.50],[-0.12,51.50],[-0.12,51.51],[-0.13,51.51],[-0.13,51.50]]]},"properties":{}}'
|
||||||
|
></leaflet-geojson>
|
||||||
|
|
||||||
|
<leaflet-control-scale position="bottomleft"></leaflet-control-scale>
|
||||||
|
</leaflet-map>
|
||||||
|
|
||||||
|
<!-- script type="module" src="./src/index.ts"></script -->
|
||||||
|
<script type="module" src="/dist/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "@buddy/leaflet-components",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"exports": "./src/index.ts"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "leaflet-components",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "LeafletJS as Web Components",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf dist && tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --loader:.css=text --outfile=dist/index.js",
|
||||||
|
"lint": "eslint 'src/**/*.{ts,js}'",
|
||||||
|
"format": "prettier --write 'src/**/*.{ts,js,json,md}'",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"leaflet",
|
||||||
|
"web-components",
|
||||||
|
"maps"
|
||||||
|
],
|
||||||
|
"author": "buddy",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/leaflet": "1.9.21",
|
||||||
|
"leaflet": "1.9.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "8.60.1",
|
||||||
|
"@typescript-eslint/parser": "8.60.1",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
|
"eslint": "10.4.1",
|
||||||
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-prettier": "5.5.6",
|
||||||
|
"prettier": "3.8.3",
|
||||||
|
"typescript": "6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { Layer, CircleMarker } from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletCircleMarker extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'lat',
|
||||||
|
'lng',
|
||||||
|
'radius',
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fill-color',
|
||||||
|
'fill-opacity',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
return new CircleMarker([lat, lng], this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof CircleMarker) {
|
||||||
|
if (property === 'lat' || property === 'lng') {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
this.leafletObject.setLatLng([lat, lng]);
|
||||||
|
} else if (
|
||||||
|
[
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fillColor',
|
||||||
|
'fillOpacity',
|
||||||
|
'stroke',
|
||||||
|
'dashArray',
|
||||||
|
'dashOffset',
|
||||||
|
'lineCap',
|
||||||
|
'lineJoin',
|
||||||
|
].includes(property)
|
||||||
|
) {
|
||||||
|
this.leafletObject.setStyle({ [property]: value });
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-circle-marker', LeafletCircleMarker);
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { Circle, Layer } from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletCircle extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'lat',
|
||||||
|
'lng',
|
||||||
|
'radius',
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fill-color',
|
||||||
|
'fill-opacity',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
return new Circle([lat, lng], this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof Circle) {
|
||||||
|
if (property === 'lat' || property === 'lng') {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
this.leafletObject.setLatLng([lat, lng]);
|
||||||
|
} else if (
|
||||||
|
[
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fillColor',
|
||||||
|
'fillOpacity',
|
||||||
|
'stroke',
|
||||||
|
'dashArray',
|
||||||
|
'dashOffset',
|
||||||
|
'lineCap',
|
||||||
|
'lineJoin',
|
||||||
|
].includes(property)
|
||||||
|
) {
|
||||||
|
this.leafletObject.setStyle({ [property]: value });
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-circle', LeafletCircle);
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { Control, ControlPosition } from 'leaflet';
|
||||||
|
import { LeafletControl } from '../core/LeafletControl.js';
|
||||||
|
|
||||||
|
export class LeafletControlAttribution extends LeafletControl {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['position', 'prefix'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createControl(): Control {
|
||||||
|
const options: Control.AttributionOptions = {};
|
||||||
|
const position = this.getAttribute('position') as ControlPosition | null;
|
||||||
|
const prefix = this.getAttribute('prefix');
|
||||||
|
if (position) options.position = position;
|
||||||
|
if (prefix !== null) options.prefix = prefix;
|
||||||
|
return new Control.Attribution(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-control-attribution', LeafletControlAttribution);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Control, ControlPosition } from 'leaflet';
|
||||||
|
import { LeafletControl } from '../core/LeafletControl.js';
|
||||||
|
|
||||||
|
export class LeafletControlScale extends LeafletControl {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['position', 'max-width', 'metric', 'imperial', 'update-when-idle'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createControl(): Control {
|
||||||
|
const options: Control.ScaleOptions = {};
|
||||||
|
const position = this.getAttribute('position') as ControlPosition | null;
|
||||||
|
const maxWidth = this.getAttribute('max-width');
|
||||||
|
const metric = this.getAttribute('metric');
|
||||||
|
const imperial = this.getAttribute('imperial');
|
||||||
|
const updateWhenIdle = this.getAttribute('update-when-idle');
|
||||||
|
if (position) options.position = position;
|
||||||
|
if (maxWidth !== null) options.maxWidth = parseInt(maxWidth, 10);
|
||||||
|
if (metric !== null) options.metric = metric !== 'false';
|
||||||
|
if (imperial !== null) options.imperial = imperial !== 'false';
|
||||||
|
if (updateWhenIdle !== null) options.updateWhenIdle = updateWhenIdle !== 'false';
|
||||||
|
return new Control.Scale(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-control-scale', LeafletControlScale);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Control, ControlPosition } from 'leaflet';
|
||||||
|
import { LeafletControl } from '../core/LeafletControl.js';
|
||||||
|
|
||||||
|
export class LeafletControlZoom extends LeafletControl {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['position', 'zoom-in-text', 'zoom-in-title', 'zoom-out-text', 'zoom-out-title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createControl(): Control {
|
||||||
|
const options: Control.ZoomOptions = {};
|
||||||
|
const position = this.getAttribute('position') as ControlPosition | null;
|
||||||
|
const zoomInText = this.getAttribute('zoom-in-text');
|
||||||
|
const zoomInTitle = this.getAttribute('zoom-in-title');
|
||||||
|
const zoomOutText = this.getAttribute('zoom-out-text');
|
||||||
|
const zoomOutTitle = this.getAttribute('zoom-out-title');
|
||||||
|
if (position) options.position = position;
|
||||||
|
if (zoomInText !== null) options.zoomInText = zoomInText;
|
||||||
|
if (zoomInTitle !== null) options.zoomInTitle = zoomInTitle;
|
||||||
|
if (zoomOutText !== null) options.zoomOutText = zoomOutText;
|
||||||
|
if (zoomOutTitle !== null) options.zoomOutTitle = zoomOutTitle;
|
||||||
|
return new Control.Zoom(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-control-zoom', LeafletControlZoom);
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { FeatureGroup, Layer } from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletFeatureGroup extends LeafletElement {
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
return new FeatureGroup([], this.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-feature-group', LeafletFeatureGroup);
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { GeoJSON, PathOptions, Layer } from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletGeoJSON extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'data',
|
||||||
|
'stroke',
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'line-cap',
|
||||||
|
'line-join',
|
||||||
|
'dash-array',
|
||||||
|
'dash-offset',
|
||||||
|
'fill',
|
||||||
|
'fill-color',
|
||||||
|
'fill-opacity',
|
||||||
|
'fill-rule',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
const raw = this.getAttribute('data');
|
||||||
|
const data = raw ? JSON.parse(raw) : undefined;
|
||||||
|
const styleOpts = Object.fromEntries(
|
||||||
|
Object.entries(this.options).filter(([k]) => k !== 'data'),
|
||||||
|
);
|
||||||
|
return new GeoJSON(data, { style: styleOpts as PathOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (!(this.leafletObject instanceof GeoJSON)) return;
|
||||||
|
if (property === 'data') {
|
||||||
|
this.leafletObject.clearLayers();
|
||||||
|
if (value) {
|
||||||
|
this.leafletObject.addData(value as Parameters<GeoJSON['addData']>[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.leafletObject.setStyle({ [property]: value } as PathOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-geojson', LeafletGeoJSON);
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
ImageOverlay,
|
||||||
|
LatLngBounds,
|
||||||
|
LatLngBoundsExpression,
|
||||||
|
LatLngExpression,
|
||||||
|
Layer,
|
||||||
|
} from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletImageOverlay extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'url',
|
||||||
|
'bounds',
|
||||||
|
'opacity',
|
||||||
|
'alt',
|
||||||
|
'interactive',
|
||||||
|
'cross-origin',
|
||||||
|
'error-overlay-url',
|
||||||
|
'z-index',
|
||||||
|
'className',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
const url = this.getAttribute('url') || '';
|
||||||
|
const bounds = this.options.bounds as LatLngBoundsExpression;
|
||||||
|
return new ImageOverlay(url, bounds, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof ImageOverlay) {
|
||||||
|
if (property === 'url') {
|
||||||
|
this.leafletObject.setUrl(value as string);
|
||||||
|
} else if (property === 'bounds') {
|
||||||
|
this.leafletObject.setBounds(new LatLngBounds(value as LatLngExpression[]));
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-image-overlay', LeafletImageOverlay);
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { LayerGroup, Layer } from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletLayerGroup extends LeafletElement {
|
||||||
|
protected createLeafletObject(): Layer {
|
||||||
|
return new LayerGroup([], this.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-layer-group', LeafletLayerGroup);
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
export class LeafletLine extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['lat', 'lng'];
|
||||||
|
}
|
||||||
|
|
||||||
|
get latlng(): [number, number] {
|
||||||
|
return [
|
||||||
|
parseFloat(this.getAttribute('lat') || '0'),
|
||||||
|
parseFloat(this.getAttribute('lng') || '0'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback() {
|
||||||
|
this.dispatchEvent(new CustomEvent('line-updated', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-line', LeafletLine);
|
||||||
@ -0,0 +1,329 @@
|
|||||||
|
import { Map as LMap, MapOptions } from 'leaflet';
|
||||||
|
import leafletCSS from 'leaflet/dist/leaflet.css';
|
||||||
|
import { LeafletRegisterEvent } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
// ── Prop types ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type NumProp = {
|
||||||
|
kind: 'num';
|
||||||
|
attr: string;
|
||||||
|
default: number;
|
||||||
|
viewState?: true;
|
||||||
|
mapGet?: (m: LMap) => number | undefined;
|
||||||
|
mapSet?: (m: LMap, v: number) => void;
|
||||||
|
event?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoolOffProp = {
|
||||||
|
kind: 'bool-off';
|
||||||
|
attr: string;
|
||||||
|
mapSet?: (m: LMap, enabled: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoolOnProp = {
|
||||||
|
kind: 'bool-on';
|
||||||
|
attr: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PropDef = NumProp | BoolOffProp | BoolOnProp;
|
||||||
|
|
||||||
|
// ── Property table ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
// View state — excluded from #buildOptions, initialised via setView()
|
||||||
|
lat: {
|
||||||
|
kind: 'num',
|
||||||
|
attr: 'lat',
|
||||||
|
default: 0,
|
||||||
|
viewState: true,
|
||||||
|
event: 'moveend',
|
||||||
|
mapGet: (m: LMap) => m.getCenter()?.lat,
|
||||||
|
mapSet: (m: LMap, v: number) => m.setView([v, m.getCenter()?.lng ?? 0], m.getZoom()),
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
kind: 'num',
|
||||||
|
attr: 'lng',
|
||||||
|
default: 0,
|
||||||
|
viewState: true,
|
||||||
|
event: 'moveend',
|
||||||
|
mapGet: (m: LMap) => m.getCenter()?.lng,
|
||||||
|
mapSet: (m: LMap, v: number) => m.setView([m.getCenter()?.lat ?? 0, v], m.getZoom()),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
kind: 'num',
|
||||||
|
attr: 'zoom',
|
||||||
|
default: 2,
|
||||||
|
viewState: true,
|
||||||
|
event: 'zoomend',
|
||||||
|
mapGet: (m: LMap) => m.getZoom(),
|
||||||
|
mapSet: (m: LMap, v: number) => m.setZoom(v),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Live numeric options — have Leaflet setters
|
||||||
|
minZoom: {
|
||||||
|
kind: 'num',
|
||||||
|
attr: 'min-zoom',
|
||||||
|
default: 0,
|
||||||
|
mapGet: (m: LMap) => m.getMinZoom(),
|
||||||
|
mapSet: (m: LMap, v: number) => m.setMinZoom(v),
|
||||||
|
},
|
||||||
|
maxZoom: {
|
||||||
|
kind: 'num',
|
||||||
|
attr: 'max-zoom',
|
||||||
|
default: Infinity,
|
||||||
|
mapGet: (m: LMap) => m.getMaxZoom(),
|
||||||
|
mapSet: (m: LMap, v: number) => m.setMaxZoom(v),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Constructor-only numeric options
|
||||||
|
zoomSnap: { kind: 'num', attr: 'zoom-snap', default: 1 },
|
||||||
|
zoomDelta: { kind: 'num', attr: 'zoom-delta', default: 1 },
|
||||||
|
keyboardPanDelta: { kind: 'num', attr: 'keyboard-pan-delta', default: 80 },
|
||||||
|
wheelDebounceTime: { kind: 'num', attr: 'wheel-debounce-time', default: 40 },
|
||||||
|
wheelPxPerZoomLevel: { kind: 'num', attr: 'wheel-px-per-zoom-level', default: 60 },
|
||||||
|
inertiaDeceleration: { kind: 'num', attr: 'inertia-deceleration', default: 3000 },
|
||||||
|
inertiaMaxSpeed: { kind: 'num', attr: 'inertia-max-speed', default: Infinity },
|
||||||
|
easeLinearity: { kind: 'num', attr: 'ease-linearity', default: 0.2 },
|
||||||
|
maxBoundsViscosity: { kind: 'num', attr: 'max-bounds-viscosity', default: 0 },
|
||||||
|
tapTolerance: { kind: 'num', attr: 'tap-tolerance', default: 15 },
|
||||||
|
zoomAnimationThreshold: { kind: 'num', attr: 'zoom-animation-threshold', default: 4 },
|
||||||
|
transform3DLimit: { kind: 'num', attr: 'transform-3d-limit', default: 8388608 },
|
||||||
|
|
||||||
|
// Boolean defaults-true → disable-* attribute; handler-based options have live mapSet
|
||||||
|
scrollWheelZoom: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-scroll-wheel-zoom',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.scrollWheelZoom.enable() : m.scrollWheelZoom.disable()),
|
||||||
|
},
|
||||||
|
dragging: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-dragging',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.dragging.enable() : m.dragging.disable()),
|
||||||
|
},
|
||||||
|
touchZoom: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-touch-zoom',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.touchZoom.enable() : m.touchZoom.disable()),
|
||||||
|
},
|
||||||
|
doubleClickZoom: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-double-click-zoom',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.doubleClickZoom.enable() : m.doubleClickZoom.disable()),
|
||||||
|
},
|
||||||
|
boxZoom: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-box-zoom',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.boxZoom.enable() : m.boxZoom.disable()),
|
||||||
|
},
|
||||||
|
keyboard: {
|
||||||
|
kind: 'bool-off',
|
||||||
|
attr: 'disable-keyboard',
|
||||||
|
mapSet: (m: LMap, v: boolean) => (v ? m.keyboard.enable() : m.keyboard.disable()),
|
||||||
|
},
|
||||||
|
closePopupOnClick: { kind: 'bool-off', attr: 'disable-close-popup-on-click' },
|
||||||
|
trackResize: { kind: 'bool-off', attr: 'disable-track-resize' },
|
||||||
|
zoomControl: { kind: 'bool-off', attr: 'disable-zoom-control' },
|
||||||
|
attributionControl: { kind: 'bool-off', attr: 'disable-attribution-control' },
|
||||||
|
inertia: { kind: 'bool-off', attr: 'disable-inertia' },
|
||||||
|
zoomAnimation: { kind: 'bool-off', attr: 'disable-zoom-animation' },
|
||||||
|
fadeAnimation: { kind: 'bool-off', attr: 'disable-fade-animation' },
|
||||||
|
markerZoomAnimation: { kind: 'bool-off', attr: 'disable-marker-zoom-animation' },
|
||||||
|
bounceAtZoomLimits: { kind: 'bool-off', attr: 'disable-bounce-at-zoom-limits' },
|
||||||
|
tapHold: { kind: 'bool-off', attr: 'disable-tap-hold' },
|
||||||
|
|
||||||
|
// Boolean defaults-false → normal attribute
|
||||||
|
preferCanvas: { kind: 'bool-on', attr: 'prefer-canvas' },
|
||||||
|
worldCopyJump: { kind: 'bool-on', attr: 'world-copy-jump' },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropName = keyof typeof PROPS;
|
||||||
|
|
||||||
|
const ATTR_TO_PROP = new globalThis.Map<string, PropName>(
|
||||||
|
(Object.entries(PROPS) as [PropName, PropDef][]).map(([name, spec]) => [spec.attr, name]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Element ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Derive property types directly from the PROPS table so adding a prop to the
|
||||||
|
// table automatically makes it part of the LeafletMap instance type.
|
||||||
|
type PropTypes = {
|
||||||
|
[K in keyof typeof PROPS]: (typeof PROPS)[K] extends { kind: 'num' } ? number : boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cast HTMLElement to a typed base whose instances include PropTypes. This is
|
||||||
|
// the mixin pattern — TypeScript sees LeafletMap as having all prop types, the
|
||||||
|
// runtime still extends HTMLElement, and no interface merge is required.
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletMap extends TypedBase {
|
||||||
|
#map?: LMap;
|
||||||
|
#container!: HTMLDivElement;
|
||||||
|
#syncing = false;
|
||||||
|
#mapEventHandlers = new globalThis.Map<string, () => void>();
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a getter/setter on the prototype for every entry in PROPS.
|
||||||
|
// The functions are defined inside the class body so they can access private fields.
|
||||||
|
static {
|
||||||
|
for (const [propName, spec] of Object.entries(PROPS) as [PropName, PropDef][]) {
|
||||||
|
if (spec.kind === 'num') {
|
||||||
|
Object.defineProperty(LeafletMap.prototype, propName, {
|
||||||
|
get(this: LeafletMap) {
|
||||||
|
const val = spec.mapGet && this.#map ? spec.mapGet(this.#map) : undefined;
|
||||||
|
return val !== undefined ? val : Number(this.getAttribute(spec.attr) ?? spec.default);
|
||||||
|
},
|
||||||
|
set(this: LeafletMap, v: number) {
|
||||||
|
if (spec.event) {
|
||||||
|
this.#syncAttr(spec.attr, String(v));
|
||||||
|
} else {
|
||||||
|
this.setAttribute(spec.attr, String(v));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
} else if (spec.kind === 'bool-off') {
|
||||||
|
Object.defineProperty(LeafletMap.prototype, propName, {
|
||||||
|
get(this: LeafletMap) {
|
||||||
|
return !this.hasAttribute(spec.attr);
|
||||||
|
},
|
||||||
|
set(this: LeafletMap, v: boolean) {
|
||||||
|
this.toggleAttribute(spec.attr, !v);
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.defineProperty(LeafletMap.prototype, propName, {
|
||||||
|
get(this: LeafletMap) {
|
||||||
|
return this.hasAttribute(spec.attr);
|
||||||
|
},
|
||||||
|
set(this: LeafletMap, v: boolean) {
|
||||||
|
this.toggleAttribute(spec.attr, v);
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.#container = document.createElement('div');
|
||||||
|
this.#container.style.width = '100%';
|
||||||
|
this.#container.style.height = '100%';
|
||||||
|
this.shadowRoot!.appendChild(this.#container);
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `:host { display: block; width: 100%; height: 400px; }\n${leafletCSS}`;
|
||||||
|
this.shadowRoot!.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
// Read view-state from attributes directly — the getters call getCenter()/getZoom()
|
||||||
|
// which throw if invoked before setView(), so we can't use them here.
|
||||||
|
const lat = Number(this.getAttribute('lat') ?? 0);
|
||||||
|
const lng = Number(this.getAttribute('lng') ?? 0);
|
||||||
|
const zoom = Number(this.getAttribute('zoom') ?? 2);
|
||||||
|
|
||||||
|
// Assign #map only after setView so the getters' `this.#map` guard is
|
||||||
|
// equivalent to "map is ready" — getCenter()/getZoom() throw before setView.
|
||||||
|
const map = new LMap(this.#container, this.#buildOptions());
|
||||||
|
map.setView([lat, lng], zoom);
|
||||||
|
this.#map = map;
|
||||||
|
|
||||||
|
// Register one handler per unique map event, updating all props that share it
|
||||||
|
const groups = new globalThis.Map<string, NumProp[]>();
|
||||||
|
for (const spec of Object.values(PROPS) as PropDef[]) {
|
||||||
|
if (spec.kind === 'num' && spec.event && spec.mapGet) {
|
||||||
|
const g = groups.get(spec.event) ?? [];
|
||||||
|
g.push(spec);
|
||||||
|
groups.set(spec.event, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [event, specs] of groups) {
|
||||||
|
const handler = () => {
|
||||||
|
for (const s of specs) {
|
||||||
|
if (this.#map && s.mapGet) {
|
||||||
|
const v = s.mapGet(this.#map);
|
||||||
|
if (v !== undefined) this.#syncAttr(s.attr, String(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.#mapEventHandlers.set(event, handler);
|
||||||
|
this.#map.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventListener(
|
||||||
|
'leaflet-register',
|
||||||
|
this.#handleLeafletRegister as EventListenerOrEventListenerObject,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeEventListener(
|
||||||
|
'leaflet-register',
|
||||||
|
this.#handleLeafletRegister as EventListenerOrEventListenerObject,
|
||||||
|
);
|
||||||
|
if (!this.#map) return;
|
||||||
|
for (const [event, handler] of this.#mapEventHandlers) {
|
||||||
|
this.#map.off(event, handler);
|
||||||
|
}
|
||||||
|
this.#mapEventHandlers.clear();
|
||||||
|
this.#map.remove();
|
||||||
|
this.#map = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
||||||
|
if (oldValue === newValue || !this.#map || this.#syncing) return;
|
||||||
|
const propName = ATTR_TO_PROP.get(name);
|
||||||
|
if (!propName) return;
|
||||||
|
const spec = PROPS[propName] as PropDef;
|
||||||
|
if (spec.kind === 'num') {
|
||||||
|
if (spec.mapSet && newValue !== null) spec.mapSet(this.#map, Number(newValue));
|
||||||
|
} else if (spec.kind === 'bool-off') {
|
||||||
|
spec.mapSet?.(this.#map, newValue === null);
|
||||||
|
}
|
||||||
|
// bool-on: constructor-only, no live update
|
||||||
|
}
|
||||||
|
|
||||||
|
#syncAttr(name: string, value: string) {
|
||||||
|
if (this.#syncing || this.getAttribute(name) === value) return;
|
||||||
|
this.#syncing = true;
|
||||||
|
this.setAttribute(name, value);
|
||||||
|
this.#syncing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildOptions(): MapOptions {
|
||||||
|
const o: Record<string, unknown> = {};
|
||||||
|
for (const [propName, spec] of Object.entries(PROPS) as [PropName, PropDef][]) {
|
||||||
|
if (spec.kind === 'num') {
|
||||||
|
if (spec.viewState) continue;
|
||||||
|
const v = this.getAttribute(spec.attr);
|
||||||
|
if (v !== null) o[propName] = Number(v);
|
||||||
|
} else if (spec.kind === 'bool-off') {
|
||||||
|
if (this.hasAttribute(spec.attr)) o[propName] = false;
|
||||||
|
} else {
|
||||||
|
if (this.hasAttribute(spec.attr)) o[propName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o as MapOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleLeafletRegister = (e: LeafletRegisterEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.#map && e.detail.leafletObject) {
|
||||||
|
e.detail.leafletObject.addTo(this.#map);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get map(): LMap | undefined {
|
||||||
|
return this.#map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-map', LeafletMap);
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletMarker extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['lat', 'lng', 'title', 'alt', 'draggable', 'opacity', 'z-index-offset'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
return new L.Marker([lat, lng], this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Marker) {
|
||||||
|
if (property === 'lat' || property === 'lng') {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
this.leafletObject.setLatLng([lat, lng]);
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-marker', LeafletMarker);
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
import { LeafletLine } from './leaflet-line.js';
|
||||||
|
|
||||||
|
export class LeafletPolygon extends LeafletElement {
|
||||||
|
private _observer?: MutationObserver;
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const coords = this.getCoords();
|
||||||
|
return new L.Polygon(coords, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCoords(): [number, number][] {
|
||||||
|
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||||
|
return lines.map((line) => line.latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener('line-updated', () => {
|
||||||
|
if (this.leafletObject instanceof L.Polygon) {
|
||||||
|
this.leafletObject.setLatLngs(this.getCoords());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._observer = new MutationObserver(() => {
|
||||||
|
if (this.leafletObject instanceof L.Polygon) {
|
||||||
|
this.leafletObject.setLatLngs(this.getCoords());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._observer.observe(this, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._observer?.disconnect();
|
||||||
|
this._observer = undefined;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Polygon) {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fillColor',
|
||||||
|
'fillOpacity',
|
||||||
|
'stroke',
|
||||||
|
'dashArray',
|
||||||
|
'dashOffset',
|
||||||
|
'lineCap',
|
||||||
|
'lineJoin',
|
||||||
|
].includes(property)
|
||||||
|
) {
|
||||||
|
this.leafletObject.setStyle({ [property]: value });
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-polygon', LeafletPolygon);
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
import { LeafletLine } from './leaflet-line.js';
|
||||||
|
|
||||||
|
export class LeafletPolyline extends LeafletElement {
|
||||||
|
private _observer?: MutationObserver;
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const coords = this.getCoords();
|
||||||
|
return new L.Polyline(coords, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCoords(): [number, number][] {
|
||||||
|
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||||
|
return lines.map((line) => line.latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener('line-updated', () => {
|
||||||
|
if (this.leafletObject instanceof L.Polyline) {
|
||||||
|
this.leafletObject.setLatLngs(this.getCoords());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._observer = new MutationObserver(() => {
|
||||||
|
if (this.leafletObject instanceof L.Polyline) {
|
||||||
|
this.leafletObject.setLatLngs(this.getCoords());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._observer.observe(this, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._observer?.disconnect();
|
||||||
|
this._observer = undefined;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Polyline) {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fillColor',
|
||||||
|
'fillOpacity',
|
||||||
|
'stroke',
|
||||||
|
'dashArray',
|
||||||
|
'dashOffset',
|
||||||
|
'lineCap',
|
||||||
|
'lineJoin',
|
||||||
|
].includes(property)
|
||||||
|
) {
|
||||||
|
this.leafletObject.setStyle({ [property]: value });
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-polyline', LeafletPolyline);
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletPopup extends LeafletElement {
|
||||||
|
private _observer?: MutationObserver;
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'lat',
|
||||||
|
'lng',
|
||||||
|
'max-width',
|
||||||
|
'min-width',
|
||||||
|
'max-height',
|
||||||
|
'auto-pan',
|
||||||
|
'close-button',
|
||||||
|
'auto-close',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const lat = this.getAttribute('lat');
|
||||||
|
const lng = this.getAttribute('lng');
|
||||||
|
const options = { ...this.options, content: this.innerHTML };
|
||||||
|
const popup = new L.Popup(options);
|
||||||
|
if (lat && lng) {
|
||||||
|
popup.setLatLng([parseFloat(lat), parseFloat(lng)]);
|
||||||
|
}
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._observer = new MutationObserver(() => {
|
||||||
|
if (this.leafletObject instanceof L.Popup) {
|
||||||
|
this.leafletObject.setContent(this.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._observer.observe(this, { childList: true, characterData: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._observer?.disconnect();
|
||||||
|
this._observer = undefined;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Popup) {
|
||||||
|
if (property === 'lat' || property === 'lng') {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
this.leafletObject.setLatLng([lat, lng]);
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-popup', LeafletPopup);
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletRectangle extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['bounds', 'color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const bounds = this.options.bounds as L.LatLngBoundsExpression;
|
||||||
|
return new L.Rectangle(bounds, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Rectangle) {
|
||||||
|
if (property === 'bounds') {
|
||||||
|
this.leafletObject.setBounds(value as L.LatLngBoundsExpression);
|
||||||
|
} else if (
|
||||||
|
[
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fillColor',
|
||||||
|
'fillOpacity',
|
||||||
|
'stroke',
|
||||||
|
'dashArray',
|
||||||
|
'dashOffset',
|
||||||
|
'lineCap',
|
||||||
|
'lineJoin',
|
||||||
|
].includes(property)
|
||||||
|
) {
|
||||||
|
this.leafletObject.setStyle({ [property]: value });
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-rectangle', LeafletRectangle);
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletSVGOverlay extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['bounds', 'opacity', 'alt', 'interactive', 'cross-origin', 'z-index', 'className'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const svg = this.querySelector('svg');
|
||||||
|
const bounds = this.options.bounds as L.LatLngBoundsExpression;
|
||||||
|
if (!svg) {
|
||||||
|
// Create a dummy SVG if none provided
|
||||||
|
const dummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
return new L.SVGOverlay(dummy, bounds, this.options);
|
||||||
|
}
|
||||||
|
return new L.SVGOverlay(svg, bounds, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.SVGOverlay) {
|
||||||
|
if (property === 'bounds') {
|
||||||
|
this.leafletObject.setBounds(new L.LatLngBounds(value as L.LatLngExpression[]));
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-svg-overlay', LeafletSVGOverlay);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletTileLayerWMS extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['url', 'layers', 'styles', 'format', 'transparent', 'version', 'crs', 'uppercase'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const url = this.getAttribute('url') || '';
|
||||||
|
return new L.TileLayer.WMS(url, this.options as L.WMSOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.TileLayer.WMS) {
|
||||||
|
if (property === 'url') {
|
||||||
|
this.leafletObject.setUrl(value as string);
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-tile-layer-wms', LeafletTileLayerWMS);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletTileLayer extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['url', 'attribution', 'min-zoom', 'max-zoom', 'opacity', 'z-index'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const url = this.getAttribute('url') || '';
|
||||||
|
return new L.TileLayer(url, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.TileLayer) {
|
||||||
|
if (property === 'url') {
|
||||||
|
this.leafletObject.setUrl(value as string);
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-tile-layer', LeafletTileLayer);
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletTooltip extends LeafletElement {
|
||||||
|
private _observer?: MutationObserver;
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['lat', 'lng', 'pane', 'offset', 'direction', 'permanent', 'sticky', 'opacity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const lat = this.getAttribute('lat');
|
||||||
|
const lng = this.getAttribute('lng');
|
||||||
|
const options = { ...this.options, content: this.innerHTML };
|
||||||
|
const tooltip = new L.Tooltip(options);
|
||||||
|
if (lat && lng) {
|
||||||
|
tooltip.setLatLng([parseFloat(lat), parseFloat(lng)]);
|
||||||
|
}
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._observer = new MutationObserver(() => {
|
||||||
|
if (this.leafletObject instanceof L.Tooltip) {
|
||||||
|
this.leafletObject.setContent(this.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._observer.observe(this, { childList: true, characterData: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._observer?.disconnect();
|
||||||
|
this._observer = undefined;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.Tooltip) {
|
||||||
|
if (property === 'lat' || property === 'lng') {
|
||||||
|
const lat = parseFloat(this.getAttribute('lat') || '0');
|
||||||
|
const lng = parseFloat(this.getAttribute('lng') || '0');
|
||||||
|
this.leafletObject.setLatLng([lat, lng]);
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-tooltip', LeafletTooltip);
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { LeafletElement } from '../core/LeafletElement.js';
|
||||||
|
|
||||||
|
export class LeafletVideoOverlay extends LeafletElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
'url',
|
||||||
|
'bounds',
|
||||||
|
'opacity',
|
||||||
|
'alt',
|
||||||
|
'interactive',
|
||||||
|
'cross-origin',
|
||||||
|
'loop',
|
||||||
|
'autoplay',
|
||||||
|
'muted',
|
||||||
|
'playsinline',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createLeafletObject(): L.Layer {
|
||||||
|
const url = this.getAttribute('url') || '';
|
||||||
|
const bounds = this.options.bounds as L.LatLngBoundsExpression;
|
||||||
|
return new L.VideoOverlay(url, bounds, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
if (this.leafletObject instanceof L.VideoOverlay) {
|
||||||
|
if (property === 'url') {
|
||||||
|
this.leafletObject.setUrl(value as string);
|
||||||
|
} else if (property === 'bounds') {
|
||||||
|
this.leafletObject.setBounds(new L.LatLngBounds(value as L.LatLngExpression[]));
|
||||||
|
} else {
|
||||||
|
super.updateLeafletObject(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement(): HTMLVideoElement | undefined {
|
||||||
|
return this.leafletObject instanceof L.VideoOverlay
|
||||||
|
? this.leafletObject.getElement()
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('leaflet-video-overlay', LeafletVideoOverlay);
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { Control } from 'leaflet';
|
||||||
|
|
||||||
|
export abstract class LeafletControl extends HTMLElement {
|
||||||
|
protected control?: Control;
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.control = this.createControl();
|
||||||
|
this.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.control?.remove();
|
||||||
|
this.control = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract createControl(): Control;
|
||||||
|
|
||||||
|
protected register() {
|
||||||
|
if (this.control) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('leaflet-register', {
|
||||||
|
detail: {
|
||||||
|
leafletObject: this.control,
|
||||||
|
element: this,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { Layer, Control, Popup, Tooltip, LayerGroup } from 'leaflet';
|
||||||
|
|
||||||
|
export interface LeafletRegisterEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
leafletObject: Layer | Control;
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class LeafletElement extends HTMLElement {
|
||||||
|
protected leafletObject?: Layer;
|
||||||
|
protected options: Record<string, unknown> = {};
|
||||||
|
protected _parentLayer?: Layer;
|
||||||
|
protected _parentBindingType?: 'popup' | 'tooltip' | 'layer';
|
||||||
|
private _registerHandler?: EventListener;
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.initOptions();
|
||||||
|
this.leafletObject = this.createLeafletObject();
|
||||||
|
|
||||||
|
this._registerHandler = ((e: LeafletRegisterEvent) => {
|
||||||
|
if (this.leafletObject) {
|
||||||
|
const childObj = e.detail.leafletObject;
|
||||||
|
const childEl = e.detail.element;
|
||||||
|
if (childObj instanceof Popup) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.leafletObject.bindPopup(childObj);
|
||||||
|
if (childEl instanceof LeafletElement) {
|
||||||
|
childEl._parentLayer = this.leafletObject;
|
||||||
|
childEl._parentBindingType = 'popup';
|
||||||
|
}
|
||||||
|
} else if (childObj instanceof Tooltip) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.leafletObject.bindTooltip(childObj);
|
||||||
|
if (childEl instanceof LeafletElement) {
|
||||||
|
childEl._parentLayer = this.leafletObject;
|
||||||
|
childEl._parentBindingType = 'tooltip';
|
||||||
|
}
|
||||||
|
} else if (childObj instanceof Layer && 'addLayer' in this.leafletObject) {
|
||||||
|
e.stopPropagation();
|
||||||
|
(this.leafletObject as LayerGroup).addLayer(childObj);
|
||||||
|
if (childEl instanceof LeafletElement) {
|
||||||
|
childEl._parentLayer = this.leafletObject;
|
||||||
|
childEl._parentBindingType = 'layer';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as EventListener;
|
||||||
|
this.addEventListener('leaflet-register', this._registerHandler);
|
||||||
|
|
||||||
|
this.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this._registerHandler) {
|
||||||
|
this.removeEventListener('leaflet-register', this._registerHandler);
|
||||||
|
this._registerHandler = undefined;
|
||||||
|
}
|
||||||
|
if (!this.leafletObject) return;
|
||||||
|
if (this._parentBindingType === 'popup' && this._parentLayer) {
|
||||||
|
this._parentLayer.unbindPopup();
|
||||||
|
} else if (this._parentBindingType === 'tooltip' && this._parentLayer) {
|
||||||
|
this._parentLayer.unbindTooltip();
|
||||||
|
} else if (
|
||||||
|
this._parentBindingType === 'layer' &&
|
||||||
|
this._parentLayer &&
|
||||||
|
'removeLayer' in this._parentLayer
|
||||||
|
) {
|
||||||
|
(this._parentLayer as LayerGroup).removeLayer(this.leafletObject);
|
||||||
|
} else {
|
||||||
|
this.leafletObject.remove();
|
||||||
|
}
|
||||||
|
this._parentLayer = undefined;
|
||||||
|
this._parentBindingType = undefined;
|
||||||
|
this.leafletObject = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initOptions() {
|
||||||
|
const observed = (this.constructor as typeof LeafletElement).observedAttributes;
|
||||||
|
observed.forEach((attr) => {
|
||||||
|
const val = this.getAttribute(attr);
|
||||||
|
if (val !== null) {
|
||||||
|
this.options[camelCase(attr)] = parseAttributeValue(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||||
|
if (oldValue === newValue) return;
|
||||||
|
const propertyName = camelCase(name);
|
||||||
|
const parsedValue = parseAttributeValue(newValue);
|
||||||
|
this.options[propertyName] = parsedValue;
|
||||||
|
|
||||||
|
if (this.leafletObject) {
|
||||||
|
this.updateLeafletObject(propertyName, parsedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract createLeafletObject(): Layer;
|
||||||
|
|
||||||
|
protected updateLeafletObject(property: string, value: unknown) {
|
||||||
|
const setter = `set${property.charAt(0).toUpperCase()}${property.slice(1)}`;
|
||||||
|
const obj = this.leafletObject as unknown as Record<string, unknown>;
|
||||||
|
if (obj && typeof obj[setter] === 'function') {
|
||||||
|
(obj[setter] as (val: unknown) => void)(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected register() {
|
||||||
|
if (!this.leafletObject) return;
|
||||||
|
const event = new CustomEvent('leaflet-register', {
|
||||||
|
detail: {
|
||||||
|
leafletObject: this.leafletObject,
|
||||||
|
element: this,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
});
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function camelCase(str: string): string {
|
||||||
|
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAttributeValue(value: string): unknown {
|
||||||
|
if (value === 'true') return true;
|
||||||
|
if (value === 'false') return false;
|
||||||
|
const num = Number(value);
|
||||||
|
if (!isNaN(num) && value !== '') return num;
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
export * from './core/LeafletElement.js';
|
||||||
|
export * from './core/LeafletControl.js';
|
||||||
|
export * from './components/leaflet-map.js';
|
||||||
|
export * from './components/leaflet-marker.js';
|
||||||
|
export * from './components/leaflet-circle.js';
|
||||||
|
export * from './components/leaflet-circle-marker.js';
|
||||||
|
export * from './components/leaflet-line.js';
|
||||||
|
export * from './components/leaflet-polygon.js';
|
||||||
|
export * from './components/leaflet-polyline.js';
|
||||||
|
export * from './components/leaflet-rectangle.js';
|
||||||
|
export * from './components/leaflet-tile-layer.js';
|
||||||
|
export * from './components/leaflet-tile-layer-wms.js';
|
||||||
|
export * from './components/leaflet-image-overlay.js';
|
||||||
|
export * from './components/leaflet-video-overlay.js';
|
||||||
|
export * from './components/leaflet-svg-overlay.js';
|
||||||
|
export * from './components/leaflet-layer-group.js';
|
||||||
|
export * from './components/leaflet-feature-group.js';
|
||||||
|
export * from './components/leaflet-geojson.js';
|
||||||
|
export * from './components/leaflet-control-zoom.js';
|
||||||
|
export * from './components/leaflet-control-attribution.js';
|
||||||
|
export * from './components/leaflet-control-scale.js';
|
||||||
|
export * from './components/leaflet-popup.js';
|
||||||
|
export * from './components/leaflet-tooltip.js';
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.css' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue