maplibre-gl-layer-control 0.1.0 → 0.2.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
@@ -16,6 +16,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
16
16
  - **Raster layers**: opacity, brightness, saturation, contrast, hue-rotate
17
17
  - ✅ **Dynamic layer detection** - Automatically detect and manage new layers
18
18
  - ✅ **Background layer grouping** - Control all basemap layers as one group
19
+ - ✅ **Background layer legend** - Gear icon to toggle individual background layer visibility
19
20
  - ✅ **Accessibility** - Full ARIA support and keyboard navigation
20
21
  - ✅ **TypeScript** - Full type safety and IntelliSense support
21
22
  - ✅ **React integration** - Optional React components and hooks
@@ -165,8 +166,21 @@ See the [examples](./examples) folder for complete working examples:
165
166
 
166
167
  - **[basic](./examples/basic)** - Simple vanilla JavaScript example
167
168
  - **[full-demo](./examples/full-demo)** - Full demo with multiple layer types
169
+ - **[background-legend](./examples/background-legend)** - Background layer visibility control
168
170
  - **[react](./examples/react)** - React integration example
169
171
 
172
+ ### Background Layer Legend
173
+
174
+ When using the `layers` option to specify specific layers, all other layers are grouped under a "Background" entry. The Background layer includes a **gear icon** that opens a detailed legend panel showing:
175
+
176
+ - Individual visibility toggles for each background layer
177
+ - Layer type indicators (fill, line, symbol, etc.)
178
+ - Quick "Show All" / "Hide All" buttons
179
+ - **"Only rendered" filter** - Shows only layers that are currently rendered in the map viewport
180
+ - Indeterminate checkbox state when some layers are hidden
181
+
182
+ This allows fine-grained control over which basemap layers are visible while maintaining a simplified layer control interface.
183
+
170
184
  ## Development
171
185
 
