maplibre-preview 1.7.2 → 1.9.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.
@@ -0,0 +1,247 @@
1
+ (function () {
2
+ let activePicker = null;
3
+
4
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
5
+
6
+ const parseInputDate = (value) => {
7
+ const parsed = value ? new Date(value) : new Date();
8
+ return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
9
+ };
10
+
11
+ const formatInputDate = (date) => {
12
+ const pad = number => String(number).padStart(2, '0');
13
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
14
+ };
15
+
16
+ const close = () => {
17
+ if (!activePicker) return;
18
+
19
+ activePicker.cleanup?.();
20
+ activePicker.element.remove();
21
+ activePicker = null;
22
+ };
23
+
24
+ const position = (input, picker) => {
25
+ const rect = input.getBoundingClientRect();
26
+ const pickerWidth = Math.min(336, window.innerWidth - 16);
27
+ const left = clamp(rect.left, 8, window.innerWidth - pickerWidth - 8);
28
+ const aboveTop = rect.top - picker.offsetHeight - 8;
29
+ const belowTop = rect.bottom + 8;
30
+ const top = aboveTop >= 8 ? aboveTop : Math.min(belowTop, window.innerHeight - picker.offsetHeight - 8);
31
+
32
+ picker.style.width = `${pickerWidth}px`;
33
+ picker.style.left = `${left}px`;
34
+ picker.style.top = `${Math.max(8, top)}px`;
35
+ };
36
+
37
+ const render = (state) => {
38
+ const {element, input, onApply, onClear} = state;
39
+ const monthDate = state.visibleMonth;
40
+ const selected = state.selectedDate;
41
+ const today = new Date();
42
+ const monthLabel = monthDate.toLocaleDateString('en-US', {month: 'long', year: 'numeric'});
43
+ const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
44
+ const firstOffset = (monthStart.getDay() + 6) % 7;
45
+ const firstCell = new Date(monthStart);
46
+ firstCell.setDate(monthStart.getDate() - firstOffset);
47
+
48
+ element.innerHTML = '';
49
+
50
+ const header = document.createElement('div');
51
+ header.className = 'temporal-picker-header';
52
+
53
+ const title = document.createElement('div');
54
+ title.className = 'temporal-picker-title';
55
+ title.textContent = monthLabel;
56
+
57
+ const nav = document.createElement('div');
58
+ nav.className = 'temporal-picker-nav';
59
+ [
60
+ ['Previous month', -1, '<'],
61
+ ['Next month', 1, '>']
62
+ ].forEach(([label, delta, text]) => {
63
+ const button = document.createElement('button');
64
+ button.type = 'button';
65
+ button.className = 'temporal-picker-nav-button';
66
+ button.title = label;
67
+ button.setAttribute('aria-label', label);
68
+ button.textContent = text;
69
+ button.addEventListener('click', () => {
70
+ state.visibleMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + delta, 1);
71
+ render(state);
72
+ });
73
+ nav.appendChild(button);
74
+ });
75
+
76
+ header.appendChild(title);
77
+ header.appendChild(nav);
78
+ element.appendChild(header);
79
+
80
+ const calendar = document.createElement('div');
81
+ calendar.className = 'temporal-picker-calendar';
82
+ ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => {
83
+ const cell = document.createElement('div');
84
+ cell.className = 'temporal-picker-weekday';
85
+ cell.textContent = day;
86
+ calendar.appendChild(cell);
87
+ });
88
+
89
+ for (let index = 0; index < 42; index += 1) {
90
+ const dayDate = new Date(firstCell);
91
+ dayDate.setDate(firstCell.getDate() + index);
92
+
93
+ const button = document.createElement('button');
94
+ button.type = 'button';
95
+ button.className = 'temporal-picker-day';
96
+ dayDate.getMonth() !== monthDate.getMonth() && button.classList.add('muted');
97
+ dayDate.toDateString() === today.toDateString() && button.classList.add('today');
98
+ dayDate.toDateString() === selected.toDateString() && button.classList.add('selected');
99
+ button.textContent = String(dayDate.getDate());
100
+ button.addEventListener('click', () => {
101
+ state.selectedDate = new Date(
102
+ dayDate.getFullYear(),
103
+ dayDate.getMonth(),
104
+ dayDate.getDate(),
105
+ selected.getHours(),
106
+ selected.getMinutes()
107
+ );
108
+ state.visibleMonth = new Date(dayDate.getFullYear(), dayDate.getMonth(), 1);
109
+ render(state);
110
+ });
111
+ calendar.appendChild(button);
112
+ }
113
+
114
+ element.appendChild(calendar);
115
+
116
+ const timeRow = document.createElement('div');
117
+ timeRow.className = 'temporal-picker-time';
118
+
119
+ const hourSelect = document.createElement('select');
120
+ hourSelect.className = 'temporal-picker-select';
121
+ hourSelect.setAttribute('aria-label', 'Hour');
122
+
123
+ const minuteSelect = document.createElement('select');
124
+ minuteSelect.className = 'temporal-picker-select';
125
+ minuteSelect.setAttribute('aria-label', 'Minute');
126
+
127
+ for (let hour = 0; hour < 24; hour += 1) {
128
+ const option = document.createElement('option');
129
+ option.value = String(hour);
130
+ option.textContent = String(hour).padStart(2, '0');
131
+ hourSelect.appendChild(option);
132
+ }
133
+ for (let minute = 0; minute < 60; minute += 1) {
134
+ const option = document.createElement('option');
135
+ option.value = String(minute);
136
+ option.textContent = String(minute).padStart(2, '0');
137
+ minuteSelect.appendChild(option);
138
+ }
139
+
140
+ hourSelect.value = String(selected.getHours());
141
+ minuteSelect.value = String(selected.getMinutes());
142
+
143
+ const updateTime = () => {
144
+ state.selectedDate = new Date(
145
+ state.selectedDate.getFullYear(),
146
+ state.selectedDate.getMonth(),
147
+ state.selectedDate.getDate(),
148
+ Number(hourSelect.value),
149
+ Number(minuteSelect.value)
150
+ );
151
+ };
152
+
153
+ hourSelect.addEventListener('change', updateTime);
154
+ minuteSelect.addEventListener('change', updateTime);
155
+
156
+ timeRow.appendChild(hourSelect);
157
+ timeRow.appendChild(document.createTextNode(':'));
158
+ timeRow.appendChild(minuteSelect);
159
+ element.appendChild(timeRow);
160
+
161
+ const footer = document.createElement('div');
162
+ footer.className = 'temporal-picker-footer';
163
+
164
+ const clearButton = document.createElement('button');
165
+ clearButton.type = 'button';
166
+ clearButton.className = 'temporal-picker-button secondary';
167
+ clearButton.textContent = 'Clear';
168
+ clearButton.addEventListener('click', () => {
169
+ onClear?.();
170
+ close();
171
+ });
172
+
173
+ const todayButton = document.createElement('button');
174
+ todayButton.type = 'button';
175
+ todayButton.className = 'temporal-picker-button secondary';
176
+ todayButton.textContent = 'Today';
177
+ todayButton.addEventListener('click', () => {
178
+ state.selectedDate = new Date();
179
+ state.visibleMonth = new Date(state.selectedDate.getFullYear(), state.selectedDate.getMonth(), 1);
180
+ render(state);
181
+ });
182
+
183
+ const applyButton = document.createElement('button');
184
+ applyButton.type = 'button';
185
+ applyButton.className = 'temporal-picker-button primary';
186
+ applyButton.textContent = 'Apply';
187
+ applyButton.addEventListener('click', () => {
188
+ onApply?.(formatInputDate(state.selectedDate));
189
+ close();
190
+ });
191
+
192
+ footer.appendChild(clearButton);
193
+ footer.appendChild(todayButton);
194
+ footer.appendChild(applyButton);
195
+ element.appendChild(footer);
196
+
197
+ position(input, element);
198
+ };
199
+
200
+ const open = (input, options = {}) => {
201
+ if (activePicker?.input === input) return;
202
+ close();
203
+
204
+ const picker = document.createElement('div');
205
+ picker.className = 'temporal-picker-popover';
206
+ picker.setAttribute('role', 'dialog');
207
+ picker.setAttribute('aria-label', `Select ${input.dataset.parameterName || 'date and time'}`);
208
+ document.body.appendChild(picker);
209
+
210
+ const selectedDate = parseInputDate(input.value);
211
+ const state = {
212
+ element: picker,
213
+ input,
214
+ selectedDate,
215
+ visibleMonth: new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1),
216
+ onApply: options.onApply || (value => { input.value = value; }),
217
+ onClear: options.onClear || (() => { input.value = ''; })
218
+ };
219
+
220
+ const outsideHandler = (event) => {
221
+ if (picker.contains(event.target) || event.target === input) return;
222
+ close();
223
+ };
224
+ const keyHandler = (event) => event.key === 'Escape' && close();
225
+ const repositionHandler = () => position(input, picker);
226
+
227
+ activePicker = {
228
+ element: picker,
229
+ input,
230
+ cleanup: () => {
231
+ document.removeEventListener('mousedown', outsideHandler);
232
+ document.removeEventListener('keydown', keyHandler);
233
+ window.removeEventListener('resize', repositionHandler);
234
+ window.removeEventListener('scroll', repositionHandler, true);
235
+ }
236
+ };
237
+
238
+ document.addEventListener('mousedown', outsideHandler);
239
+ document.addEventListener('keydown', keyHandler);
240
+ window.addEventListener('resize', repositionHandler);
241
+ window.addEventListener('scroll', repositionHandler, true);
242
+
243
+ render(state);
244
+ };
245
+
246
+ window.TemporalPicker = {open, close};
247
+ })();
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module MapLibrePreview
2
- VERSION = '1.7.2'
2
+ VERSION = '1.9.0'
3
3
  end
