maplibre-preview 1.6.0 → 1.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/bin/maplibre-preview +1 -1
- data/lib/maplibre-preview/public/js/overlay_layout.js +415 -0
- data/lib/maplibre-preview/public/js/tilegrid.js +9 -0
- data/lib/maplibre-preview/version.rb +1 -1
- data/lib/maplibre-preview/views/maplibre_layout.slim +137 -6
- data/lib/maplibre-preview/views/maplibre_map.slim +474 -14
- data/spec/maplibre_preview_spec.rb +22 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61c7dd8ecf3753aa43ba5272cc06dab258cbb643824e74336e21e07680795cd6
|
|
4
|
+
data.tar.gz: 6a008af2ffe1b2f3331dc752ab36041767299cc38271f9f9433567f268952198
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c75eede1968d1ddfaa8c388de5c827bf6f8e27f5bcc628938aca1c61f7ccf2896cb70f9a498f6262f352a4c1a211881a87ac09133da2445cd3e6c02f6fa42aac
|
|
7
|
+
data.tar.gz: 435d1192991bacd4730d547eb3f580f99c8d287fabb29e221432a44b309c6b73e25c198f6abea453e0a9ab0ba5d4de6b83adff1fcd4e68cbd73ec03c67e9f22e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.0] - 2026-05-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Movable overlay windows** - added `OverlayLayoutManager` for shared drag, edge clamping, snapping and persisted positions of movable map UI panels
|
|
7
|
+
- **Window layout reset** - added `Reset window layout` action in Map Settings to clear saved panel positions from local storage and restore defaults
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Panel layout foundation** - moved Map Settings, Style Controls, Style Parameters, Performance, Elevation Profile and Tile Boundaries to the shared overlay layout manager
|
|
11
|
+
- **Default panel positions** - kept static map controls attached to map edges and placed Map Settings in the top-left default position
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Overlay visibility** - fixed managed panel z-index so map settings, filter controls and static MapLibre controls remain visible
|
|
15
|
+
- **Elevation profile frame** - fixed profile window positioning and sizing while dragging near the bottom edge
|
|
16
|
+
|
|
17
|
+
## [1.7.2] - 2026-05-14
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Map settings styling** - rounded all control panel corners and styled range sliders to match the dark UI
|
|
21
|
+
|
|
22
|
+
## [1.7.0] - 2026-05-14
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Style source parameters** - detect `query_params` / `queryParams` from style sources and source metadata
|
|
26
|
+
- **Style parameters panel** - add a bottom-center collapsible panel for passing detected parameters into style, source, tile, data, and metadata URLs
|
|
27
|
+
- **Date/time parameter inputs** - render date/time-like parameters as `datetime-local` fields and send them as epoch seconds
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **Overlay layout** - stack bottom overlays so Style Parameters, Loading, and Elevation Profile panels do not cover each other
|
|
31
|
+
- **Style parameter state** - persist parameter values per style URL and keep applied values in the page query string
|
|
32
|
+
|
|
3
33
|
## [1.6.0] - 2026-05-13
|
|
4
34
|
|
|
5
35
|
### Added
|
data/bin/maplibre-preview
CHANGED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
class OverlayLayoutManager {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.storageKey = options.storageKey || 'maplibre-preview:overlay-layout:v3';
|
|
4
|
+
this.snapThreshold = options.snapThreshold || 32;
|
|
5
|
+
this.mobileBreakpoint = options.mobileBreakpoint || 768;
|
|
6
|
+
this.edgeGap = options.edgeGap || 10;
|
|
7
|
+
this.getReservedBounds = options.getReservedBounds || (() => ({}));
|
|
8
|
+
this.panels = new Map();
|
|
9
|
+
this.state = this.loadState();
|
|
10
|
+
this.drag = null;
|
|
11
|
+
this.zIndex = 1100;
|
|
12
|
+
this.resizeHandler = () => this.refreshBounds();
|
|
13
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registerPanel(config) {
|
|
17
|
+
const element = this.resolveElement(config.element);
|
|
18
|
+
if (!element) return null;
|
|
19
|
+
|
|
20
|
+
const id = config.id || element.id;
|
|
21
|
+
if (!id) return null;
|
|
22
|
+
|
|
23
|
+
const existing = this.panels.get(id);
|
|
24
|
+
if (existing) this.unregisterPanel(id);
|
|
25
|
+
|
|
26
|
+
const panel = {
|
|
27
|
+
id,
|
|
28
|
+
element,
|
|
29
|
+
handle: this.resolveHandle(element, config.handleSelector),
|
|
30
|
+
defaultAnchor: config.defaultAnchor || 'left',
|
|
31
|
+
defaultOffset: config.defaultOffset || {x: 0, y: 0},
|
|
32
|
+
snap: config.snap !== false,
|
|
33
|
+
lockSizeOnDrag: config.lockSizeOnDrag === true,
|
|
34
|
+
movable: config.movable !== false,
|
|
35
|
+
cleanup: []
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
element.dataset.overlayPanelId = id;
|
|
39
|
+
element.classList.add('overlay-managed-panel');
|
|
40
|
+
this.panels.set(id, panel);
|
|
41
|
+
|
|
42
|
+
if (panel.movable && panel.handle) {
|
|
43
|
+
this.prepareHandle(panel);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.applyStoredOrDefaultPosition(panel);
|
|
47
|
+
return panel;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
unregisterPanel(id) {
|
|
51
|
+
const panel = this.panels.get(id);
|
|
52
|
+
if (!panel) return;
|
|
53
|
+
|
|
54
|
+
panel.cleanup.forEach(fn => fn());
|
|
55
|
+
panel.element.classList.remove('overlay-managed-panel', 'overlay-dragging');
|
|
56
|
+
panel.element.removeAttribute('data-overlay-panel-id');
|
|
57
|
+
this.panels.delete(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
movePanelTo(id, anchor) {
|
|
61
|
+
const panel = this.panels.get(id);
|
|
62
|
+
if (!panel) return;
|
|
63
|
+
|
|
64
|
+
const nextState = {mode: 'anchor', anchor, offset: {x: 0, y: 0}};
|
|
65
|
+
this.setPanelState(panel, nextState);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resetPanel(id) {
|
|
69
|
+
const panel = this.panels.get(id);
|
|
70
|
+
if (!panel) return;
|
|
71
|
+
|
|
72
|
+
delete this.state[id];
|
|
73
|
+
this.persistState();
|
|
74
|
+
this.applyStoredOrDefaultPosition(panel);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resetLayout() {
|
|
78
|
+
this.state = {};
|
|
79
|
+
this.clearStoredState();
|
|
80
|
+
this.panels.forEach(panel => this.applyStoredOrDefaultPosition(panel));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
refreshPanel(id) {
|
|
84
|
+
const panel = this.panels.get(id);
|
|
85
|
+
if (!panel) return;
|
|
86
|
+
if (this.drag?.panel?.id === id) return;
|
|
87
|
+
|
|
88
|
+
const panelState = this.state[id] || this.defaultState(panel);
|
|
89
|
+
this.applyPanelState(panel, panelState);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
refreshBounds() {
|
|
93
|
+
if (this.drag) return;
|
|
94
|
+
this.panels.forEach(panel => this.refreshPanel(panel.id));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
destroy() {
|
|
98
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
99
|
+
[...this.panels.keys()].forEach(id => this.unregisterPanel(id));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolveElement(elementOrSelector) {
|
|
103
|
+
if (!elementOrSelector) return null;
|
|
104
|
+
return typeof elementOrSelector === 'string'
|
|
105
|
+
? document.querySelector(elementOrSelector)
|
|
106
|
+
: elementOrSelector;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolveHandle(element, selector) {
|
|
110
|
+
return selector ? element.querySelector(selector) : element;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
prepareHandle(panel) {
|
|
114
|
+
const pointerDown = event => this.onPointerDown(event, panel);
|
|
115
|
+
const keyDown = event => this.onHandleKeyDown(event, panel);
|
|
116
|
+
|
|
117
|
+
panel.handle.classList.add('overlay-panel-handle');
|
|
118
|
+
panel.handle.tabIndex = panel.handle.tabIndex >= 0 ? panel.handle.tabIndex : 0;
|
|
119
|
+
if (!panel.handle.querySelector('button, input, select, textarea, a')) {
|
|
120
|
+
panel.handle.setAttribute('role', panel.handle.getAttribute('role') || 'button');
|
|
121
|
+
}
|
|
122
|
+
panel.handle.setAttribute('aria-label', panel.handle.getAttribute('aria-label') || `Move ${panel.id} panel`);
|
|
123
|
+
panel.handle.addEventListener('pointerdown', pointerDown);
|
|
124
|
+
panel.handle.addEventListener('keydown', keyDown);
|
|
125
|
+
panel.cleanup.push(() => panel.handle.removeEventListener('pointerdown', pointerDown));
|
|
126
|
+
panel.cleanup.push(() => panel.handle.removeEventListener('keydown', keyDown));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onPointerDown(event, panel) {
|
|
130
|
+
if (event.button !== 0 || !panel.movable || this.isInteractiveTarget(event.target)) return;
|
|
131
|
+
|
|
132
|
+
const rect = panel.element.getBoundingClientRect();
|
|
133
|
+
const pointerMove = moveEvent => this.onPointerMove(moveEvent);
|
|
134
|
+
const pointerUp = upEvent => this.onPointerUp(upEvent);
|
|
135
|
+
|
|
136
|
+
if (panel.lockSizeOnDrag) {
|
|
137
|
+
panel.element.style.width = `${Math.round(rect.width)}px`;
|
|
138
|
+
panel.element.style.height = `${Math.round(rect.height)}px`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.drag = {
|
|
142
|
+
panel,
|
|
143
|
+
pointerId: event.pointerId,
|
|
144
|
+
startX: event.clientX,
|
|
145
|
+
startY: event.clientY,
|
|
146
|
+
originX: rect.left,
|
|
147
|
+
originY: rect.top,
|
|
148
|
+
pointerMove,
|
|
149
|
+
pointerUp
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
panel.element.classList.add('overlay-dragging');
|
|
153
|
+
this.bringToFront(panel);
|
|
154
|
+
try {
|
|
155
|
+
panel.handle.setPointerCapture?.(event.pointerId);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Some synthetic pointer events do not create an active pointer capture target.
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
panel.handle.addEventListener('pointermove', pointerMove);
|
|
161
|
+
panel.handle.addEventListener('pointerup', pointerUp);
|
|
162
|
+
panel.handle.addEventListener('pointercancel', pointerUp);
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onPointerMove(event) {
|
|
167
|
+
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
|
168
|
+
|
|
169
|
+
const nextX = this.drag.originX + event.clientX - this.drag.startX;
|
|
170
|
+
const nextY = this.drag.originY + event.clientY - this.drag.startY;
|
|
171
|
+
const position = this.clampPosition(this.drag.panel, nextX, nextY);
|
|
172
|
+
|
|
173
|
+
this.applyPosition(this.drag.panel, position.x, position.y);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
onPointerUp(event) {
|
|
177
|
+
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
|
178
|
+
|
|
179
|
+
const panel = this.drag.panel;
|
|
180
|
+
const rect = panel.element.getBoundingClientRect();
|
|
181
|
+
const nextState = this.stateFromPosition(panel, rect.left, rect.top);
|
|
182
|
+
this.cleanupDrag();
|
|
183
|
+
this.setPanelState(panel, nextState);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
cleanupDrag() {
|
|
187
|
+
if (!this.drag) return null;
|
|
188
|
+
|
|
189
|
+
const drag = this.drag;
|
|
190
|
+
try {
|
|
191
|
+
drag.panel.handle.releasePointerCapture?.(drag.pointerId);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Pointer capture may already be released by the browser.
|
|
194
|
+
}
|
|
195
|
+
drag.panel.handle.removeEventListener('pointermove', drag.pointerMove);
|
|
196
|
+
drag.panel.handle.removeEventListener('pointerup', drag.pointerUp);
|
|
197
|
+
drag.panel.handle.removeEventListener('pointercancel', drag.pointerUp);
|
|
198
|
+
drag.panel.element.classList.remove('overlay-dragging');
|
|
199
|
+
this.drag = null;
|
|
200
|
+
return drag;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
onHandleKeyDown(event, panel) {
|
|
204
|
+
if (event.key === 'Escape' && this.drag) {
|
|
205
|
+
this.cleanupDrag();
|
|
206
|
+
this.refreshPanel(panel.id);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!event.shiftKey || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
211
|
+
|
|
212
|
+
const rect = panel.element.getBoundingClientRect();
|
|
213
|
+
const step = event.altKey ? 32 : 8;
|
|
214
|
+
const delta = {
|
|
215
|
+
ArrowLeft: [-step, 0],
|
|
216
|
+
ArrowRight: [step, 0],
|
|
217
|
+
ArrowUp: [0, -step],
|
|
218
|
+
ArrowDown: [0, step]
|
|
219
|
+
}[event.key];
|
|
220
|
+
const position = this.clampPosition(panel, rect.left + delta[0], rect.top + delta[1]);
|
|
221
|
+
|
|
222
|
+
this.setPanelState(panel, {mode: 'free', x: position.x, y: position.y});
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
isInteractiveTarget(target) {
|
|
227
|
+
return target?.closest?.('button, input, select, textarea, a, label, [data-overlay-ignore-drag="true"]');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
bringToFront(panel) {
|
|
231
|
+
this.zIndex += 1;
|
|
232
|
+
panel.element.style.zIndex = String(this.zIndex);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
loadState() {
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(localStorage.getItem(this.storageKey) || '{}');
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.warn('OverlayLayoutManager: could not load layout state', e);
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
persistState() {
|
|
245
|
+
try {
|
|
246
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.warn('OverlayLayoutManager: could not persist layout state', e);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
clearStoredState() {
|
|
253
|
+
try {
|
|
254
|
+
localStorage.removeItem(this.storageKey);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.warn('OverlayLayoutManager: could not clear layout state', e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
defaultState(panel) {
|
|
261
|
+
const offset = typeof panel.defaultOffset === 'function'
|
|
262
|
+
? panel.defaultOffset(panel)
|
|
263
|
+
: panel.defaultOffset;
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
mode: 'anchor',
|
|
267
|
+
anchor: panel.defaultAnchor,
|
|
268
|
+
offset: {...offset}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
applyStoredOrDefaultPosition(panel) {
|
|
273
|
+
const panelState = this.state[panel.id] || this.defaultState(panel);
|
|
274
|
+
this.applyPanelState(panel, panelState);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
setPanelState(panel, panelState) {
|
|
278
|
+
this.state[panel.id] = panelState;
|
|
279
|
+
this.persistState();
|
|
280
|
+
this.applyPanelState(panel, panelState);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
applyPanelState(panel, panelState) {
|
|
284
|
+
const responsiveState = this.isConstrainedViewport() && panelState.mode === 'free'
|
|
285
|
+
? this.defaultState(panel)
|
|
286
|
+
: panelState;
|
|
287
|
+
const position = responsiveState.mode === 'free'
|
|
288
|
+
? this.clampPosition(panel, responsiveState.x, responsiveState.y)
|
|
289
|
+
: this.positionForAnchor(panel, responsiveState.anchor, responsiveState.offset || {x: 0, y: 0});
|
|
290
|
+
|
|
291
|
+
this.applyPosition(panel, position.x, position.y);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
applyPosition(panel, x, y) {
|
|
295
|
+
panel.element.style.position = 'fixed';
|
|
296
|
+
panel.element.style.left = `${Math.round(x)}px`;
|
|
297
|
+
panel.element.style.top = `${Math.round(y)}px`;
|
|
298
|
+
panel.element.style.right = 'auto';
|
|
299
|
+
panel.element.style.bottom = 'auto';
|
|
300
|
+
panel.element.style.transform = 'none';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
positionForAnchor(panel, anchor, offset) {
|
|
304
|
+
const rect = this.panelRect(panel);
|
|
305
|
+
const viewport = this.viewportBounds();
|
|
306
|
+
const centerX = (viewport.width - rect.width) / 2;
|
|
307
|
+
const centerY = (viewport.height - viewport.reservedBottom - rect.height) / 2;
|
|
308
|
+
let x = centerX;
|
|
309
|
+
let y = centerY;
|
|
310
|
+
|
|
311
|
+
if (anchor === 'left') {
|
|
312
|
+
x = this.edgeGap + (offset.x || 0);
|
|
313
|
+
y = centerY + (offset.y || 0);
|
|
314
|
+
} else if (anchor === 'right') {
|
|
315
|
+
x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
|
|
316
|
+
y = centerY + (offset.y || 0);
|
|
317
|
+
} else if (anchor === 'top') {
|
|
318
|
+
x = centerX + (offset.x || 0);
|
|
319
|
+
y = this.edgeGap + (offset.y || 0);
|
|
320
|
+
} else if (anchor === 'bottom') {
|
|
321
|
+
x = centerX + (offset.x || 0);
|
|
322
|
+
y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
|
|
323
|
+
} else if (anchor === 'top-left') {
|
|
324
|
+
x = this.edgeGap + (offset.x || 0);
|
|
325
|
+
y = this.edgeGap + (offset.y || 0);
|
|
326
|
+
} else if (anchor === 'top-right') {
|
|
327
|
+
x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
|
|
328
|
+
y = this.edgeGap + (offset.y || 0);
|
|
329
|
+
} else if (anchor === 'bottom-left') {
|
|
330
|
+
x = this.edgeGap + (offset.x || 0);
|
|
331
|
+
y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
|
|
332
|
+
} else if (anchor === 'bottom-right') {
|
|
333
|
+
x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
|
|
334
|
+
y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return this.clampPosition(panel, x, y);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
stateFromPosition(panel, x, y) {
|
|
341
|
+
if (this.isConstrainedViewport()) {
|
|
342
|
+
return {mode: 'anchor', anchor: this.nearestAnchor(panel, x, y).anchor, offset: {x: 0, y: 0}};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const nearest = this.nearestAnchor(panel, x, y);
|
|
346
|
+
if (!panel.snap || nearest.distance > this.snapThreshold) {
|
|
347
|
+
const clamped = this.clampPosition(panel, x, y);
|
|
348
|
+
return {mode: 'free', x: clamped.x, y: clamped.y};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const rect = this.panelRect(panel);
|
|
352
|
+
const viewport = this.viewportBounds();
|
|
353
|
+
const centerX = (viewport.width - rect.width) / 2;
|
|
354
|
+
const centerY = (viewport.height - viewport.reservedBottom - rect.height) / 2;
|
|
355
|
+
const offset = {x: 0, y: 0};
|
|
356
|
+
|
|
357
|
+
if (nearest.anchor === 'left' || nearest.anchor === 'right') {
|
|
358
|
+
offset.y = y - centerY;
|
|
359
|
+
} else {
|
|
360
|
+
offset.x = x - centerX;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {mode: 'anchor', anchor: nearest.anchor, offset};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
nearestAnchor(panel, x, y) {
|
|
367
|
+
const rect = this.panelRect(panel);
|
|
368
|
+
const viewport = this.viewportBounds();
|
|
369
|
+
const distances = [
|
|
370
|
+
['left', x],
|
|
371
|
+
['right', viewport.width - (x + rect.width)],
|
|
372
|
+
['top', y],
|
|
373
|
+
['bottom', viewport.height - viewport.reservedBottom - (y + rect.height)]
|
|
374
|
+
].map(([anchor, distance]) => ({anchor, distance: Math.abs(distance)}));
|
|
375
|
+
|
|
376
|
+
return distances.sort((a, b) => a.distance - b.distance)[0];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
clampPosition(panel, x, y) {
|
|
380
|
+
const rect = this.panelRect(panel);
|
|
381
|
+
const viewport = this.viewportBounds();
|
|
382
|
+
const minX = this.edgeGap;
|
|
383
|
+
const minY = this.edgeGap;
|
|
384
|
+
const maxX = Math.max(minX, viewport.width - rect.width - this.edgeGap);
|
|
385
|
+
const maxY = Math.max(minY, viewport.height - viewport.reservedBottom - rect.height - this.edgeGap);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
x: Math.min(maxX, Math.max(minX, x)),
|
|
389
|
+
y: Math.min(maxY, Math.max(minY, y))
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
panelRect(panel) {
|
|
394
|
+
const rect = panel.element.getBoundingClientRect();
|
|
395
|
+
return {
|
|
396
|
+
width: rect.width || panel.element.offsetWidth || 1,
|
|
397
|
+
height: rect.height || panel.element.offsetHeight || 1
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
viewportBounds() {
|
|
402
|
+
const reserved = this.getReservedBounds() || {};
|
|
403
|
+
return {
|
|
404
|
+
width: window.innerWidth,
|
|
405
|
+
height: window.innerHeight,
|
|
406
|
+
reservedBottom: Math.max(0, Number(reserved.bottom || 0))
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
isConstrainedViewport() {
|
|
411
|
+
return window.innerWidth < this.mobileBreakpoint;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
window.OverlayLayoutManager = OverlayLayoutManager;
|
|
@@ -44,6 +44,13 @@ class TileGridManager {
|
|
|
44
44
|
`;
|
|
45
45
|
|
|
46
46
|
document.getElementById('map-container').appendChild(this.panelContainer);
|
|
47
|
+
window.overlayLayoutManager?.registerPanel({
|
|
48
|
+
id: 'tilegrid',
|
|
49
|
+
element: this.panelContainer,
|
|
50
|
+
handleSelector: '.tilegrid-title',
|
|
51
|
+
defaultAnchor: 'right',
|
|
52
|
+
defaultOffset: {x: 0, y: 0}
|
|
53
|
+
});
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
setupEventListeners() {
|
|
@@ -114,6 +121,7 @@ class TileGridManager {
|
|
|
114
121
|
|
|
115
122
|
if (this.panelContainer) {
|
|
116
123
|
this.panelContainer.style.display = this.isVisible ? 'block' : 'none';
|
|
124
|
+
this.isVisible && window.overlayLayoutManager?.refreshPanel('tilegrid');
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
const btn = document.getElementById('tilegrid-mode-btn');
|
|
@@ -163,6 +171,7 @@ class TileGridManager {
|
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
if (this.panelContainer && this.panelContainer.parentNode) {
|
|
174
|
+
window.overlayLayoutManager?.unregisterPanel('tilegrid');
|
|
166
175
|
this.panelContainer.parentNode.removeChild(this.panelContainer);
|
|
167
176
|
}
|
|
168
177
|
|