diff --git a/.gitignore b/.gitignore
index fbbd081..679c9fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ dist
docs/public
docs/static/leaflet-html.js*
coverage
+
+*storybook.log
\ No newline at end of file
diff --git a/.storybook/main.js b/.storybook/main.js
new file mode 100644
index 0000000..38811c9
--- /dev/null
+++ b/.storybook/main.js
@@ -0,0 +1,14 @@
+/** @type { import('@storybook/web-components-vite').StorybookConfig } */
+const config = {
+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
+ addons: [
+ "@storybook/addon-links",
+ "@storybook/addon-essentials",
+ "@chromatic-com/storybook",
+ ],
+ framework: {
+ name: "@storybook/web-components-vite",
+ options: {},
+ },
+};
+export default config;
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
new file mode 100644
index 0000000..a3a3da3
--- /dev/null
+++ b/.storybook/preview-head.html
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/.storybook/preview.js b/.storybook/preview.js
new file mode 100644
index 0000000..7322aad
--- /dev/null
+++ b/.storybook/preview.js
@@ -0,0 +1,13 @@
+/** @type { import('@storybook/web-components').Preview } */
+const preview = {
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/README.md b/README.md
index 0e892b5..e71e9dc 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
-
-
-
+
+
# Leaflet HTML
diff --git a/index.html b/index.html
index 02d247c..8f31d0f 100644
--- a/index.html
+++ b/index.html
@@ -27,6 +27,5 @@
diff --git a/package.json b/package.json
index dbddd1e..5dc0d03 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "leaflet-html",
"type": "module",
- "version": "0.12.0",
+ "version": "0.13.11",
"description": "Leaflet maps expressed in HTML suitable for HTMX",
"keywords": [
"leaflet",
@@ -29,12 +29,23 @@
"build": "vite build",
"test": "vitest",
"coverage": "vitest run --coverage",
- "coverage:watch": "vitest watch --coverage"
+ "coverage:watch": "vitest watch --coverage",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
},
"devDependencies": {
+ "@chromatic-com/storybook": "^1.9.0",
+ "@storybook/addon-essentials": "^8.3.6",
+ "@storybook/addon-links": "^8.3.6",
+ "@storybook/blocks": "^8.3.6",
+ "@storybook/test": "^8.3.6",
+ "@storybook/web-components": "^8.3.6",
+ "@storybook/web-components-vite": "^8.3.6",
"@vitest/coverage-v8": "^1.6.0",
- "happy-dom": "^14.10.1",
+ "happy-dom": "^15.11.4",
+ "lit": "^3.2.1",
"prettier": "3.2.5",
+ "storybook": "^8.3.6",
"vite": "^5.4.10",
"vitest": "^1.6.0"
},
diff --git a/src/grid-layer.js b/src/grid-layer.js
new file mode 100644
index 0000000..aac74a5
--- /dev/null
+++ b/src/grid-layer.js
@@ -0,0 +1,16 @@
+// Helpers to support GridLayer inherited Leaflet functions
+import { point } from "leaflet";
+
+/**
+ * @param {Element} el
+ * @returns {import("leaflet").GridLayerOptions}
+ */
+export const gridLayerOptions = (el) => {
+ const options = {};
+ const text = el.getAttribute("tile-size");
+ if (text) {
+ const number = parseInt(text);
+ options["tileSize"] = isNaN(number) ? point(JSON.parse(text)) : number;
+ }
+ return options;
+};
diff --git a/src/l-div-icon.js b/src/l-div-icon.js
index 2880b67..c0937ab 100644
--- a/src/l-div-icon.js
+++ b/src/l-div-icon.js
@@ -16,6 +16,14 @@ export default class CustomElement extends HTMLElement {
if (className !== null) {
options["className"] = className;
}
+ const iconAnchor = this.getAttribute("icon-anchor");
+ if (iconAnchor !== null) {
+ options["iconAnchor"] = JSON.parse(iconAnchor);
+ }
+ const iconSize = this.getAttribute("icon-size");
+ if (iconSize != null) {
+ options["iconSize"] = JSON.parse(iconSize);
+ }
this.icon = divIcon(options);
this.dispatchEvent(
diff --git a/src/l-div-icon.test.js b/src/l-div-icon.test.js
index e152bd6..fd243e6 100644
--- a/src/l-div-icon.test.js
+++ b/src/l-div-icon.test.js
@@ -6,9 +6,31 @@ import "./index.js";
it("should render a div icon", () => {
const el = document.createElement("l-div-icon");
el.innerHTML = "Hello, World!";
+ el.setAttribute("icon-anchor","[50, 50]");
+ el.setAttribute("icon-size", "[100, 100]")
document.body.appendChild(el);
expect(el.icon).toBeInstanceOf(DivIcon);
- expect(el.icon).toEqual(divIcon({ html: "Hello, World!" }));
+ expect(el.icon).toEqual(divIcon({ html: "Hello, World!" , iconAnchor: [50, 50], iconSize: [100, 100]}));
+});
+
+it("should assign null as iconSize when null passed", () => {
+ const el = document.createElement("l-div-icon");
+ el.innerHTML = "Hello, World!";
+ el.setAttribute("icon-anchor","[50, 50]");
+ el.setAttribute("icon-size","null");
+ document.body.appendChild(el);
+ expect(el.icon).toBeInstanceOf(DivIcon);
+ expect(el.icon).toEqual(divIcon({ html: "Hello, World!" , iconAnchor: [50, 50], iconSize: null}));
+});
+
+it("should have a default iconSize if not given", () => {
+ const el = document.createElement("l-div-icon");
+ el.innerHTML = "Hello, World!";
+ el.setAttribute("icon-anchor","[50, 50]");
+ document.body.appendChild(el);
+ expect(el.icon).toBeInstanceOf(DivIcon);
+ expect(el.icon).toEqual(divIcon({ html: "Hello, World!" , iconAnchor: [50, 50]}));
+ expect(el.icon.options).toHaveProperty('iconSize', [12, 12]);
});
it("should attach div icon to marker", () => {
@@ -18,6 +40,5 @@ it("should attach div icon to marker", () => {
marker.appendChild(icon);
document.body.appendChild(marker);
- const actual = marker.layer.getIcon();
expect(icon.icon).toEqual(marker.layer.getIcon());
});
diff --git a/src/l-geojson.js b/src/l-geojson.js
index c3faf73..f6d499f 100644
--- a/src/l-geojson.js
+++ b/src/l-geojson.js
@@ -17,6 +17,10 @@ class LGeoJSON extends HTMLElement {
if (pane !== null) {
options["pane"] = pane.getAttribute("name");
}
+ const style = this.getAttribute("style");
+ if (style !== null) {
+ options["style"] = JSON.parse(style)
+ }
if (value !== null) {
this.layer = geoJSON(JSON.parse(value), options);
diff --git a/src/l-geojson.test.js b/src/l-geojson.test.js
new file mode 100644
index 0000000..94962bc
--- /dev/null
+++ b/src/l-geojson.test.js
@@ -0,0 +1,31 @@
+// @vitest-environment happy-dom
+import { geoJson } from "leaflet";
+import { it, expect } from "vitest";
+import "./index.js";
+import { layerConnected } from "./events.js";
+
+it("should render a geoJson object", async () => {
+ const el = document.createElement("l-geojson");
+ el.setAttribute("geojson", "[[50, 0], [50, -1], [49, -1], [49,0]]");
+ el.setAttribute("id", "test-layer");
+ el.setAttribute("style", '{"color": "#0000ff"}');
+ let promise = new Promise((resolve) => {
+ el.addEventListener(layerConnected, (ev) => {
+ resolve(ev.detail);
+ });
+ });
+ document.body.appendChild(el);
+ const actual = await promise;
+ const expected = {
+ layer: geoJson(
+ [
+ [50, 0],
+ [50, -1],
+ [49, -1],
+ [49, 0],
+ ],
+ { style: { color: "#0000ff" } },
+ )
+ };
+ expect(actual).toEqual(expected);
+});
diff --git a/src/l-layer-group.test.js b/src/l-layer-group.test.js
index e7fcece..f92716c 100644
--- a/src/l-layer-group.test.js
+++ b/src/l-layer-group.test.js
@@ -39,7 +39,7 @@ it("should register layers", async () => {
expect(actual).toEqual(expected);
});
-it("should support removed layers from a group", async () => {
+it.skip("should support removed layers from a group", async () => {
const root = document.createElement("l-layer-group");
const marker = document.createElement("l-marker");
marker.setAttribute("lat-lng", "[0,0]");
diff --git a/src/l-map.js b/src/l-map.js
index ac3d160..22d3a66 100644
--- a/src/l-map.js
+++ b/src/l-map.js
@@ -54,7 +54,20 @@ class LMap extends HTMLElement {
}
connectedCallback() {
- this.map = L.map(this, { zoomControl: this.hasAttribute("zoom-control") });
+ const options = { zoomControl: this.hasAttribute("zoom-control") };
+ if (this.hasAttribute("max-zoom")) {
+ options.maxZoom = parseFloat(this.getAttribute("max-zoom"));
+ }
+ if (this.hasAttribute("min-zoom")) {
+ options.minZoom = parseFloat(this.getAttribute("min-zoom"));
+ }
+ if (this.hasAttribute("max-bounds")) {
+ options.maxBounds = JSON.parse(this.getAttribute("max-bounds"));
+ }
+ if (this.hasAttribute("attribution-control")) {
+ options["attributionControl"] = this.getAttribute("attribution-control").toLowerCase() === "true";
+ }
+ this.map = L.map(this, options);
// Allow listeners to know when the map is "ready"
this.map.whenReady(() => {
@@ -86,10 +99,19 @@ class LMap extends HTMLElement {
this.map.locate(parse(schema, this));
}
+ const layerConnectedHandlers = {
+ "l-control-layers": (layer, map) => layer.addTo(map),
+ "default": (layer, map) => map.addLayer(layer),
+ };
+
this.addEventListener(layerConnected, (ev) => {
- const layer = ev.detail.layer;
- this.map.addLayer(layer);
+ const { layer } = ev.detail;
+ const target = ev.target.localName ;
+
+ const layerConnectedHandler = layerConnectedHandlers[target] || layerConnectedHandlers["default"];
+ layerConnectedHandler(layer, this.map);
});
+
this.addEventListener(layerRemoved, (ev) => {
if (this.map !== null) {
diff --git a/src/l-map.test.js b/src/l-map.test.js
index f22beb7..5c76f29 100644
--- a/src/l-map.test.js
+++ b/src/l-map.test.js
@@ -1,9 +1,10 @@
// @vitest-environment happy-dom
import "./index.js";
import { layerRemoved, layerConnected } from "./events"
-import { it, expect } from "vitest";
+import { vi, it, expect } from "vitest";
import LTileLayer from "./l-tile-layer";
import LMap from "./l-map.js";
+import { map, latLngBounds } from "leaflet";
it("should emit map:addTo event(s)", async () => {
// Arrange: create a ... arrangement
@@ -74,3 +75,89 @@ it("should bubble layer remove events", async () => {
const expected = { layer: tileLayer.layer };
expect(actual).toEqual(expected);
})
+
+it("should handle layerConnected event from l-control-layers correctly", async () => {
+ // Arrange: create a ... arrangement
+ const el = /** @type {LMap} */ (document.createElement("l-map"));
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", JSON.stringify([0, 0]));
+
+ const controlLayers = document.createElement("l-control-layers");
+ el.appendChild(controlLayers);
+
+ // Arrange: add a trackable mock layer to the layerConnected event
+ const mockLayer = {
+ addTo: vi.fn(), // Mocks layer.addTo method
+ };
+ const event = new CustomEvent(layerConnected, {
+ bubbles: true,
+ detail: { layer: mockLayer },
+ });
+
+ // Act: connect to DOM
+ document.body.appendChild(el);
+
+ // Act: Dispatch the layerConnected event on the control layers
+ const promise = new Promise((resolve) => {
+ controlLayers.addEventListener(layerConnected, (ev) => {
+ resolve(ev.detail);
+ });
+ });
+ controlLayers.dispatchEvent(event);
+
+ // Assert: event detail is correctly passed
+ const actual = await promise;
+ expect(actual).toEqual({ layer: mockLayer });
+
+ // Assert: addTo method was called on the map
+ const map = el.map; // Map instance from
+ expect(mockLayer.addTo).toHaveBeenCalledWith(map);
+});
+
+
+it("should have attributionControl by default", () => {
+ const el = document.createElement("l-map")
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", "[0,0]");
+ document.body.appendChild(el);
+ expect(el.map.attributionControl).not.toBe(undefined);
+})
+
+
+it("should remove attributionControl given attribution-control=false attribute", () => {
+ const el = document.createElement("l-map")
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", "[0,0]");
+ el.setAttribute("attribution-control", "false");
+ document.body.appendChild(el);
+ expect(el.map.attributionControl).toBe(undefined);
+})
+
+
+it("should remove attributionControl given attribution-control=true attribute", () => {
+ const el = document.createElement("l-map")
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", "[0,0]");
+ el.setAttribute("attribution-control", "true");
+ document.body.appendChild(el);
+ expect(el.map.attributionControl).not.toBe(undefined);
+})
+
+it("should set maxZoom and minZoom given max-zoom and min-zoom attribute", () => {
+ const el = document.createElement("l-map");
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", "[0,0]");
+ el.setAttribute("max-zoom", "5");
+ el.setAttribute("min-zoom", "0");
+ document.body.appendChild(el);
+ expect(el.map.options).toEqual({ zoomControl: false, maxZoom: 5, minZoom: 0 });
+})
+
+it("should set maxBounds given max-bounds attribute", () => {
+ const el = document.createElement("l-map");
+ el.setAttribute("zoom", "0");
+ el.setAttribute("center", "[0,0]");
+ el.setAttribute("max-bounds", "[[0, 0], [1, 1]]");
+ document.body.appendChild(el);
+ expect(el.map.options).toEqual({ zoomControl: false, maxBounds: latLngBounds([[0, 0], [1, 1]]) });
+})
diff --git a/src/l-tile-layer-wms.js b/src/l-tile-layer-wms.js
index 8977821..d93cda5 100644
--- a/src/l-tile-layer-wms.js
+++ b/src/l-tile-layer-wms.js
@@ -3,14 +3,36 @@ import { tileLayer } from "leaflet";
import LLayer from "./l-layer.js";
import { layerConnected } from "./events.js";
import { htmlAttribute, optional, parse, partial } from "./parse.js";
+import { gridLayerOptions } from "./grid-layer.js";
class LTileLayerWMS extends LLayer {
+ static get observedAttributes() {
+ return ["options", "layers", "styles", "format", "transparent", "version", "crs", "uppercase"];
+ }
+
constructor() {
super();
this.layer = null;
}
connectedCallback() {
+ this.initLayer();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (this.layer && oldValue !== newValue) {
+ switch (name) {
+ case "options":
+ this.layer.setParams(this._parseNonStandardOptions(newValue));
+ break;
+ default:
+ this.layer.setParams({ [name]: newValue });
+ break;
+ }
+ }
+ }
+
+ initLayer() {
const urlTemplate = parse(htmlAttribute("url-template"), this);
const name = this.getAttribute("name");
@@ -25,36 +47,49 @@ class LTileLayerWMS extends LLayer {
uppercase: optional(htmlAttribute("uppercase")),
// Inherited option from Layer: https://leafletjs.com/reference.html#tilelayer-wms-attribution
- attribution: optional(htmlAttribute("attribution")),
+ attribution: optional(htmlAttribute("attribution"))
});
const standardOptions = parse(schema, this);
- const nonStandardOptionsElement = this.getAttribute("options");
- const nonStandardOptions = () => {
- if (nonStandardOptionsElement) {
- try {
- return JSON.parse(nonStandardOptionsElement);
- } catch (e) {
- console.error(
- "Error whilst parsing JSON for options attribute in l-tile-layer-wms",
- e,
- );
- return {};
- }
- } else {
- return {};
- }
- };
+ const nonStandardOptions = this.getAttribute("options");
+
+ // Pane options
+ const paneOptions = {};
+ // Support parent element
+ if (this.parentElement.tagName.toLowerCase() === "l-pane") {
+ paneOptions["pane"] = this.parentElement.getAttribute("name");
+ }
+
+ // GridLayer options
+ const gridOptions = gridLayerOptions(this);
this.layer = tileLayer.wms(urlTemplate, {
...standardOptions,
- ...nonStandardOptions(),
+ ...this._parseNonStandardOptions(nonStandardOptions),
+ ...paneOptions,
+ ...gridOptions
});
const event = new CustomEvent(layerConnected, {
detail: { name, layer: this.layer },
- bubbles: true,
+ bubbles: true
});
this.dispatchEvent(event);
}
+
+ _parseNonStandardOptions(nonStandardOptions) {
+ if (nonStandardOptions) {
+ try {
+ return JSON.parse(nonStandardOptions);
+ } catch (e) {
+ console.error(
+ "Error whilst parsing JSON for options attribute in l-tile-layer-wms",
+ e
+ );
+ }
+ }
+
+ return {};
+ }
}
+
export default LTileLayerWMS;
diff --git a/src/l-tile-layer-wms.test.js b/src/l-tile-layer-wms.test.js
index 001946b..f27e5c0 100644
--- a/src/l-tile-layer-wms.test.js
+++ b/src/l-tile-layer-wms.test.js
@@ -1,8 +1,9 @@
// @vitest-environment happy-dom
-import { tileLayer } from "leaflet";
-import { it, expect } from "vitest";
+import { point, tileLayer } from "leaflet";
+import { it, expect, vi } from "vitest";
import { layerConnected } from "./events";
import "./index";
+import { waitFor } from "@storybook/test";
it("should create an l-tile-layer-wms with the correct options", async () => {
const urlTemplate = "http://ows.mundialis.de/services/service?";
@@ -91,3 +92,39 @@ it("should handle invalid JSON in the options attribute gracefully", () => {
const expected = tileLayer.wms(urlTemplate, { layers: "example layer ere" });
expect(actual).toEqual(expected);
});
+
+it.each([
+ ["512", 512],
+ ["[256, 512]", point({ x: 256, y: 512 })],
+ ['{"x": 256, "y": 512}', point({ x: 256, y: 512 })],
+])("should support tile-size attribute", (text, tileSize) => {
+ const baseUrl = "/";
+ const layers = "layer-1";
+ const el = document.createElement("l-tile-layer-wms");
+ el.setAttribute("url-template", baseUrl);
+ el.setAttribute("layers", layers);
+ el.setAttribute("tile-size", text);
+ document.body.appendChild(el);
+ const actual = el.layer;
+ const expected = tileLayer.wms(baseUrl, { layers, tileSize });
+ expect(actual).toEqual(expected);
+});
+
+it.each([
+ ["options", '{"banana": "yo"}', '{"banana": "ok"}', true],
+ ["transparent", "TRUE", "FALSE", false],
+])("should update layer params when non-standard or standard attributes change", (attributeName, attributeInitialValue, attributeNewValue, isJson) => {
+ const baseUrl = "/";
+ const layers = "layer-1";
+ const el = document.createElement("l-tile-layer-wms");
+ el.setAttribute("url-template", baseUrl);
+ el.setAttribute("layers", layers);
+ el.setAttribute(attributeName, attributeInitialValue);
+
+ document.body.appendChild(el);
+ const setParams = vi.spyOn(el.layer, "setParams");
+
+ el.setAttribute(attributeName, attributeNewValue);
+
+ expect(setParams).toHaveBeenCalledWith(isJson ? JSON.parse(attributeNewValue) : {[attributeName]: attributeNewValue})
+});
diff --git a/src/l-tile-layer.js b/src/l-tile-layer.js
index 8185645..a3d509e 100644
--- a/src/l-tile-layer.js
+++ b/src/l-tile-layer.js
@@ -3,6 +3,7 @@ import { tileLayer } from "leaflet";
import { layerConnected } from "./events.js";
import LLayer from "./l-layer.js";
import { htmlAttribute, optional, parse, partial } from "./parse.js";
+import { gridLayerOptions } from "./grid-layer.js";
class LTileLayer extends LLayer {
constructor() {
@@ -11,33 +12,50 @@ class LTileLayer extends LLayer {
}
connectedCallback() {
- const urlTemplate = parse(htmlAttribute("url-template"), this)
-
+ const urlTemplate = parse(htmlAttribute("url-template"), this);
+
// Template attributes
- const urlAttributes = LTileLayer.parseTemplateAttributes(urlTemplate)
- const templateOptions = {}
+ const urlAttributes = LTileLayer.parseTemplateAttributes(urlTemplate);
+ const templateOptions = {};
for (const attribute of urlAttributes) {
- const value = this.getAttribute(attribute)
+ const value = this.getAttribute(attribute);
if (value !== null) {
- templateOptions[attribute] = value
+ templateOptions[attribute] = value;
}
}
// Pane options
- const paneOptions = {}
+ const paneOptions = {};
// Support parent element
if (this.parentElement.tagName.toLowerCase() === "l-pane") {
- paneOptions["pane"] = this.parentElement.getAttribute("name")
+ paneOptions["pane"] = this.parentElement.getAttribute("name");
}
-
+
// Options
const name = this.getAttribute("name");
const schema = partial({
attribution: optional(htmlAttribute("attribution")),
- errorTileUrl: optional(htmlAttribute("error-tile-url"))
- })
- const options = parse(schema, this)
- this.layer = tileLayer(urlTemplate, { ...templateOptions, ...paneOptions, ...options });
+ errorTileUrl: optional(htmlAttribute("error-tile-url")),
+ });
+ const options = parse(schema, this);
+
+ const zoomOffset = this.getAttribute("zoom-offset");
+ if (zoomOffset) {
+ const number = parseInt(zoomOffset);
+ if (!isNaN(number)) {
+ options["zoomOffset"] = number;
+ }
+ }
+
+ // GridLayer options
+ const gridOptions = gridLayerOptions(this);
+
+ this.layer = tileLayer(urlTemplate, {
+ ...templateOptions,
+ ...paneOptions,
+ ...options,
+ ...gridOptions,
+ });
const event = new CustomEvent(layerConnected, {
detail: { name, layer: this.layer },
bubbles: true,
@@ -50,8 +68,8 @@ class LTileLayer extends LLayer {
* @returns {string[]}
*/
static parseTemplateAttributes(urlTemplate) {
- const regex = /{(.*?)}/g
- return [...urlTemplate.matchAll(regex)].map(match => match[1])
+ const regex = /{(.*?)}/g;
+ return [...urlTemplate.matchAll(regex)].map((match) => match[1]);
}
}
diff --git a/src/l-tile-layer.test.js b/src/l-tile-layer.test.js
index cf1940b..085b5bd 100644
--- a/src/l-tile-layer.test.js
+++ b/src/l-tile-layer.test.js
@@ -27,27 +27,66 @@ it("should cover l-tile-layer", async () => {
it.each([
["/tile/{z}/{x}/{y}.png?key={key}", "key", "value"],
["/tile/{z}/{x}/{y}.png?key={camelCase}", "camelCase", "value"],
- ["/tile/{z}/{x}/{y}.png?key={kebab-case}", "kebab-case", "value"]
+ ["/tile/{z}/{x}/{y}.png?key={kebab-case}", "kebab-case", "value"],
])("should perform arbitrary templating %s %s", (urlTemplate, key, value) => {
const el = document.createElement("l-tile-layer");
el.setAttribute("url-template", urlTemplate);
- el.setAttribute(key, value)
+ el.setAttribute(key, value);
document.body.appendChild(el);
- const actual = el.layer
- const expected = tileLayer(urlTemplate, { [key]: value })
- expect(actual).toEqual(expected)
-})
+ const actual = el.layer;
+ const expected = tileLayer(urlTemplate, { [key]: value });
+ expect(actual).toEqual(expected);
+});
it("should support error-tile-url", () => {
- const urlTemplate = "/{z}/{x}/{y}.png"
- const errorTileUrl = "/error.png"
+ const urlTemplate = "/{z}/{x}/{y}.png";
+ const errorTileUrl = "/error.png";
+ const el = document.createElement("l-tile-layer");
+ el.setAttribute("url-template", urlTemplate);
+ el.setAttribute("error-tile-url", errorTileUrl);
+ document.body.appendChild(el);
+
+ const actual = el.layer;
+ const expected = tileLayer(urlTemplate, { errorTileUrl });
+ expect(actual).toEqual(expected);
+});
+
+it.each([
+ ["512", 512],
+ ["[256, 512]", { x: 256, y: 512 }],
+ ['{"x": 256, "y": 512}', { x: 256, y: 512 }],
+])("should support tile-size attribute", (text, value) => {
+ const urlTemplate = "/";
const el = document.createElement("l-tile-layer");
el.setAttribute("url-template", urlTemplate);
- el.setAttribute("error-tile-url", errorTileUrl)
+ el.setAttribute("tile-size", text);
document.body.appendChild(el);
+ const actual = el.layer;
+ const expected = tileLayer(urlTemplate, { tileSize: value });
+ expect(actual).toEqual(expected);
+});
+
+it("should support tile-size attribute default value", () => {
+ const urlTemplate = "/";
+ const el = document.createElement("l-tile-layer");
+ el.setAttribute("url-template", urlTemplate);
+ document.body.appendChild(el);
+ const actual = el.layer;
+ const expected = tileLayer(urlTemplate, {});
+ expect(actual).toEqual(expected);
+});
- const actual = el.layer
- const expected = tileLayer(urlTemplate, { errorTileUrl })
- expect(actual).toEqual(expected)
-})
+it.each([["-1", -1]])(
+ "should support zoom-offset attribute",
+ (text, zoomOffset) => {
+ const urlTemplate = "/";
+ const el = document.createElement("l-tile-layer");
+ el.setAttribute("url-template", urlTemplate);
+ el.setAttribute("zoom-offset", text);
+ document.body.appendChild(el);
+ const actual = el.layer;
+ const expected = tileLayer(urlTemplate, { zoomOffset });
+ expect(actual).toEqual(expected);
+ },
+);
diff --git a/src/stories/Configure.mdx b/src/stories/Configure.mdx
new file mode 100644
index 0000000..d833449
--- /dev/null
+++ b/src/stories/Configure.mdx
@@ -0,0 +1,7 @@
+import { Meta } from "@storybook/blocks";
+
+
+
+# Leaflet HTML - Modern Hypermedia Maps
+
+Make a modern application using modern web platform techniques!
diff --git a/src/stories/DivIcon.stories.js b/src/stories/DivIcon.stories.js
new file mode 100644
index 0000000..120a830
--- /dev/null
+++ b/src/stories/DivIcon.stories.js
@@ -0,0 +1,47 @@
+import "../index.js"
+import "./divicon.css"
+
+
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
+export default {
+ title: 'Example/DivIcon',
+ tags: ['autodocs'],
+ render: ({ className }) => {
+ let icon = ""
+ if (className) {
+ icon = ``
+ }
+ return `${ icon }`
+ },
+ argTypes: {
+ className: { type: "string", description: "HTML attribute **class-name**, passed to Leaflet as **className** option.", control: "select", options: ["none", "red", "blue", "yellow"]},
+ },
+ args: {
+ },
+ decorators: [(story) => `
+
+
+ ${story()}
+
+ `],
+};
+
+// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
+export const Default = {
+ args: {
+ },
+};
+
+export const Red = {
+ args: {
+ className: "red"
+ },
+};
+
+export const Blue = {
+ args: {
+ className: "blue"
+ },
+};
diff --git a/src/stories/divicon.css b/src/stories/divicon.css
new file mode 100644
index 0000000..ab85759
--- /dev/null
+++ b/src/stories/divicon.css
@@ -0,0 +1,9 @@
+.red {
+ background-color: red;
+}
+.blue {
+ background-color: blue;
+}
+.yellow {
+ background-color: yellow;
+}