172
186
  ```bash
package/dist/index.cjs CHANGED
@@ -197,7 +197,10 @@ class LayerControl {
197
197
  activeStyleEditor: null,
198
198
  layerStates: options.layerStates || {},
199
199
  originalStyles: /* @__PURE__ */ new Map(),
200
- userInteractingWithSlider: false
200
+ userInteractingWithSlider: false,
201
+ backgroundLegendOpen: false,
202
+ backgroundLayerVisibility: /* @__PURE__ */ new Map(),
203
+ onlyRenderedFilter: false
201
204
  };
202
205
  this.targetLayers = options.layers || Object.keys(this.state.layerStates);
203
206
  this.styleEditors = /* @__PURE__ */ new Map();
@@ -629,7 +632,10 @@ class LayerControl {
629
632
  row.appendChild(checkbox);
630
633
  row.appendChild(name);
631
634
  row.appendChild(opacity);
632
- if (layerId !== "Background") {
635
+ if (layerId === "Background") {
636
+ const legendButton = this.createBackgroundLegendButton();
637
+ row.appendChild(legendButton);
638
+ } else {
633
639
  const styleButton = this.createStyleButton(layerId);
634
640
  if (styleButton) {
635
641
  row.appendChild(styleButton);
@@ -683,9 +689,19 @@ class LayerControl {
683
689
  const styleLayers = this.map.getStyle().layers || [];
684
690
  styleLayers.forEach((layer) => {
685
691
  if (!this.isUserAddedLayer(layer.id)) {
692
+ this.state.backgroundLayerVisibility.set(layer.id, visible);
686
693
  this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
687
694
  }
688
695
  });
696
+ if (this.state.backgroundLegendOpen) {
697
+ const legendPanel = this.panel.querySelector(".layer-control-background-legend");
698
+ if (legendPanel) {
699
+ const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
700
+ checkboxes.forEach((checkbox) => {
701
+ checkbox.checked = visible;
702
+ });
703
+ }
704
+ }
689
705
  }
690
706
  /**
691
707
  * Change opacity for all background layers (basemap layers)
@@ -704,6 +720,260 @@ class LayerControl {
704
720
  }
705
721
  });
706
722
  }
723
+ // ===== Background Legend Methods =====
724
+ /**
725
+ * Create legend button for Background layer
726
+ */
727
+ createBackgroundLegendButton() {
728
+ const button = document.createElement("button");
729
+ button.className = "layer-control-style-button layer-control-background-legend-button";
730
+ button.innerHTML = "⚙";
731
+ button.title = "Show background layer details";
732
+ button.setAttribute("aria-label", "Show background layer visibility controls");
733
+ button.setAttribute("aria-expanded", String(this.state.backgroundLegendOpen));
734
+ button.addEventListener("click", (e) => {
735
+ e.stopPropagation();
736
+ this.toggleBackgroundLegend();
737
+ });
738
+ return button;
739
+ }
740
+ /**
741
+ * Toggle background legend panel visibility
742
+ */
743
+ toggleBackgroundLegend() {
744
+ if (this.state.backgroundLegendOpen) {
745
+ this.closeBackgroundLegend();
746
+ } else {
747
+ this.openBackgroundLegend();
748
+ }
749
+ }
750
+ /**
751
+ * Open background legend panel
752
+ */
753
+ openBackgroundLegend() {
754
+ if (this.state.activeStyleEditor) {
755
+ this.closeStyleEditor(this.state.activeStyleEditor);
756
+ }
757
+ const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
758
+ if (!itemEl) return;
759
+ let legendPanel = itemEl.querySelector(".layer-control-background-legend");
760
+ if (legendPanel) {
761
+ const layerList = legendPanel.querySelector(".background-legend-layer-list");
762
+ if (layerList) {
763
+ this.populateBackgroundLayerList(layerList);
764
+ }
765
+ } else {
766
+ legendPanel = this.createBackgroundLegendPanel();
767
+ itemEl.appendChild(legendPanel);
768
+ }
769
+ this.state.backgroundLegendOpen = true;
770
+ const button = itemEl.querySelector(".layer-control-background-legend-button");
771
+ if (button) {
772
+ button.setAttribute("aria-expanded", "true");
773
+ button.classList.add("active");
774
+ }
775
+ setTimeout(() => {
776
+ legendPanel == null ? void 0 : legendPanel.scrollIntoView({ behavior: "smooth", block: "nearest" });
777
+ }, 50);
778
+ }
779
+ /**
780
+ * Close background legend panel
781
+ */
782
+ closeBackgroundLegend() {
783
+ const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
784
+ if (!itemEl) return;
785
+ const legendPanel = itemEl.querySelector(".layer-control-background-legend");
786
+ if (legendPanel) {
787
+ legendPanel.remove();
788
+ }
789
+ this.state.backgroundLegendOpen = false;
790
+ const button = itemEl.querySelector(".layer-control-background-legend-button");
791
+ if (button) {
792
+ button.setAttribute("aria-expanded", "false");
793
+ button.classList.remove("active");
794
+ }
795
+ }
796
+ /**
797
+ * Create the background legend panel with individual layer controls
798
+ */
799
+ createBackgroundLegendPanel() {
800
+ const panel = document.createElement("div");
801
+ panel.className = "layer-control-background-legend";
802
+ const header = document.createElement("div");
803
+ header.className = "background-legend-header";
804
+ const title = document.createElement("span");
805
+ title.className = "background-legend-title";
806
+ title.textContent = "Background Layers";
807
+ const closeBtn = document.createElement("button");
808
+ closeBtn.className = "background-legend-close";
809
+ closeBtn.innerHTML = "×";
810
+ closeBtn.title = "Close";
811
+ closeBtn.addEventListener("click", (e) => {
812
+ e.stopPropagation();
813
+ this.closeBackgroundLegend();
814
+ });
815
+ header.appendChild(title);
816
+ header.appendChild(closeBtn);
817
+ const actionsRow = document.createElement("div");
818
+ actionsRow.className = "background-legend-actions";
819
+ const showAllBtn = document.createElement("button");
820
+ showAllBtn.className = "background-legend-action-btn";
821
+ showAllBtn.textContent = "Show All";
822
+ showAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(true));
823
+ const hideAllBtn = document.createElement("button");
824
+ hideAllBtn.className = "background-legend-action-btn";
825
+ hideAllBtn.textContent = "Hide All";
826
+ hideAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(false));
827
+ actionsRow.appendChild(showAllBtn);
828
+ actionsRow.appendChild(hideAllBtn);
829
+ const filterRow = document.createElement("div");
830
+ filterRow.className = "background-legend-filter";
831
+ const filterCheckbox = document.createElement("input");
832
+ filterCheckbox.type = "checkbox";
833
+ filterCheckbox.className = "background-legend-filter-checkbox";
834
+ filterCheckbox.id = "background-legend-only-rendered";
835
+ filterCheckbox.checked = this.state.onlyRenderedFilter;
836
+ filterCheckbox.addEventListener("change", () => {
837
+ this.state.onlyRenderedFilter = filterCheckbox.checked;
838
+ const layerList2 = panel.querySelector(".background-legend-layer-list");
839
+ if (layerList2) {
840
+ this.populateBackgroundLayerList(layerList2);
841
+ }
842
+ });
843
+ const filterLabel = document.createElement("label");
844
+ filterLabel.className = "background-legend-filter-label";
845
+ filterLabel.htmlFor = "background-legend-only-rendered";
846
+ filterLabel.textContent = "Only rendered";
847
+ filterRow.appendChild(filterCheckbox);
848
+ filterRow.appendChild(filterLabel);
849
+ const layerList = document.createElement("div");
850
+ layerList.className = "background-legend-layer-list";
851
+ this.populateBackgroundLayerList(layerList);
852
+ panel.appendChild(header);
853
+ panel.appendChild(actionsRow);
854
+ panel.appendChild(filterRow);
855
+ panel.appendChild(layerList);
856
+ return panel;
857
+ }
858
+ /**
859
+ * Check if a layer is currently rendered in the map viewport
860
+ */
861
+ isLayerRendered(layerId) {
862
+ try {
863
+ const layer = this.map.getLayer(layerId);
864
+ if (!layer) return false;
865
+ const visibility = this.map.getLayoutProperty(layerId, "visibility");
866
+ if (visibility === "none") return false;
867
+ if (layer.type === "raster" || layer.type === "hillshade") {
868
+ return true;
869
+ }
870
+ if (layer.type === "background") {
871
+ return true;
872
+ }
873
+ const features = this.map.queryRenderedFeatures({ layers: [layerId] });
874
+ return features.length > 0;
875
+ } catch (error) {
876
+ return true;
877
+ }
878
+ }
879
+ /**
880
+ * Populate the background layer list with individual layers
881
+ */
882
+ populateBackgroundLayerList(container) {
883
+ container.innerHTML = "";
884
+ const styleLayers = this.map.getStyle().layers || [];
885
+ styleLayers.forEach((layer) => {
886
+ if (!this.isUserAddedLayer(layer.id)) {
887
+ if (this.state.onlyRenderedFilter && !this.isLayerRendered(layer.id)) {
888
+ return;
889
+ }
890
+ const layerRow = document.createElement("div");
891
+ layerRow.className = "background-legend-layer-row";
892
+ layerRow.setAttribute("data-background-layer-id", layer.id);
893
+ const checkbox = document.createElement("input");
894
+ checkbox.type = "checkbox";
895
+ checkbox.className = "background-legend-checkbox";
896
+ const visibility = this.map.getLayoutProperty(layer.id, "visibility");
897
+ const isVisible = visibility !== "none";
898
+ checkbox.checked = isVisible;
899
+ this.state.backgroundLayerVisibility.set(layer.id, isVisible);
900
+ checkbox.addEventListener("change", () => {
901
+ this.toggleIndividualBackgroundLayer(layer.id, checkbox.checked);
902
+ });
903
+ const name = document.createElement("span");
904
+ name.className = "background-legend-layer-name";
905
+ name.textContent = this.generateFriendlyName(layer.id);
906
+ name.title = layer.id;
907
+ const typeIndicator = document.createElement("span");
908
+ typeIndicator.className = "background-legend-layer-type";
909
+ typeIndicator.textContent = layer.type;
910
+ layerRow.appendChild(checkbox);
911
+ layerRow.appendChild(name);
912
+ layerRow.appendChild(typeIndicator);
913
+ container.appendChild(layerRow);
914
+ }
915
+ });
916
+ if (container.children.length === 0) {
917
+ const emptyMsg = document.createElement("p");
918
+ emptyMsg.className = "background-legend-empty";
919
+ emptyMsg.textContent = this.state.onlyRenderedFilter ? "No rendered layers in current view." : "No background layers found.";
920
+ container.appendChild(emptyMsg);
921
+ }
922
+ }
923
+ /**
924
+ * Toggle visibility of an individual background layer
925
+ */
926
+ toggleIndividualBackgroundLayer(layerId, visible) {
927
+ this.state.backgroundLayerVisibility.set(layerId, visible);
928
+ this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
929
+ this.updateBackgroundCheckboxState();
930
+ }
931
+ /**
932
+ * Set visibility for all background layers
933
+ */
934
+ setAllBackgroundLayersVisibility(visible) {
935
+ const styleLayers = this.map.getStyle().layers || [];
936
+ styleLayers.forEach((layer) => {
937
+ if (!this.isUserAddedLayer(layer.id)) {
938
+ this.state.backgroundLayerVisibility.set(layer.id, visible);
939
+ this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
940
+ }
941
+ });
942
+ const legendPanel = this.panel.querySelector(".layer-control-background-legend");
943
+ if (legendPanel) {
944
+ const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
945
+ checkboxes.forEach((checkbox) => {
946
+ checkbox.checked = visible;
947
+ });
948
+ }
949
+ this.updateBackgroundCheckboxState();
950
+ }
951
+ /**
952
+ * Update the main Background checkbox based on individual layer states
953
+ */
954
+ updateBackgroundCheckboxState() {
955
+ const styleLayers = this.map.getStyle().layers || [];
956
+ let anyVisible = false;
957
+ let allVisible = true;
958
+ styleLayers.forEach((layer) => {
959
+ if (!this.isUserAddedLayer(layer.id)) {
960
+ const visible = this.state.backgroundLayerVisibility.get(layer.id);
961
+ if (visible === true) anyVisible = true;
962
+ if (visible === false) allVisible = false;
963
+ }
964
+ });
965
+ const backgroundItem = this.panel.querySelector('[data-layer-id="Background"]');
966
+ if (backgroundItem) {
967
+ const checkbox = backgroundItem.querySelector(".layer-control-checkbox");
968
+ if (checkbox) {
969
+ checkbox.checked = anyVisible;
970
+ checkbox.indeterminate = anyVisible && !allVisible;
971
+ }
972
+ }
973
+ if (this.state.layerStates["Background"]) {
974
+ this.state.layerStates["Background"].visible = anyVisible;
975
+ }
976
+ }
707
977
  /**
708
978
  * Create style button for a layer
709
979
  */