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 @@ -

- -

+ +![f26c08f1-ab0e-47de-ae8d-fbd062383555~3.jpg](https://github.com/user-attachments/assets/38887c1b-1c20-48de-be01-df264ee6cdeb) # 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; +}