@@ -7,12 +7,15 @@ html
7
7
  - base_path = request.script_name
8
8
  link[rel="icon" type="image/png" href="#{base_path}/icons/favicon.png"]
9
9
  link[href="#{base_path}/vendor/maplibre-gl/maplibre-gl.css" rel='stylesheet']
10
+ link[href="#{base_path}/css/temporal_picker.css" rel='stylesheet']
10
11
  script[src="#{base_path}/vendor/maplibre-gl/maplibre-gl.js"]
11
12
  script[src="#{base_path}/vendor/maplibre-contour/index.min.js"]
12
13
  script[src="#{base_path}/vendor/d3/d3.v7.min.js"]
14
+ script[src="#{base_path}/js/overlay_layout.js"]
13
15
  script[src="#{base_path}/js/filters.js"]
14
16
  script[src="#{base_path}/js/contour.js"]
15
17
  script[src="#{base_path}/js/tilegrid.js"]
18
+ script[src="#{base_path}/js/temporal_picker.js"]
16
19
  style
17
20
  | * { margin: 0; padding: 0; box-sizing: border-box; }
18
21
  | html { height: 100%; }
@@ -174,7 +177,7 @@ html
174
177
  | .controls a { color: #6897bb; text-decoration: none; margin-right: 15px; }
175
178
  | .controls a:hover { text-decoration: underline; }
176
179
  | .layer-controls-wrapper {
177
- | position: absolute; top: 50%; left: 10px; transform: translateY(-50%); z-index: 1000;
180
+ | position: static; z-index: 1000;
178
181
  | display: flex; align-items: flex-start; gap: 0;
179
182
  | }
180
183
  | .layer-controls {
@@ -204,6 +207,22 @@ html
204
207
  | .control-section-header {
205
208
  | display: flex; align-items: center; justify-content: space-between; gap: 8px;
206
209
  | }
210
+ | .overlay-managed-panel {
211
+ | z-index: 1000; will-change: left, top;
212
+ | }
213
+ | .overlay-dragging {
214
+ | cursor: grabbing;
215
+ | user-select: none;
216
+ | transition: none !important;
217
+ | }
218
+ | .overlay-panel-handle {
219
+ | cursor: grab; touch-action: none; user-select: none;
220
+ | -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;
221
+ | }
222
+ | .overlay-panel-handle:focus-visible {
223
+ | outline: 1px solid #6897bb;
224
+ | outline-offset: 2px;
225
+ | }
207
226
  | .control-section-title {
208
227
  | color: #ffc66d; font-size: 11px; font-weight: bold; line-height: 1.2;
209
228
  | text-transform: uppercase;
@@ -330,13 +349,21 @@ html
330
349
  | color: #a9b7c6; overflow: hidden;
331
350
  | }
332
351
  | .style-parameters-header {
333
- | display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 12px;
352
+ | display: flex; align-items: center; gap: 12px;
334
353
  | width: 100%; background: transparent; color: inherit; border: none;
335
- | padding: 10px 12px; cursor: pointer; text-align: left;
354
+ | padding: 10px 12px; text-align: left;
336
355
  | }
337
356
  | .style-parameters-header:hover {
338
357
  | background: rgba(75, 77, 79, 0.95);
339
358
  | }
359
+ | .style-parameters-toggle {
360
+ | display: inline-flex; align-items: center; justify-content: center;
361
+ | width: 22px; height: 22px; background: transparent; color: #a9b7c6;
362
+ | border: 1px solid transparent; border-radius: 2px; cursor: pointer;
363
+ | }
364
+ | .style-parameters-toggle:hover {
365
+ | background: #4b4d4f; border-color: #555555;
366
+ | }
340
367
  | .style-parameters-title {
341
368
  | color: #ffc66d; font-size: 11px; font-weight: bold; line-height: 1.2;
342
369
  | text-transform: uppercase;
@@ -362,17 +389,44 @@ html
362
389
  | .style-parameter-row {
363
390
  | display: flex; flex-direction: column; gap: 4px;
364
391
  | }
392
+ | .style-parameter-header {
393
+ | display: flex; align-items: center; justify-content: space-between; gap: 8px;
394
+ | }
365
395
  | .style-parameter-label {
366
396
  | color: #808080; font-size: 10px; font-weight: bold;
367
397
  | text-transform: uppercase;
368
398
  | }
399
+ | .style-parameter-counts {
400
+ | color: #6897bb; font-family: 'Courier New', monospace; font-size: 10px;
401
+ | white-space: nowrap;
402
+ | }
403
+ | .style-parameter-input-group {
404
+ | display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px;
405
+ | }
406
+ | .style-parameter-input-group.temporal {
407
+ | grid-template-columns: minmax(0, 1fr);
408
+ | }
369
409
  | .style-parameter-input {
370
410
  | width: 100%; background: #313335; color: #a9b7c6; border: 1px solid #555555;
371
411
  | border-radius: 3px; padding: 6px 8px; font-size: 11px; box-sizing: border-box;
412
+ | color-scheme: dark;
372
413
  | }
373
414
  | .style-parameter-input:focus {
374
415
  | outline: none; border-color: #6897bb; box-shadow: 0 0 0 1px #6897bb;
375
416
  | }
417
+ | .style-parameter-input.temporal {
418
+ | font-family: 'Courier New', monospace; font-size: 11px; cursor: pointer;
419
+ | }
420
+ | .style-parameter-context {
421
+ | display: flex; flex-direction: column; gap: 2px;
422
+ | color: #999999; font-size: 10px; line-height: 1.25;
423
+ | }
424
+ | .style-parameter-context-row {
425
+ | display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
426
+ | }
427
+ | .style-parameter-context-key {
428
+ | color: #808080; text-transform: uppercase; margin-right: 4px;
429
+ | }
376
430
  | .style-parameter-actions {
377
431
  | display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
378
432
  | }
@@ -380,13 +434,18 @@ html
380
434
  | flex: 1; min-height: 0; max-height: calc(80vh - 245px); width: max-content; max-width: 100%;
381
435
  | overflow: hidden;
382
436
  | }
383
- | .control-panel::-webkit-scrollbar { width: 8px; }
384
- | .control-panel::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
385
- | .control-panel::-webkit-scrollbar-thumb {
437
+ | .control-panel::-webkit-scrollbar,
438
+ | .style-parameters-body::-webkit-scrollbar { width: 8px; }
439
+ | .control-panel::-webkit-scrollbar-track,
440
+ | .style-parameters-body::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
441
+ | .control-panel::-webkit-scrollbar-thumb,
442
+ | .style-parameters-body::-webkit-scrollbar-thumb {
386
443
  | background: #555555; border-radius: 4px; border: 1px solid #3c3f41;
387
444
  | }
388
- | .control-panel::-webkit-scrollbar-thumb:hover { background: #666666; }
389
- | .control-panel { scrollbar-width: thin; scrollbar-color: #555555 #3c3f41; }
445
+ | .control-panel::-webkit-scrollbar-thumb:hover,
446
+ | .style-parameters-body::-webkit-scrollbar-thumb:hover { background: #666666; }
447
+ | .control-panel,
448
+ | .style-parameters-body { scrollbar-width: thin; scrollbar-color: #555555 #3c3f41; }
390
449
  | .control-panel.active { display: flex; }
391
450
  | .control-panel-header {
392
451
  | flex-shrink: 0; border-bottom: 1px solid #555555; padding-bottom: 8px; margin-bottom: 8px;
@@ -487,6 +546,10 @@ html
487
546
  | transition: width 0.3s ease; width: 0%;
488
547
  | }
489
548
  | /* MapLibre Controls */
549
+ | .maplibregl-ctrl-top-left, .maplibregl-ctrl-top-right,
550
+ | .maplibregl-ctrl-bottom-left, .maplibregl-ctrl-bottom-right {
551
+ | z-index: 1000 !important;
552
+ | }
490
553
  | .maplibregl-ctrl-scale, .maplibregl-ctrl-group, .maplibregl-ctrl-terrain, .maplibregl-ctrl-globe {
491
554
  | background: rgba(60, 63, 65, 0.95) !important; border: 1px solid #555555 !important;
492
555
  | border-radius: 4px !important;
@@ -512,10 +575,12 @@ html
512
575
  | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
513
576
  | border-radius: 6px; padding: 12px; width: 70vw; max-width: 800px;
514
577
  | backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
578
+ | overflow: hidden;
515
579
  | }
516
580
  | .profile-header {
517
- | display: flex; justify-content: space-between; align-items: center;
581
+ | display: flex; justify-content: flex-start; align-items: center;
518
582
  | margin-bottom: 8px; border-bottom: 1px solid #555555; padding-bottom: 8px;
583
+ | gap: 8px;
519
584
  | }
520
585
  | .profile-title { color: #ffc66d; font-weight: bold; font-size: 12px; }
521
586
  | .profile-close {
@@ -523,6 +588,7 @@ html
523
588
  | font-size: 14px; padding: 0; width: 16px; height: 16px;
524
589
  | display: flex; align-items: center; justify-content: center;
525
590
  | border-radius: 2px; transition: all 0.2s ease;
591
+ | margin-left: auto;
526
592
  | }
527
593
  | .profile-close:hover { background: #4b4d4f; color: #a9b7c6; }
528
594
  | .profile-stats {
@@ -567,7 +633,7 @@ html
567
633
  | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
568
634
  | }
569
635
  | .tilegrid-header {
570
- | display: flex; justify-content: space-between; align-items: center;
636
+ | display: flex; justify-content: flex-start; align-items: center; gap: 8px;
571
637
  | margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #555555;
572
638
  | }
573
639
  | .tilegrid-title {
@@ -578,6 +644,7 @@ html
578
644
  | font-size: 14px; padding: 0; width: 16px; height: 16px;
579
645
  | display: flex; align-items: center; justify-content: center;
580
646
  | border-radius: 2px; transition: all 0.2s ease;
647
+ | margin-left: auto;
581
648
  | }
582
649
  | .tilegrid-close:hover {
583
650
  | background: #4b4d4f; color: #a9b7c6;