maplibre-gl-layer-control 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
24
24
  - ✅ **Dynamic layer detection** - Automatically detect and manage new layers
25
25
  - ✅ **Background layer grouping** - Control all basemap layers as one group
26
26
  - ✅ **Background layer legend** - Gear icon to toggle individual background layer visibility
27
+ - ✅ **Saved configurations** - Save the current basemap element visibility as a named preset and re-apply it with one click; presets persist across sessions and projects via `localStorage`
27
28
  - ✅ **Accessibility** - Full ARIA support and keyboard navigation
28
29
  - ✅ **TypeScript** - Full type safety and IntelliSense support
29
30
  - ✅ **React integration** - Optional React components and hooks
@@ -175,6 +176,9 @@ function MapComponent() {
175
176
  | `excludeLayers` | `string[]` | `undefined` | Array of wildcard patterns to exclude layers by name (e.g., `['*-temp-*', 'debug-*']`) |
176
177
  | `customLayerAdapters` | `CustomLayerAdapter[]` | `undefined` | Adapters for non-MapLibre layers (deck.gl, Zarr, etc.) |
177
178
  | `basemapStyleUrl` | `string` | `undefined` | URL of basemap style JSON for reliable layer detection (see below) |
179
+ | `enableBackgroundPresets` | `boolean` | `true` | Show the "Saved configurations" controls in the Background Layers panel |
180
+ | `backgroundPresetStorageKey` | `string` | `'maplibre-layer-control:background-presets'` | `localStorage` key under which background visibility presets are stored |
181
+ | `onBackgroundPresetsChange` | `(presets: BackgroundPresets) => void` | `undefined` | Called whenever the saved preset set changes (created or deleted); applying a preset does not change the set, so it does not fire |
178
182
 
179
183
  ### LayerState
180
184
 
@@ -293,9 +297,31 @@ When using the `layers` option to specify specific layers, all other layers are
293
297
  - Quick "Show All" / "Hide All" buttons
294
298
  - **"Only rendered" filter** - Shows only layers that are currently rendered in the map viewport
295
299
  - Indeterminate checkbox state when some layers are hidden
300
+ - **Saved configurations** - Save the current set of visibility toggles as a named preset and re-apply it later with one click
296
301
 
297
302
  This allows fine-grained control over which basemap layers are visible while maintaining a simplified layer control interface.
298
303
 
304
+ #### Saved configurations (presets)
305
+
306
+ The Background Layers panel includes a **Saved configurations** section that lets users save the current basemap element visibility as a named preset and re-apply it with a single click. Presets are stored in `localStorage`, so they persist across page reloads and apply to any project that uses the same basemap layers. Toggle the UI off with `enableBackgroundPresets: false`, or change the storage location with `backgroundPresetStorageKey`.
307
+
308
+ The same functionality is available programmatically:
309
+
310
+ ```javascript
311
+ // Capture the current basemap element visibility
312
+ const visibility = layerControl.getBackgroundLayerVisibility();
313
+ // → { 'water': true, 'road-primary': false, ... }
314
+
315
+ // Apply a configuration (only layers present in the style are affected)
316
+ layerControl.applyBackgroundLayerVisibility(visibility);
317
+
318
+ // Named presets (persisted to localStorage)
319
+ layerControl.saveBackgroundPreset('Minimal'); // save current visibility
320
+ layerControl.getBackgroundPresets(); // → { Minimal: { ... } }
321
+ layerControl.applyBackgroundPreset('Minimal'); // → true if it existed
322
+ layerControl.deleteBackgroundPreset('Minimal');
323
+ ```
324
+
299
325
  ### Custom Layer Adapters
300
326
 
301
327
  The layer control supports non-MapLibre layers (such as deck.gl or Zarr layers) through the Custom Layer Adapter interface. This allows you to integrate any custom layer type with the layer control's visibility toggle, opacity slider, and layer list.
package/dist/index.cjs CHANGED
@@ -730,6 +730,10 @@ class LayerControl {
730
730
  __publicField(this, "onLayerRename");
731
731
  __publicField(this, "onLayerReorder");
732
732
  __publicField(this, "onLayerRemove");
733
+ // Background-layer visibility presets
734
+ __publicField(this, "enableBackgroundPresets");
735
+ __publicField(this, "backgroundPresetStorageKey");
736
+ __publicField(this, "onBackgroundPresetsChange");
733
737
  this.minPanelWidth = options.panelMinWidth || 240;
734
738
  this.maxPanelWidth = options.panelMaxWidth || 420;
735
739
  this.initialPanelWidth = options.panelWidth || 350;
@@ -746,6 +750,9 @@ class LayerControl {
746
750
  this.onLayerRename = options.onLayerRename;
747
751
  this.onLayerReorder = options.onLayerReorder;
748
752
  this.onLayerRemove = options.onLayerRemove;
753
+ this.enableBackgroundPresets = options.enableBackgroundPresets !== false;
754
+ this.backgroundPresetStorageKey = options.backgroundPresetStorageKey || "maplibre-layer-control:background-presets";
755
+ this.onBackgroundPresetsChange = options.onBackgroundPresetsChange;
749
756
  this.initialLayerStates = options.layerStates || {};
750
757
  this.state = {
751
758
  collapsed: options.collapsed !== false,
@@ -2234,8 +2241,112 @@ class LayerControl {
2234
2241
  panel.appendChild(actionsRow);
2235
2242
  panel.appendChild(filterRow);
2236
2243
  panel.appendChild(layerList);
2244
+ if (this.enableBackgroundPresets) {
2245
+ panel.appendChild(this.createBackgroundPresetsRow());
2246
+ }
2237
2247
  return panel;
2238
2248
  }
2249
+ /**
2250
+ * Build the "Saved configurations" controls: a dropdown of saved presets with
2251
+ * Apply/Delete actions, plus a name field and Save button to capture the
2252
+ * current basemap element visibility as a reusable, persisted preset.
2253
+ */
2254
+ createBackgroundPresetsRow() {
2255
+ const section = document.createElement("div");
2256
+ section.className = "background-legend-presets";
2257
+ const label = document.createElement("div");
2258
+ label.className = "background-legend-presets-label";
2259
+ label.textContent = "Saved configurations";
2260
+ section.appendChild(label);
2261
+ const selectRow = document.createElement("div");
2262
+ selectRow.className = "background-legend-presets-row";
2263
+ const select = document.createElement("select");
2264
+ select.className = "background-legend-presets-select";
2265
+ select.title = "Saved configurations";
2266
+ const applyBtn = document.createElement("button");
2267
+ applyBtn.className = "background-legend-action-btn";
2268
+ applyBtn.textContent = "Apply";
2269
+ applyBtn.title = "Apply the selected configuration";
2270
+ const deleteBtn = document.createElement("button");
2271
+ deleteBtn.className = "background-legend-action-btn";
2272
+ deleteBtn.textContent = "Delete";
2273
+ deleteBtn.title = "Delete the selected configuration";
2274
+ const refreshSelect = (selected) => {
2275
+ const presets = this.getBackgroundPresets();
2276
+ const names = Object.keys(presets).sort(
2277
+ (a, b) => a.localeCompare(b, void 0, { sensitivity: "base" })
2278
+ );
2279
+ select.innerHTML = "";
2280
+ const placeholder = document.createElement("option");
2281
+ placeholder.value = "";
2282
+ placeholder.textContent = names.length ? "Select a configuration…" : "No saved configurations";
2283
+ placeholder.disabled = names.length > 0;
2284
+ select.appendChild(placeholder);
2285
+ names.forEach((name) => {
2286
+ const option = document.createElement("option");
2287
+ option.value = name;
2288
+ option.textContent = name;
2289
+ select.appendChild(option);
2290
+ });
2291
+ select.value = selected && names.includes(selected) ? selected : "";
2292
+ const hasSelection = select.value !== "";
2293
+ applyBtn.disabled = !hasSelection;
2294
+ deleteBtn.disabled = !hasSelection;
2295
+ };
2296
+ select.addEventListener("change", () => {
2297
+ const hasSelection = select.value !== "";
2298
+ applyBtn.disabled = !hasSelection;
2299
+ deleteBtn.disabled = !hasSelection;
2300
+ });
2301
+ applyBtn.addEventListener("click", (e) => {
2302
+ e.stopPropagation();
2303
+ if (select.value) this.applyBackgroundPreset(select.value);
2304
+ });
2305
+ deleteBtn.addEventListener("click", (e) => {
2306
+ e.stopPropagation();
2307
+ if (select.value) {
2308
+ this.deleteBackgroundPreset(select.value);
2309
+ refreshSelect();
2310
+ }
2311
+ });
2312
+ selectRow.appendChild(select);
2313
+ selectRow.appendChild(applyBtn);
2314
+ selectRow.appendChild(deleteBtn);
2315
+ const saveRow = document.createElement("div");
2316
+ saveRow.className = "background-legend-presets-row";
2317
+ const nameInput = document.createElement("input");
2318
+ nameInput.type = "text";
2319
+ nameInput.className = "background-legend-presets-input";
2320
+ nameInput.placeholder = "Configuration name…";
2321
+ nameInput.maxLength = 60;
2322
+ const saveBtn = document.createElement("button");
2323
+ saveBtn.className = "background-legend-action-btn";
2324
+ saveBtn.textContent = "Save";
2325
+ saveBtn.title = "Save the current visibility as a configuration";
2326
+ const commitSave = () => {
2327
+ const name = nameInput.value.trim();
2328
+ if (!name) return;
2329
+ this.saveBackgroundPreset(name);
2330
+ nameInput.value = "";
2331
+ refreshSelect(name);
2332
+ };
2333
+ saveBtn.addEventListener("click", (e) => {
2334
+ e.stopPropagation();
2335
+ commitSave();
2336
+ });
2337
+ nameInput.addEventListener("keydown", (e) => {
2338
+ if (e.key === "Enter") {
2339
+ e.preventDefault();
2340
+ commitSave();
2341
+ }
2342
+ });
2343
+ saveRow.appendChild(nameInput);
2344
+ saveRow.appendChild(saveBtn);
2345
+ section.appendChild(selectRow);
2346
+ section.appendChild(saveRow);
2347
+ refreshSelect();
2348
+ return section;
2349
+ }
2239
2350
  /**
2240
2351
  * Check if a layer is currently rendered in the map viewport
2241
2352
  */
@@ -2383,6 +2494,147 @@ class LayerControl {
2383
2494
  this.state.layerStates["Background"].visible = anyVisible;
2384
2495
  }
2385
2496
  }
2497
+ /**
2498
+ * Return the IDs of the background (basemap) style layers the control can
2499
+ * toggle, honoring the drawn-layer and user-defined exclusion filters but not
2500
+ * the transient "only rendered" view filter.
2501
+ */
2502
+ getControllableBackgroundLayerIds() {
2503
+ const styleLayers = this.map.getStyle().layers || [];
2504
+ return styleLayers.filter((layer) => {
2505
+ if (this.isUserAddedLayer(layer.id)) return false;
2506
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id)) return false;
2507
+ if (this.isExcludedByPattern(layer.id)) return false;
2508
+ return true;
2509
+ }).map((layer) => layer.id);
2510
+ }
2511
+ /**
2512
+ * Get the current visibility of every controllable background (basemap) layer.
2513
+ *
2514
+ * @returns A map of style-layer ID to whether it is currently visible.
2515
+ */
2516
+ getBackgroundLayerVisibility() {
2517
+ const visibility = {};
2518
+ for (const layerId of this.getControllableBackgroundLayerIds()) {
2519
+ const value = this.map.getLayoutProperty(layerId, "visibility");
2520
+ visibility[layerId] = value !== "none";
2521
+ }
2522
+ return visibility;
2523
+ }
2524
+ /**
2525
+ * Apply a saved background-layer visibility configuration to the map. Only
2526
+ * layers that currently exist in the style are affected, so a configuration
2527
+ * captured on one basemap degrades gracefully when applied to another.
2528
+ *
2529
+ * @param visibility - Map of style-layer ID to desired visibility.
2530
+ */
2531
+ applyBackgroundLayerVisibility(visibility) {
2532
+ for (const [layerId, visible] of Object.entries(visibility)) {
2533
+ if (this.isUserAddedLayer(layerId)) continue;
2534
+ if (!this.map.getLayer(layerId)) continue;
2535
+ this.state.backgroundLayerVisibility.set(layerId, visible);
2536
+ this.map.setLayoutProperty(
2537
+ layerId,
2538
+ "visibility",
2539
+ visible ? "visible" : "none"
2540
+ );
2541
+ }
2542
+ const legendPanel = this.panel.querySelector(
2543
+ ".background-legend-layer-list"
2544
+ );
2545
+ if (legendPanel) {
2546
+ this.populateBackgroundLayerList(legendPanel);
2547
+ }
2548
+ this.updateBackgroundCheckboxState();
2549
+ }
2550
+ /**
2551
+ * Read all saved background-layer presets from localStorage. Returns an empty
2552
+ * object when storage is unavailable or the stored value is malformed.
2553
+ */
2554
+ getBackgroundPresets() {
2555
+ try {
2556
+ const raw = typeof localStorage !== "undefined" ? localStorage.getItem(this.backgroundPresetStorageKey) : null;
2557
+ if (!raw) return {};
2558
+ const parsed = JSON.parse(raw);
2559
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2560
+ return {};
2561
+ }
2562
+ const presets = {};
2563
+ for (const [name, value] of Object.entries(parsed)) {
2564
+ if (this.isUnsafePresetKey(name)) continue;
2565
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2566
+ presets[name] = value;
2567
+ }
2568
+ }
2569
+ return presets;
2570
+ } catch {
2571
+ return {};
2572
+ }
2573
+ }
2574
+ /**
2575
+ * Reject preset names that, used as object keys, could pollute the prototype
2576
+ * chain or otherwise alias built-in object members.
2577
+ */
2578
+ isUnsafePresetKey(name) {
2579
+ return name === "__proto__" || name === "constructor" || name === "prototype";
2580
+ }
2581
+ /**
2582
+ * Persist the full preset map to localStorage and notify listeners.
2583
+ */
2584
+ writeBackgroundPresets(presets) {
2585
+ var _a;
2586
+ try {
2587
+ if (typeof localStorage !== "undefined") {
2588
+ localStorage.setItem(
2589
+ this.backgroundPresetStorageKey,
2590
+ JSON.stringify(presets)
2591
+ );
2592
+ }
2593
+ } catch {
2594
+ }
2595
+ (_a = this.onBackgroundPresetsChange) == null ? void 0 : _a.call(this, presets);
2596
+ }
2597
+ /**
2598
+ * Save the current background-layer visibility as a named preset. Reusing an
2599
+ * existing name overwrites that preset.
2600
+ *
2601
+ * @param name - Preset name; whitespace is trimmed. Empty names are ignored.
2602
+ * @returns The updated preset map.
2603
+ */
2604
+ saveBackgroundPreset(name) {
2605
+ const trimmed = name.trim();
2606
+ const presets = this.getBackgroundPresets();
2607
+ if (!trimmed || this.isUnsafePresetKey(trimmed)) return presets;
2608
+ presets[trimmed] = this.getBackgroundLayerVisibility();
2609
+ this.writeBackgroundPresets(presets);
2610
+ return presets;
2611
+ }
2612
+ /**
2613
+ * Apply a previously saved preset to the map.
2614
+ *
2615
+ * @param name - The preset name to apply.
2616
+ * @returns `true` if a preset with that name existed and was applied.
2617
+ */
2618
+ applyBackgroundPreset(name) {
2619
+ const presets = this.getBackgroundPresets();
2620
+ if (!Object.prototype.hasOwnProperty.call(presets, name)) return false;
2621
+ this.applyBackgroundLayerVisibility(presets[name]);
2622
+ return true;
2623
+ }
2624
+ /**
2625
+ * Delete a saved preset.
2626
+ *
2627
+ * @param name - The preset name to remove.
2628
+ * @returns The updated preset map.
2629
+ */
2630
+ deleteBackgroundPreset(name) {
2631
+ const presets = this.getBackgroundPresets();
2632
+ if (Object.prototype.hasOwnProperty.call(presets, name)) {
2633
+ delete presets[name];
2634
+ this.writeBackgroundPresets(presets);
2635
+ }
2636
+ return presets;
2637
+ }
2386
2638
  /**
2387
2639
  * Create style button for a layer
2388
2640
  */