From 0ed3cfd6242ca080ff954cfaf39d4f468ab15061 Mon Sep 17 00:00:00 2001 From: Ana Lameira Date: Thu, 2 Jan 2025 17:58:47 +0000 Subject: [PATCH 1/9] Fix change options attribute handling --- src/l-tile-layer-wms.js | 39 +++++++----------- src/l-tile-layer-wms.test.js | 76 ------------------------------------ 2 files changed, 15 insertions(+), 100 deletions(-) diff --git a/src/l-tile-layer-wms.js b/src/l-tile-layer-wms.js index ebc7e6e..f90e2bd 100644 --- a/src/l-tile-layer-wms.js +++ b/src/l-tile-layer-wms.js @@ -20,10 +20,8 @@ class LTileLayerWMS extends LLayer { } attributeChangedCallback(name, oldValue, newValue) { - if (name === "options" && oldValue !== newValue) { - if (this.isConnected) { - this.reloadLayer(); - } + if (name === "options" && oldValue !== newValue && this.layer) { + this.layer.setParams(this._parseNonStandardOptions(newValue)); } } @@ -47,21 +45,6 @@ class LTileLayerWMS extends LLayer { 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 {}; - } - }; // Pane options const paneOptions = {}; @@ -75,7 +58,7 @@ class LTileLayerWMS extends LLayer { this.layer = tileLayer.wms(urlTemplate, { ...standardOptions, - ...nonStandardOptions(), + ...this._parseNonStandardOptions(nonStandardOptionsElement), ...paneOptions, ...gridOptions, }); @@ -86,11 +69,19 @@ class LTileLayerWMS extends LLayer { this.dispatchEvent(event); } - reloadLayer() { - if (this.layer) { - this.layer.remove(); + _parseNonStandardOptions(nonStandardOptionsElement) { + if (nonStandardOptionsElement) { + try { + return JSON.parse(nonStandardOptionsElement); + } catch (e) { + console.error( + "Error whilst parsing JSON for options attribute in l-tile-layer-wms", + e, + ); + } } - this.initLayer(); + + return {}; } } diff --git a/src/l-tile-layer-wms.test.js b/src/l-tile-layer-wms.test.js index 8bece42..fb1054f 100644 --- a/src/l-tile-layer-wms.test.js +++ b/src/l-tile-layer-wms.test.js @@ -92,82 +92,6 @@ it("should handle invalid JSON in the options attribute gracefully", () => { expect(actual).toEqual(expected); }); -it("should reload the layer when the options attribute changes", async () => { - const urlTemplate = "http://example.com/wms"; - const initialOptions = JSON.stringify({ height: 101, bbox: "coords ere" }); - const updatedOptions = JSON.stringify({ height: 202, bbox: "new coords" }); - - const el = document.createElement("l-tile-layer-wms"); - el.setAttribute("url-template", urlTemplate); - el.setAttribute("layers", "example layer ere"); - el.setAttribute("options", initialOptions); - - let layerConnectedEventEmittedCount = 0; - let promise = new Promise((resolve) => { - el.addEventListener(layerConnected, (ev) => { - layerConnectedEventEmittedCount += 1; - resolve(ev.detail); - }); - }); - - document.body.appendChild(el); - - // Wait for the initial layer to be created - let detail = await promise; - expect(detail.layer.options.height).toBe(101); - expect(detail.layer.options.bbox).toBe("coords ere"); - - // Change the options attribute - promise = new Promise((resolve) => { - el.addEventListener(layerConnected, (ev) => { - resolve(ev.detail); - }); - }); - - // Update the options attribute - el.setAttribute("options", updatedOptions); - - // Wait for the layer to reload - detail = await promise; - expect(detail.layer.options.height).toBe(202); - expect(detail.layer.options.bbox).toBe("new coords"); - expect(layerConnectedEventEmittedCount).toBe(2); // initial layer creation + reload -}); - -it("should not reload the layer when non-options attributes are changed", async () => { - const urlTemplate = "http://example.com/wms"; - const initialOptions = JSON.stringify({ height: 101, bbox: "coords ere" }); - - const el = document.createElement("l-tile-layer-wms"); - el.setAttribute("url-template", urlTemplate); - el.setAttribute("layers", "example layer ere"); - el.setAttribute("options", initialOptions); - - let layerConnectedEventEmittedCount = 0; - let promise = new Promise((resolve) => { - el.addEventListener(layerConnected, (ev) => { - layerConnectedEventEmittedCount += 1; - resolve(ev.detail); - }); - }); - - document.body.appendChild(el); - - // Wait for the initial layer to be created - let detail = await promise; - expect(detail.layer.options.height).toBe(101); - expect(detail.layer.options.bbox).toBe("coords ere"); - - // Update the a different attribute to options - el.setAttribute("a-different-attribute", "with different value"); - - // Give the layer a chance to reload - detail = await promise; - expect(detail.layer.options.height).toBe(101); - expect(detail.layer.options.bbox).toBe("coords ere"); - expect(layerConnectedEventEmittedCount).toBe(1); // initial layer creation only -}); - it.each([ ["512", 512], ["[256, 512]", point({ x: 256, y: 512 })], From 215c1b4481211a7aeee37d3ea712f58db4a0f36e Mon Sep 17 00:00:00 2001 From: Ana Lameira Date: Fri, 3 Jan 2025 11:20:24 +0000 Subject: [PATCH 2/9] Add support for standard attribute changes as well (grid options not included) --- src/l-tile-layer-wms.js | 31 +++++++++++++++++++------------ src/l-tile-layer-wms.test.js | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/l-tile-layer-wms.js b/src/l-tile-layer-wms.js index f90e2bd..d93cda5 100644 --- a/src/l-tile-layer-wms.js +++ b/src/l-tile-layer-wms.js @@ -7,7 +7,7 @@ import { gridLayerOptions } from "./grid-layer.js"; class LTileLayerWMS extends LLayer { static get observedAttributes() { - return ["options"]; + return ["options", "layers", "styles", "format", "transparent", "version", "crs", "uppercase"]; } constructor() { @@ -20,8 +20,15 @@ class LTileLayerWMS extends LLayer { } attributeChangedCallback(name, oldValue, newValue) { - if (name === "options" && oldValue !== newValue && this.layer) { - this.layer.setParams(this._parseNonStandardOptions(newValue)); + if (this.layer && oldValue !== newValue) { + switch (name) { + case "options": + this.layer.setParams(this._parseNonStandardOptions(newValue)); + break; + default: + this.layer.setParams({ [name]: newValue }); + break; + } } } @@ -40,11 +47,11 @@ 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 = this.getAttribute("options"); // Pane options const paneOptions = {}; @@ -58,25 +65,25 @@ class LTileLayerWMS extends LLayer { this.layer = tileLayer.wms(urlTemplate, { ...standardOptions, - ...this._parseNonStandardOptions(nonStandardOptionsElement), + ...this._parseNonStandardOptions(nonStandardOptions), ...paneOptions, - ...gridOptions, + ...gridOptions }); const event = new CustomEvent(layerConnected, { detail: { name, layer: this.layer }, - bubbles: true, + bubbles: true }); this.dispatchEvent(event); } - _parseNonStandardOptions(nonStandardOptionsElement) { - if (nonStandardOptionsElement) { + _parseNonStandardOptions(nonStandardOptions) { + if (nonStandardOptions) { try { - return JSON.parse(nonStandardOptionsElement); + return JSON.parse(nonStandardOptions); } catch (e) { console.error( "Error whilst parsing JSON for options attribute in l-tile-layer-wms", - e, + e ); } } diff --git a/src/l-tile-layer-wms.test.js b/src/l-tile-layer-wms.test.js index fb1054f..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 { point, tileLayer } from "leaflet"; -import { it, expect } from "vitest"; +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?"; @@ -108,3 +109,22 @@ it.each([ 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}) +}); From 3e28a69224ee8052afe8e3c3d8f0ab860cc07e03 Mon Sep 17 00:00:00 2001 From: andrewgryan Date: Fri, 3 Jan 2025 12:29:32 +0000 Subject: [PATCH 3/9] Release 0.13.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 763f43d..64d265c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "leaflet-html", "type": "module", - "version": "0.13.7", + "version": "0.13.8", "description": "Leaflet maps expressed in HTML suitable for HTMX", "keywords": [ "leaflet", From f82f9150246712efb1769eab4c47dd2fefa8fcb1 Mon Sep 17 00:00:00 2001 From: andrewgryan Date: Tue, 19 Aug 2025 12:08:57 +0100 Subject: [PATCH 4/9] add max-zoom, min-zoom and max-bounds attributes to l-map connectedCallback --- src/l-map.js | 9 +++++++++ src/l-map.test.js | 21 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/l-map.js b/src/l-map.js index f73f264..22d3a66 100644 --- a/src/l-map.js +++ b/src/l-map.js @@ -55,6 +55,15 @@ class LMap extends HTMLElement { connectedCallback() { 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"; } diff --git a/src/l-map.test.js b/src/l-map.test.js index 77ee079..5c76f29 100644 --- a/src/l-map.test.js +++ b/src/l-map.test.js @@ -4,7 +4,7 @@ import { layerRemoved, layerConnected } from "./events" import { vi, it, expect } from "vitest"; import LTileLayer from "./l-tile-layer"; import LMap from "./l-map.js"; -import { map } from "leaflet"; +import { map, latLngBounds } from "leaflet"; it("should emit map:addTo event(s)", async () => { // Arrange: create a ... arrangement @@ -142,3 +142,22 @@ it("should remove attributionControl given attribution-control=true attribute", 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]]) }); +}) From 9b699d773dfe9bfbcde0ee60eb8f8813ffcf1746 Mon Sep 17 00:00:00 2001 From: andrewgryan Date: Tue, 19 Aug 2025 12:24:17 +0100 Subject: [PATCH 5/9] Release 0.13.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64d265c..2ccb0b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "leaflet-html", "type": "module", - "version": "0.13.8", + "version": "0.13.9", "description": "Leaflet maps expressed in HTML suitable for HTMX", "keywords": [ "leaflet", From b894f3153f7ec528711a491e3add6b97c9df5f1c Mon Sep 17 00:00:00 2001 From: "Nick.Brigden" Date: Tue, 26 Aug 2025 08:40:13 +0100 Subject: [PATCH 6/9] Added capture for icon-anchor and div-icon style --- src/l-div-icon.js | 4 ++++ src/l-div-icon.test.js | 3 ++- src/l-geojson.js | 4 ++++ src/l-geojson.test.js | 31 +++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/l-geojson.test.js diff --git a/src/l-div-icon.js b/src/l-div-icon.js index 2880b67..d234314 100644 --- a/src/l-div-icon.js +++ b/src/l-div-icon.js @@ -16,6 +16,10 @@ export default class CustomElement extends HTMLElement { if (className !== null) { options["className"] = className; } + const iconAnchor = this.getAttribute("icon-anchor"); + if (iconAnchor !== null) { + options["iconAnchor"] = iconAnchor; + } this.icon = divIcon(options); this.dispatchEvent( diff --git a/src/l-div-icon.test.js b/src/l-div-icon.test.js index e152bd6..53c8e1f 100644 --- a/src/l-div-icon.test.js +++ b/src/l-div-icon.test.js @@ -6,9 +6,10 @@ 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]"); 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]"})); }); it("should attach div icon to marker", () => { 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); +}); From d138d013b03576294a8df7224934167bb06a45c3 Mon Sep 17 00:00:00 2001 From: andrewgryan Date: Fri, 29 Aug 2025 11:42:29 +0100 Subject: [PATCH 7/9] Release 0.13.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ccb0b1..ecf44f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "leaflet-html", "type": "module", - "version": "0.13.9", + "version": "0.13.10", "description": "Leaflet maps expressed in HTML suitable for HTMX", "keywords": [ "leaflet", From 3cc99af59c8effe70f4d7b37dd04eb224d67c557 Mon Sep 17 00:00:00 2001 From: "Nick.Brigden" Date: Mon, 1 Sep 2025 15:06:02 +0100 Subject: [PATCH 8/9] Added capture for icon-size --- src/l-div-icon.js | 6 +++++- src/l-div-icon.test.js | 24 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/l-div-icon.js b/src/l-div-icon.js index d234314..c0937ab 100644 --- a/src/l-div-icon.js +++ b/src/l-div-icon.js @@ -18,7 +18,11 @@ export default class CustomElement extends HTMLElement { } const iconAnchor = this.getAttribute("icon-anchor"); if (iconAnchor !== null) { - options["iconAnchor"] = iconAnchor; + options["iconAnchor"] = JSON.parse(iconAnchor); + } + const iconSize = this.getAttribute("icon-size"); + if (iconSize != null) { + options["iconSize"] = JSON.parse(iconSize); } this.icon = divIcon(options); diff --git a/src/l-div-icon.test.js b/src/l-div-icon.test.js index 53c8e1f..fd243e6 100644 --- a/src/l-div-icon.test.js +++ b/src/l-div-icon.test.js @@ -7,9 +7,30 @@ 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!" , iconAnchor: "[50, 50]"})); + 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", () => { @@ -19,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()); }); From 97caa99813f32fe6c3fac348aebf980e28690bb9 Mon Sep 17 00:00:00 2001 From: andrewgryan Date: Tue, 2 Sep 2025 13:24:35 +0100 Subject: [PATCH 9/9] Release 0.13.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecf44f0..5dc0d03 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "leaflet-html", "type": "module", - "version": "0.13.10", + "version": "0.13.11", "description": "Leaflet maps expressed in HTML suitable for HTMX", "keywords": [ "leaflet",