ol-layer-control 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the version is below 1.0.0, a minor version bump may contain breaking changes.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-28
10
+
11
+ First public release.
12
+
13
+ ### Added
14
+
15
+ - `LayerControl`, an OpenLayers control: a map button that opens a panel docked to the left or right of the map. The map shrinks while the panel is open, so the panel never covers it.
16
+ - Five map button positions (`buttonPosition`) that stay clear of the default Zoom, Rotate and Attribution controls. The default follows the panel's side.
17
+ - Resizable panel width, with configurable minimum and maximum.
18
+ - Layer tree with nested, foldable groups. Basemaps (`type: 'base'`) and groups with `exclusive: true` show radio buttons; other groups show checkboxes.
19
+ - Right-click on a group title to switch it between exclusive and non-exclusive (`exclusiveToggle`).
20
+ - Greyed-out entries, with a tooltip, for layers outside their resolution or zoom range and for layers inside a switched-off group.
21
+ - Optional opacity slider per layer (`opacitySlider`).
22
+ - Optional search box that filters the layer tree (`search`), toggled by clicking the panel title.
23
+ - `displayInLayerControl: false` hides a layer or group from the panel.
24
+ - English UI strings, all replaceable through the `i18n` option or `setI18n()`.
25
+ - Theming through CSS custom properties.
26
+ - TypeScript declarations generated from the JSDoc.
27
+
28
+ [Unreleased]: https://github.com/TWIAV/ol-layer-control/compare/v0.1.0...HEAD
29
+ [0.1.0]: https://github.com/TWIAV/ol-layer-control/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TWIAV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "ol-layer-control",
3
+ "version": "0.1.0",
4
+ "description": "A layer control for OpenLayers: a map button that opens a docked, resizable side panel to switch layers and layer groups on and off.",
5
+ "keywords": [
6
+ "openlayers",
7
+ "ol",
8
+ "layer-control",
9
+ "layer-switcher",
10
+ "layerswitcher",
11
+ "map",
12
+ "gis",
13
+ "control"
14
+ ],
15
+ "homepage": "https://github.com/TWIAV/ol-layer-control#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/TWIAV/ol-layer-control/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/TWIAV/ol-layer-control.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "TWIAV",
25
+ "type": "module",
26
+ "main": "./src/ol-layer-control.js",
27
+ "types": "./types/ol-layer-control.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./types/ol-layer-control.d.ts",
31
+ "default": "./src/ol-layer-control.js"
32
+ },
33
+ "./ol-layer-control.css": "./src/ol-layer-control.css",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "src",
38
+ "types",
39
+ "CHANGELOG.md"
40
+ ],
41
+ "sideEffects": [
42
+ "*.css"
43
+ ],
44
+ "scripts": {
45
+ "start": "vite",
46
+ "build": "vite build",
47
+ "serve": "vite preview",
48
+ "build:types": "tsc -p tsconfig.types.json && node scripts/clean-types.js",
49
+ "prepack": "npm run build:types"
50
+ },
51
+ "peerDependencies": {
52
+ "ol": "^10.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "ol": "^10.10.0",
56
+ "proj4": "^2.22.0",
57
+ "typescript": "^6.0.3",
58
+ "vite": "^8.3.0"
59
+ }
60
+ }
package/readme.md ADDED
@@ -0,0 +1,239 @@
1
+ # OpenLayers Layer Control
2
+
3
+ A layer control for [OpenLayers](https://openlayers.org/): a map button that opens a docked, resizable side panel in which the user switches layers and layer groups on and off.
4
+
5
+ The panel is rendered **next to** the map, never on top of it. The map shrinks while the panel is open.
6
+
7
+ ## Features
8
+
9
+ - Map button with an inline SVG icon that opens and closes the panel, in one of five positions that stay clear of the OpenLayers default controls.
10
+ - Panel docked to the right (or left) of the map, resizable by dragging its edge. Double-click the edge to reset the width.
11
+ - Nested layer groups, with fold/unfold per group.
12
+ - Basemaps as radio buttons (only one visible at a time).
13
+ - Any other group can be **exclusive** (radio buttons) or **non-exclusive** (checkboxes). Right-click a group title to switch between the two.
14
+ - Layers that are not drawn at the current zoom level (because of `minResolution`/`maxResolution` or `minZoom`/`maxZoom`) are greyed out with a tooltip. Layers inside a switched-off group are greyed out too, with a tooltip naming the group.
15
+ - Optional opacity slider per layer.
16
+ - Optional search box that filters the layer list. Click the panel title to show or hide it.
17
+ - Follows the map: layers added, removed or changed in code are reflected in the panel.
18
+ - English by default. Every string can be translated.
19
+ - Keyboard accessible: real checkboxes, radios and buttons, Escape closes the panel.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install ol-layer-control
25
+ ```
26
+
27
+ The package requires OpenLayers 10 (`ol` is a peer dependency, so your app provides it). TypeScript declarations are included.
28
+
29
+ ## Usage
30
+
31
+ ```js
32
+ import Map from 'ol/Map.js';
33
+ import LayerControl from 'ol-layer-control';
34
+
35
+ const map = new Map({ /* ... */ });
36
+ map.addControl(new LayerControl({ open: true }));
37
+ ```
38
+
39
+ The package is published as ES modules and needs a bundler, such as Vite, webpack, Parcel, or Rollup with a CSS plugin. The control imports its own stylesheet, so the single import above is all you need.
40
+
41
+ If your setup handles CSS separately, the stylesheet is also available on its own:
42
+
43
+ ```js
44
+ import 'ol-layer-control/ol-layer-control.css';
45
+ ```
46
+
47
+ ### Layer properties
48
+
49
+ The control reads these properties from layers and groups. Set them as constructor options or with `layer.set(...)`.
50
+
51
+ | Property | On | Meaning |
52
+ | ------------------------ | ------------- | ---------------------------------------------------------------------------------------- |
53
+ | `title` | layer, group | Text shown in the panel. Layers without a title are not listed. |
54
+ | `displayInLayerControl` | layer, group | `false` hides the layer or group from the panel. |
55
+ | `type: 'base'` | layer | Marks a basemap. A group containing basemaps is exclusive and has no checkbox of its own. |
56
+ | `exclusive` | group | `true` renders the children as radio buttons. Right-clicking the group title toggles it. |
57
+ | `combine` | group | `true` shows the group as a single layer instead of expanding its children. |
58
+ | `folded` | group | `true` starts the group collapsed. Updated when the user folds or unfolds. |
59
+
60
+ ```js
61
+ const baseMaps = new LayerGroup({
62
+ title: 'Basemaps',
63
+ layers: [
64
+ new TileLayer({ title: 'OpenStreetMap', type: 'base', source: new OSM() }),
65
+ new TileLayer({ title: 'None', type: 'base', visible: false }),
66
+ ],
67
+ });
68
+
69
+ const overlays = new LayerGroup({
70
+ title: 'Administrative areas',
71
+ exclusive: true,
72
+ layers: [municipalities, provinces],
73
+ });
74
+ ```
75
+
76
+ ### Options
77
+
78
+ | Option | Type | Default | Description |
79
+ | ----------------- | ------------------------- | --------- | ---------------------------------------------------------------------------------------------------- |
80
+ | `open` | `boolean` | `false` | Start with the panel open. |
81
+ | `side` | `'right' \| 'left'` | `'right'` | Side of the map the panel docks to. |
82
+ | `buttonPosition` | `ButtonPosition` | per side | Where the map button sits. See [Button position](#button-position). Defaults to `'top-left'` when `side` is `'left'`, otherwise `'right'`. |
83
+ | `panelWidth` | `number` | `320` | Initial panel width in pixels. |
84
+ | `minPanelWidth` | `number` | `200` | Smallest width the user can resize to. |
85
+ | `maxPanelWidth` | `number` | `800` | Largest width the user can resize to. The control also always leaves some map visible. |
86
+ | `resizable` | `boolean` | `true` | Show the drag handle. |
87
+ | `reverse` | `boolean` | `true` | List layers top-most first (the reverse of the map's rendering order). |
88
+ | `exclusiveToggle` | `boolean` | `true` | Let the user right-click a group title to switch between exclusive and non-exclusive. |
89
+ | `opacitySlider` | `boolean` | `false` | Give every layer a button that reveals an opacity slider. |
90
+ | `search` | `boolean` | `false` | Show the search box from the start. The user can always show or hide it by clicking the panel title. |
91
+ | `panelTarget` | `HTMLElement \| string` | | Render the panel into this element (or element id) instead of next to the map. See [Layout](#layout). |
92
+ | `target` | `HTMLElement \| string` | | Standard OpenLayers control option: where to render the map button. |
93
+ | `i18n` | `Partial<LayerControlI18n>` | | Translations, see [Translating](#translating). |
94
+
95
+ ### API
96
+
97
+ | Method | Description |
98
+ | ------------------------------- | ------------------------------------------------------------------------------------ |
99
+ | `open()`, `close()`, `toggle()` | Show or hide the panel. |
100
+ | `isOpen()` | Whether the panel is shown. |
101
+ | `setPanelWidth(px)` | Set the panel width (clamped to min/max). |
102
+ | `setButtonPosition(position)`, `getButtonPosition()` | Move the map button, or read where it is. |
103
+ | `getPanelWidth()` | Current panel width in pixels. |
104
+ | `getPanelElement()` | The panel element. |
105
+ | `setExclusive(group, boolean)` | Make a group exclusive or not. When several children are visible, only the top-most stays visible. |
106
+ | `showSearch(boolean)`, `toggleSearch()`, `isSearchVisible()` | Show or hide the search box. Hiding it clears the filter. |
107
+ | `setFilter(text)`, `getFilter()` | Filter the list by (part of) a title. |
108
+ | `setI18n(partial)`, `getI18n()` | Replace (part of) the UI strings at runtime. |
109
+ | `refresh()` | Rebuild the list. Normally not needed; the control follows the map by itself. |
110
+
111
+ The control is an OpenLayers `Control`, so `control.on(...)` works. These properties fire `change:` events:
112
+
113
+ | Property | Type | Event |
114
+ | --------------- | --------- | ----------------------- |
115
+ | `open` | `boolean` | `change:open` |
116
+ | `panelWidth` | `number` | `change:panelWidth` |
117
+ | `searchVisible` | `boolean` | `change:searchVisible` |
118
+ | `buttonPosition` | `ButtonPosition` | `change:buttonPosition` |
119
+
120
+ ```js
121
+ control.on('change:open', () => console.log('panel open:', control.isOpen()));
122
+ ```
123
+
124
+ ### Button position
125
+
126
+ The map button can sit in five places. Each one leaves room for the OpenLayers default control that normally uses that part of the map.
127
+
128
+ | `buttonPosition` | Where |
129
+ | ---------------- | ----------------------------------------------------------------------------------------------- |
130
+ | `'top-left'` | Top left, below the Zoom buttons. |
131
+ | `'bottom-left'` | Bottom left corner. |
132
+ | `'top-right'` | Top right corner. This is the Rotate button's spot, see the note below. |
133
+ | `'right'` | Top right, below the Rotate button. |
134
+ | `'bottom-right'` | Bottom right, above the Attribution button. |
135
+
136
+ The Rotate button only appears when the map is rotated, and it appears in the top-right corner. With `'top-right'` the layer button covers it. That is why the default for a right-side panel is `'right'`, just below it.
137
+
138
+ ```js
139
+ new LayerControl({ side: 'right', buttonPosition: 'bottom-right' });
140
+ ```
141
+
142
+ The list of valid values is exported as `BUTTON_POSITIONS`. An unknown value logs a warning and falls back to the default for the panel's side.
143
+
144
+ The offsets assume the default OpenLayers controls. When you add others, such as a ScaleLine at the bottom left, adjust the spacing with `--ol-layer-control-edge` and `--ol-layer-control-gap` (see [Theming](#theming)), or override the position classes `ol-layer-control--top-left`, `ol-layer-control--bottom-left`, `ol-layer-control--top-right`, `ol-layer-control--right` and `ol-layer-control--bottom-right`.
145
+
146
+ ### Translating
147
+
148
+ Pass any subset of the strings in the `i18n` option, or call `setI18n()` later. The full set, with the English defaults, is exported as `DEFAULT_I18N`.
149
+
150
+ ```js
151
+ import LayerControl, { DEFAULT_I18N } from 'ol-layer-control';
152
+
153
+ new LayerControl({
154
+ i18n: {
155
+ buttonTitle: 'Lagen',
156
+ panelTitle: 'Lagen',
157
+ closeTitle: 'Lagenpaneel sluiten',
158
+ resizeTitle: 'Sleep om de breedte te wijzigen, dubbelklik om te herstellen',
159
+ collapseTitle: 'Groep inklappen',
160
+ expandTitle: 'Groep uitklappen',
161
+ exclusiveHint: 'Rechtsklik om te wisselen tussen één laag tegelijk en meerdere lagen',
162
+ outOfRangeHint: 'Niet zichtbaar op dit zoomniveau',
163
+ groupOffHint: 'Niet zichtbaar: groep "{group}" staat uit',
164
+ opacityTitle: 'Transparantie',
165
+ searchToggleTitle: 'Zoekveld tonen of verbergen',
166
+ searchPlaceholder: 'Lagen zoeken',
167
+ searchClearTitle: 'Zoekopdracht wissen',
168
+ searchNoResults: 'Geen lagen gevonden',
169
+ },
170
+ });
171
+ ```
172
+
173
+ `{group}` in `groupOffHint` is replaced by the title of the switched-off group. Layer and group titles come from the layers themselves and are not translated by the control.
174
+
175
+ ### Theming
176
+
177
+ All colours and sizes are CSS custom properties. Override them on any ancestor of the panel, for example `:root`:
178
+
179
+ ```css
180
+ :root {
181
+ --ol-layer-control-bg: #1e1e1e;
182
+ --ol-layer-control-fg: #eee;
183
+ --ol-layer-control-border: #444;
184
+ --ol-layer-control-hover: rgba(255, 255, 255, 0.08);
185
+ --ol-layer-control-accent: #6ea8ff;
186
+ --ol-layer-control-font: 14px/1.4 system-ui, sans-serif;
187
+ --ol-layer-control-indent: 1.4em;
188
+ --ol-layer-control-shadow: none;
189
+ --ol-layer-control-edge: 0.5em; /* map button distance from the map edge */
190
+ --ol-layer-control-gap: 0.5em; /* space between the map button and the OpenLayers control it avoids */
191
+ }
192
+ ```
193
+
194
+ Every element carries a class starting with `ol-layer-control-`, so anything else can be restyled with plain CSS.
195
+
196
+ ### Layout
197
+
198
+ By default the control inserts the panel as a sibling of the map's target element and, while the panel is open, sets an inline `width: calc(100% - <panel width>)` on the map element (plus a `margin-left` when the panel is on the left). It calls `map.updateSize()` after every change and restores the original inline style when the panel closes.
199
+
200
+ The panel is absolutely positioned with `top: 0; bottom: 0` against the map's nearest positioned ancestor. This works out of the box when the map fills the page. When the map sits inside a page with other content, give the map's parent element `position: relative` so the panel lines up with the map.
201
+
202
+ If your app has its own layout (flexbox, a sidebar component, a framework), pass `panelTarget`. The control then renders the panel into that element and leaves the map element alone.
203
+
204
+ ## Development
205
+
206
+ The control lives in `src/`: `ol-layer-control.js` and `ol-layer-control.css`. It is published as-is, without a build step.
207
+
208
+ The `demo/` folder holds an app (Dutch PDOK services in EPSG:28992) used to develop and test the control. It requires Node 20.19 or newer.
209
+
210
+ ```bash
211
+ npm install
212
+ npm start
213
+ ```
214
+
215
+ The demo runs at http://localhost:5173. To create a production build of the demo in `dist/`:
216
+
217
+ ```bash
218
+ npm run build
219
+ ```
220
+
221
+ The TypeScript declarations in `types/` are generated from the JSDoc comments in `src/`. `npm pack` and `npm publish` generate them automatically. To generate them by hand:
222
+
223
+ ```bash
224
+ npm run build:types
225
+ ```
226
+
227
+ To see exactly which files a release would contain:
228
+
229
+ ```bash
230
+ npm pack --dry-run
231
+ ```
232
+
233
+ ## Changelog
234
+
235
+ See [CHANGELOG.md](CHANGELOG.md).
236
+
237
+ ## Roadmap
238
+
239
+ - Legends, using the `legend` property already present on the demo layers.