maplibre-gl-layer-control 0.16.0 → 0.17.1

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
@@ -14,7 +14,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
14
14
  - ✅ **Layer visibility toggle** - Checkbox control for each layer
15
15
  - ✅ **Layer opacity control** - Smooth opacity slider with type-aware property mapping; **double-click the slider to type an exact percentage (0-100%)**
16
16
  - ✅ **Layer symbols** - Visual type indicators (colored shapes) next to layer names, auto-detected from layer paint properties
17
- - ✅ **Resizable panel** - **Drag either edge of the panel to resize it**, plus a width slider and keyboard support
17
+ - ✅ **Resizable panel** - **Drag either edge of the panel to resize it**; double-click an edge to reset to the default width. The panel also grows to fill the available height so long layer lists only scroll once they exceed the map.
18
18
  - ✅ **Advanced style editor** - Per-layer-type styling controls:
19
19
  - **Fill layers**: color, opacity, outline-color
20
20
  - **Line layers**: color, width, opacity, blur
@@ -73,7 +73,7 @@ map.on('load', () => {
73
73
  layers: ['my-layer'], // LayerControl auto-detects opacity, visibility, and generates friendly names
74
74
  panelWidth: 340,
75
75
  panelMinWidth: 240,
76
- panelMaxWidth: 450
76
+ panelMaxWidth: 960
77
77
  });
78
78
 
79
79
  // Option 2: Auto-detect with basemapStyleUrl (recommended for reliable basemap detection)
@@ -93,7 +93,7 @@ map.on('load', () => {
93
93
  // collapsed: false,
94
94
  // panelWidth: 340,
95
95
  // panelMinWidth: 240,
96
- // panelMaxWidth: 450
96
+ // panelMaxWidth: 960
97
97
  // });
98
98
 
99
99
  // Option 4: Manually specify layer states (for full control over names)
@@ -167,8 +167,8 @@ function MapComponent() {
167
167
  | `layerStates` | `Record<string, LayerState>` | `undefined` | Manual layer state configuration |
168
168
  | `panelWidth` | `number` | `320` | Initial panel width in pixels |
169
169
  | `panelMinWidth` | `number` | `240` | Minimum panel width |
170
- | `panelMaxWidth` | `number` | `420` | Maximum panel width |
171
- | `panelMaxHeight` | `number` | `600` | Maximum panel height (scrollable when exceeded) |
170
+ | `panelMaxWidth` | `number` | `960` | Maximum panel width |
171
+ | `panelMaxHeight` | `number` | `undefined` | Maximum panel height in pixels. Omit to fill the available vertical space (scrollable only when the layer list is taller than the map) |
172
172
  | `showStyleEditor` | `boolean` | `true` | Show gear icon for style editor |
173
173
  | `showOpacitySlider` | `boolean` | `true` | Show opacity slider for layers |
174
174
  | `showLayerSymbol` | `boolean` | `true` | Show layer type symbols (colored icons) next to layer names |
package/dist/index.cjs CHANGED
@@ -691,6 +691,7 @@ class LayerControl {
691
691
  __publicField(this, "minPanelWidth");
692
692
  __publicField(this, "maxPanelWidth");
693
693
  __publicField(this, "initialPanelWidth");
694
+ /** Explicit max panel height; null means fill available vertical space */
694
695
  __publicField(this, "maxPanelHeight");
695
696
  __publicField(this, "showStyleEditor");
696
697
  __publicField(this, "showOpacitySlider");
@@ -703,13 +704,6 @@ class LayerControl {
703
704
  __publicField(this, "nativeLayerGroups", /* @__PURE__ */ new Map());
704
705
  __publicField(this, "basemapStyleUrl", null);
705
706
  __publicField(this, "basemapLayerIds", null);
706
- __publicField(this, "widthSliderEl", null);
707
- __publicField(this, "widthThumbEl", null);
708
- __publicField(this, "widthValueEl", null);
709
- __publicField(this, "isWidthSliderActive", false);
710
- __publicField(this, "widthDragRectWidth", null);
711
- __publicField(this, "widthDragStartX", null);
712
- __publicField(this, "widthDragStartWidth", null);
713
707
  __publicField(this, "widthFrame", null);
714
708
  // Exact-opacity input popup
715
709
  __publicField(this, "opacityInputEl", null);
@@ -735,9 +729,9 @@ class LayerControl {
735
729
  __publicField(this, "backgroundPresetStorageKey");
736
730
  __publicField(this, "onBackgroundPresetsChange");
737
731
  this.minPanelWidth = options.panelMinWidth || 240;
738
- this.maxPanelWidth = options.panelMaxWidth || 420;
732
+ this.maxPanelWidth = options.panelMaxWidth || 960;
739
733
  this.initialPanelWidth = options.panelWidth || 350;
740
- this.maxPanelHeight = options.panelMaxHeight || 600;
734
+ this.maxPanelHeight = options.panelMaxHeight ?? null;
741
735
  this.showStyleEditor = options.showStyleEditor !== false;
742
736
  this.showOpacitySlider = options.showOpacitySlider !== false;
743
737
  this.showLayerSymbol = options.showLayerSymbol !== false;
@@ -818,7 +812,6 @@ class LayerControl {
818
812
  this.contextMenuEl = this.createContextMenu();
819
813
  this.mapContainer.appendChild(this.contextMenuEl);
820
814
  }
821
- this.updateWidthDisplay();
822
815
  this.setupEventListeners();
823
816
  if (this.basemapStyleUrl && !this.basemapLayerIds) {
824
817
  this.fetchBasemapStyle().then(() => {
@@ -1218,7 +1211,9 @@ class LayerControl {
1218
1211
  const panel = document.createElement("div");
1219
1212
  panel.className = "layer-control-panel";
1220
1213
  panel.style.width = `${this.state.panelWidth}px`;
1221
- panel.style.maxHeight = `${this.maxPanelHeight}px`;
1214
+ if (this.maxPanelHeight != null) {
1215
+ panel.style.maxHeight = `${this.maxPanelHeight}px`;
1216
+ }
1222
1217
  if (!this.state.collapsed) {
1223
1218
  panel.classList.add("expanded");
1224
1219
  }
@@ -1385,6 +1380,15 @@ class LayerControl {
1385
1380
  this.panel.style.right = `${buttonRight}px`;
1386
1381
  break;
1387
1382
  }
1383
+ const edgeMargin = 8;
1384
+ const isTopAnchored = position === "top-left" || position === "top-right";
1385
+ const occupiedFromEdge = isTopAnchored ? buttonTop + buttonRect.height + panelGap : buttonBottom + buttonRect.height + panelGap;
1386
+ const availableHeight = Math.max(
1387
+ 120,
1388
+ Math.round(mapRect.height - occupiedFromEdge - edgeMargin)
1389
+ );
1390
+ const maxHeight = this.maxPanelHeight != null ? Math.min(this.maxPanelHeight, availableHeight) : availableHeight;
1391
+ this.panel.style.maxHeight = `${maxHeight}px`;
1388
1392
  }
1389
1393
  /**
1390
1394
  * Create action buttons for Show All / Hide All
@@ -1433,7 +1437,8 @@ class LayerControl {
1433
1437
  });
1434
1438
  }
1435
1439
  /**
1436
- * Create the panel header with title and width control
1440
+ * Create the panel header with the title. Panel width is adjusted by
1441
+ * dragging either edge of the panel (see createResizeHandle).
1437
1442
  */
1438
1443
  createPanelHeader() {
1439
1444
  const header = document.createElement("div");
@@ -1442,135 +1447,8 @@ class LayerControl {
1442
1447
  title.className = "layer-control-panel-title";
1443
1448
  title.textContent = "Layers";
1444
1449
  header.appendChild(title);
1445
- const widthControl = this.createWidthControl();
1446
- header.appendChild(widthControl);
1447
1450
  return header;
1448
1451
  }
1449
- /**
1450
- * Create the width control slider
1451
- */
1452
- createWidthControl() {
1453
- const widthControl = document.createElement("label");
1454
- widthControl.className = "layer-control-width-control";
1455
- widthControl.title = "Adjust layer panel width";
1456
- const widthLabel = document.createElement("span");
1457
- widthLabel.textContent = "Width";
1458
- widthControl.appendChild(widthLabel);
1459
- const widthSlider = document.createElement("div");
1460
- widthSlider.className = "layer-control-width-slider";
1461
- widthSlider.setAttribute("role", "slider");
1462
- widthSlider.setAttribute("aria-valuemin", String(this.minPanelWidth));
1463
- widthSlider.setAttribute("aria-valuemax", String(this.maxPanelWidth));
1464
- widthSlider.setAttribute("aria-valuenow", String(this.state.panelWidth));
1465
- widthSlider.setAttribute("aria-valuestep", "10");
1466
- widthSlider.setAttribute("aria-label", "Layer panel width");
1467
- widthSlider.tabIndex = 0;
1468
- const widthTrack = document.createElement("div");
1469
- widthTrack.className = "layer-control-width-track";
1470
- const widthThumb = document.createElement("div");
1471
- widthThumb.className = "layer-control-width-thumb";
1472
- widthSlider.appendChild(widthTrack);
1473
- widthSlider.appendChild(widthThumb);
1474
- this.widthSliderEl = widthSlider;
1475
- this.widthThumbEl = widthThumb;
1476
- const widthValue = document.createElement("span");
1477
- widthValue.className = "layer-control-width-value";
1478
- this.widthValueEl = widthValue;
1479
- widthControl.appendChild(widthSlider);
1480
- widthControl.appendChild(widthValue);
1481
- this.updateWidthDisplay();
1482
- this.setupWidthSliderEvents(widthSlider);
1483
- return widthControl;
1484
- }
1485
- /**
1486
- * Setup event listeners for width slider
1487
- */
1488
- setupWidthSliderEvents(widthSlider) {
1489
- widthSlider.addEventListener("pointerdown", (event) => {
1490
- event.preventDefault();
1491
- const rect = widthSlider.getBoundingClientRect();
1492
- this.widthDragRectWidth = rect.width || 1;
1493
- this.widthDragStartX = event.clientX;
1494
- this.widthDragStartWidth = this.state.panelWidth;
1495
- this.isWidthSliderActive = true;
1496
- widthSlider.setPointerCapture(event.pointerId);
1497
- this.updateWidthFromPointer(event, true);
1498
- });
1499
- widthSlider.addEventListener("pointermove", (event) => {
1500
- if (!this.isWidthSliderActive) return;
1501
- this.updateWidthFromPointer(event);
1502
- });
1503
- const endPointerDrag = (event) => {
1504
- if (!this.isWidthSliderActive) return;
1505
- if (event.pointerId !== void 0) {
1506
- try {
1507
- widthSlider.releasePointerCapture(event.pointerId);
1508
- } catch (error) {
1509
- }
1510
- }
1511
- this.isWidthSliderActive = false;
1512
- this.widthDragRectWidth = null;
1513
- this.widthDragStartX = null;
1514
- this.widthDragStartWidth = null;
1515
- this.updateWidthDisplay();
1516
- };
1517
- widthSlider.addEventListener("pointerup", endPointerDrag);
1518
- widthSlider.addEventListener("pointercancel", endPointerDrag);
1519
- widthSlider.addEventListener("lostpointercapture", endPointerDrag);
1520
- widthSlider.addEventListener("keydown", (event) => {
1521
- let handled = true;
1522
- const step = event.shiftKey ? 20 : 10;
1523
- switch (event.key) {
1524
- case "ArrowLeft":
1525
- case "ArrowDown":
1526
- this.applyPanelWidth(this.state.panelWidth - step, true);
1527
- break;
1528
- case "ArrowRight":
1529
- case "ArrowUp":
1530
- this.applyPanelWidth(this.state.panelWidth + step, true);
1531
- break;
1532
- case "Home":
1533
- this.applyPanelWidth(this.minPanelWidth, true);
1534
- break;
1535
- case "End":
1536
- this.applyPanelWidth(this.maxPanelWidth, true);
1537
- break;
1538
- case "PageUp":
1539
- this.applyPanelWidth(this.state.panelWidth + 50, true);
1540
- break;
1541
- case "PageDown":
1542
- this.applyPanelWidth(this.state.panelWidth - 50, true);
1543
- break;
1544
- default:
1545
- handled = false;
1546
- }
1547
- if (handled) {
1548
- event.preventDefault();
1549
- this.updateWidthDisplay();
1550
- }
1551
- });
1552
- }
1553
- /**
1554
- * Update panel width from pointer event
1555
- */
1556
- updateWidthFromPointer(event, resetBaseline = false) {
1557
- if (!this.widthSliderEl) return;
1558
- const sliderWidth = this.widthDragRectWidth || this.widthSliderEl.getBoundingClientRect().width || 1;
1559
- const widthRange = this.maxPanelWidth - this.minPanelWidth;
1560
- let width;
1561
- if (resetBaseline) {
1562
- const rect = this.widthSliderEl.getBoundingClientRect();
1563
- const relative = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0;
1564
- const clampedRatio = Math.min(1, Math.max(0, relative));
1565
- width = this.minPanelWidth + clampedRatio * widthRange;
1566
- this.widthDragStartWidth = width;
1567
- this.widthDragStartX = event.clientX;
1568
- } else {
1569
- const delta = event.clientX - (this.widthDragStartX || event.clientX);
1570
- width = (this.widthDragStartWidth || this.state.panelWidth) + delta / sliderWidth * widthRange;
1571
- }
1572
- this.applyPanelWidth(width, this.isWidthSliderActive);
1573
- }
1574
1452
  /**
1575
1453
  * Apply panel width (clamped to min/max)
1576
1454
  */
@@ -1582,7 +1460,6 @@ class LayerControl {
1582
1460
  this.state.panelWidth = clamped;
1583
1461
  const px = `${clamped}px`;
1584
1462
  this.panel.style.width = px;
1585
- this.updateWidthDisplay();
1586
1463
  };
1587
1464
  if (immediate) {
1588
1465
  applyWidth();
@@ -1596,34 +1473,6 @@ class LayerControl {
1596
1473
  this.widthFrame = null;
1597
1474
  });
1598
1475
  }
1599
- /**
1600
- * Update width display (value label and thumb position)
1601
- */
1602
- updateWidthDisplay() {
1603
- if (this.widthValueEl) {
1604
- this.widthValueEl.textContent = `${this.state.panelWidth}px`;
1605
- }
1606
- if (this.widthSliderEl) {
1607
- this.widthSliderEl.setAttribute(
1608
- "aria-valuenow",
1609
- String(this.state.panelWidth)
1610
- );
1611
- const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
1612
- if (this.widthThumbEl) {
1613
- const sliderWidth = this.widthSliderEl.clientWidth;
1614
- if (sliderWidth === 0) {
1615
- requestAnimationFrame(() => this.updateWidthDisplay());
1616
- return;
1617
- }
1618
- const thumbWidth = this.widthThumbEl.offsetWidth || 14;
1619
- const padding = 16;
1620
- const available = Math.max(0, sliderWidth - padding - thumbWidth);
1621
- const clampedRatio = Math.min(1, Math.max(0, ratio));
1622
- const leftPx = 8 + available * clampedRatio;
1623
- this.widthThumbEl.style.left = `${leftPx}px`;
1624
- }
1625
- }
1626
- }
1627
1476
  /**
1628
1477
  * Setup main event listeners
1629
1478
  */
@@ -1722,7 +1571,21 @@ class LayerControl {
1722
1571
  const existingItems = this.panel.querySelectorAll(".layer-control-item");
1723
1572
  existingItems.forEach((item) => item.remove());
1724
1573
  this.styleEditors.clear();
1725
- Object.entries(this.state.layerStates).forEach(([layerId, state]) => {
1574
+ const orderedLayerIds = this.getUserLayerIdsInMapOrder();
1575
+ const captured = new Set(orderedLayerIds);
1576
+ for (const layerId of Object.keys(this.state.layerStates)) {
1577
+ if (layerId !== "Background" && !captured.has(layerId)) {
1578
+ orderedLayerIds.push(layerId);
1579
+ }
1580
+ }
1581
+ if (this.state.layerStates["Background"]) {
1582
+ orderedLayerIds.push("Background");
1583
+ }
1584
+ orderedLayerIds.forEach((layerId) => {
1585
+ const state = this.state.layerStates[layerId];
1586
+ if (!state) {
1587
+ return;
1588
+ }
1726
1589
  if (this.targetLayers.length === 0 || this.targetLayers.includes(layerId)) {
1727
1590
  this.addLayerItem(layerId, state);
1728
1591
  }
@@ -2503,7 +2366,8 @@ class LayerControl {
2503
2366
  const styleLayers = this.map.getStyle().layers || [];
2504
2367
  return styleLayers.filter((layer) => {
2505
2368
  if (this.isUserAddedLayer(layer.id)) return false;
2506
- if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id)) return false;
2369
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id))
2370
+ return false;
2507
2371
  if (this.isExcludedByPattern(layer.id)) return false;
2508
2372
  return true;
2509
2373
  }).map((layer) => layer.id);
@@ -3879,22 +3743,33 @@ class LayerControl {
3879
3743
  const style = this.map.getStyle();
3880
3744
  if (!(style == null ? void 0 : style.layers)) return [];
3881
3745
  const mapLayerIds = style.layers.map((l) => l.id);
3746
+ const indexById = new Map(mapLayerIds.map((id, i) => [id, i]));
3882
3747
  const userLayerIds = Object.keys(this.state.layerStates).filter(
3883
3748
  (id) => id !== "Background"
3884
3749
  );
3885
- const mapLibreLayers = userLayerIds.filter(
3886
- (id) => mapLayerIds.includes(id)
3887
- );
3888
- const customLayers = userLayerIds.filter(
3889
- (id) => {
3890
- var _a;
3891
- return ((_a = this.state.layerStates[id]) == null ? void 0 : _a.isCustomLayer) && !mapLayerIds.includes(id);
3750
+ const mapIndexOf = (id) => {
3751
+ var _a;
3752
+ const direct = indexById.get(id);
3753
+ if (direct !== void 0) return direct;
3754
+ const nativeIds = (_a = this.customLayerRegistry) == null ? void 0 : _a.getNativeLayerIds(id);
3755
+ if (nativeIds && nativeIds.length > 0) {
3756
+ let best = -1;
3757
+ for (const nativeId of nativeIds) {
3758
+ const idx = indexById.get(nativeId);
3759
+ if (idx !== void 0 && idx > best) best = idx;
3760
+ }
3761
+ if (best !== -1) return best;
3892
3762
  }
3893
- );
3894
- const sortedMapLibreLayers = mapLibreLayers.sort(
3895
- (a, b) => mapLayerIds.indexOf(b) - mapLayerIds.indexOf(a)
3896
- );
3897
- return [...customLayers, ...sortedMapLibreLayers];
3763
+ return Number.POSITIVE_INFINITY;
3764
+ };
3765
+ return userLayerIds.map((id, insertionIndex) => ({
3766
+ id,
3767
+ insertionIndex,
3768
+ mapIndex: mapIndexOf(id)
3769
+ })).sort((a, b) => {
3770
+ if (a.mapIndex !== b.mapIndex) return a.mapIndex > b.mapIndex ? -1 : 1;
3771
+ return a.insertionIndex - b.insertionIndex;
3772
+ }).map((entry) => entry.id);
3898
3773
  }
3899
3774
  /**
3900
3775
  * Check if a layer is a MapLibre layer (not a custom layer)
@@ -4320,18 +4195,17 @@ class LayerControl {
4320
4195
  uiLayerIds.push(layerId);
4321
4196
  }
4322
4197
  });
4323
- const reversedIds = [...uiLayerIds].reverse();
4324
4198
  const style = this.map.getStyle();
4325
4199
  const mapLibreLayerIds = new Set(((_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.map((l) => l.id)) || []);
4326
- for (let i = 0; i < reversedIds.length; i++) {
4327
- const layerId = reversedIds[i];
4200
+ for (let i = 0; i < uiLayerIds.length; i++) {
4201
+ const layerId = uiLayerIds[i];
4328
4202
  if (!mapLibreLayerIds.has(layerId)) {
4329
4203
  continue;
4330
4204
  }
4331
4205
  let beforeId = void 0;
4332
4206
  for (let j = i - 1; j >= 0; j--) {
4333
- if (mapLibreLayerIds.has(reversedIds[j])) {
4334
- beforeId = reversedIds[j];
4207
+ if (mapLibreLayerIds.has(uiLayerIds[j])) {
4208
+ beforeId = uiLayerIds[j];
4335
4209
  break;
4336
4210
  }
4337
4211
  }