@yz-social/civildefense.io 4.5.7 → 4.5.9
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/package.json +2 -2
- package/public/about/en.html +1 -1
- package/public/about/es.html +1 -1
- package/public/index.html +102 -102
- package/public/javascripts/alert.js +7 -6
- package/public/javascripts/conversation.js +8 -5
- package/public/javascripts/hashtags.js +188 -21
- package/public/javascripts/map.js +4 -2
- package/public/javascripts/p2pWebNetwork.js +8 -0
- package/public/junk.html +145 -0
- package/public/material-combobox.html +379 -0
- package/public/service-worker.js +1 -1
- package/public/stylesheets/style.css +69 -8
- package/server/app.js +19 -5
- package/server/location.json +7 -0
- package/spec/conversationSpec.js +2 -2
|
@@ -44,6 +44,14 @@ export class P2PWebNetwork {
|
|
|
44
44
|
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
|
|
45
45
|
network.resetStatePromises();
|
|
46
46
|
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(region.lat, region.lng).toString(16)}.`);
|
|
47
|
+
peer.onError(error => {
|
|
48
|
+
network.info(`error: ${error.message || error}`);
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
//peer.onLog('debug', (...rest) => network.debug('DEBUG', ...rest));
|
|
52
|
+
//peer.onLog('info', (...rest) => network.debug('INFO', ...rest));
|
|
53
|
+
peer.onLog('warn', (...rest) => network.info('WARNING', ...rest));
|
|
54
|
+
peer.onLog('error', (...rest) => network.info('ERROR', ...rest));
|
|
47
55
|
const { peers, ms } = status;
|
|
48
56
|
network.info(`Connected ${peers} connections through ${bridgeUrl} in ${ms.toLocaleString()} ms.`);
|
|
49
57
|
network.attached(network);
|
package/public/junk.html
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Material Web Autocomplete Combo Box</title>
|
|
6
|
+
<script type="importmap">
|
|
7
|
+
{
|
|
8
|
+
"imports": {
|
|
9
|
+
"uuid": "https://unpkg.com/uuid@13.0.0/dist/index.js",
|
|
10
|
+
"@material/web/": "https://esm.run/@material/web/",
|
|
11
|
+
"s2js": "./s2js/s2js.esm.js",
|
|
12
|
+
"bigfloat": "./bigfloat/esm/index.js",
|
|
13
|
+
"leaflet": "./leaflet/leaflet-src.esm.js",
|
|
14
|
+
"minidenticons": "./minidenticons/minidenticons.min.js",
|
|
15
|
+
"@axona/protocol": "./axona-protocol/src/index.js",
|
|
16
|
+
"@axona/protocol/std": "./axona-protocol/std/index.js",
|
|
17
|
+
"@axona/protocol/connect.js": "./axona-protocol/src/connect.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
<script type="module">
|
|
22
|
+
import '@material/web/all.js';
|
|
23
|
+
import {styles as typescaleStyles} from '@material/web/typography/md-typescale-styles.js';
|
|
24
|
+
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
|
|
25
|
+
</script>
|
|
26
|
+
<!-- Load Material Web Components via CDN -->
|
|
27
|
+
<!-- <script type="module"> -->
|
|
28
|
+
<!-- import '@material/web/textfield/filled-text-field.js'; -->
|
|
29
|
+
<!-- import '@material/web/menu/menu.js'; -->
|
|
30
|
+
<!-- import '@material/web/menu/menu-item.js'; -->
|
|
31
|
+
<!-- </script> -->
|
|
32
|
+
<style>
|
|
33
|
+
.combo-box-container {
|
|
34
|
+
position: relative;
|
|
35
|
+
display: inline-block;
|
|
36
|
+
width: 300px;
|
|
37
|
+
margin: 40px;
|
|
38
|
+
}
|
|
39
|
+
md-filled-text-field {
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
md-menu {
|
|
43
|
+
/* Match the width of the textfield anchor */
|
|
44
|
+
--md-menu-container-width: 300px;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
|
|
50
|
+
<div class="combo-box-container">
|
|
51
|
+
<!-- The editable text input acting as the anchor -->
|
|
52
|
+
<md-filled-text-field
|
|
53
|
+
id="combo-input"
|
|
54
|
+
label="Choose a fruit"
|
|
55
|
+
placeholder="Type to search..."
|
|
56
|
+
autocomplete="off">
|
|
57
|
+
</md-filled-text-field>
|
|
58
|
+
|
|
59
|
+
<!-- The dropdown menu containing autocomplete suggestions -->
|
|
60
|
+
<md-menu id="combo-menu" anchor="combo-input" stay-open-on-outside-click>
|
|
61
|
+
<!-- Populated dynamically via JavaScript -->
|
|
62
|
+
</md-menu>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<script>
|
|
66
|
+
const input = document.getElementById('combo-input');
|
|
67
|
+
const menu = document.getElementById('combo-menu');
|
|
68
|
+
|
|
69
|
+
// Dataset for autocomplete
|
|
70
|
+
const itemsList = [
|
|
71
|
+
'Apple', 'Banana', 'Blueberry', 'Cherry', 'Grape',
|
|
72
|
+
'Lemon', 'Mango', 'Orange', 'Peach', 'Strawberry'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Build and filter menu items based on input value
|
|
76
|
+
function updateMenu(filterText = '') {
|
|
77
|
+
const normalizedFilter = filterText.toLowerCase().trim();
|
|
78
|
+
|
|
79
|
+
// Filter list items
|
|
80
|
+
const filtered = itemsList.filter(item =>
|
|
81
|
+
item.toLowerCase().includes(normalizedFilter)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Clear existing menu items
|
|
85
|
+
menu.innerHTML = '';
|
|
86
|
+
|
|
87
|
+
if (filtered.length === 0) {
|
|
88
|
+
// Show a disabled helper item if no match found
|
|
89
|
+
const noResult = document.createElement('md-menu-item');
|
|
90
|
+
noResult.disabled = true;
|
|
91
|
+
//noResult.headline = 'No matches found';
|
|
92
|
+
noResult.textContent = 'No matches found';
|
|
93
|
+
menu.appendChild(noResult);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Append matched options
|
|
98
|
+
filtered.forEach(item => {
|
|
99
|
+
const menuItem = document.createElement('md-menu-item');
|
|
100
|
+
//menuItem.headline = item;
|
|
101
|
+
menuItem.textContent = item;
|
|
102
|
+
// Store raw value on the element for extraction during selection
|
|
103
|
+
menuItem.dataset.value = item;
|
|
104
|
+
menu.appendChild(menuItem);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Filter list and open menu as user types
|
|
109
|
+
input.addEventListener('input', (e) => {
|
|
110
|
+
updateMenu(e.target.value);
|
|
111
|
+
if (!menu.open) {
|
|
112
|
+
menu.open = true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Re-open full menu list when user clicks or focuses inside the input
|
|
117
|
+
input.addEventListener('focus', () => {
|
|
118
|
+
updateMenu(input.value);
|
|
119
|
+
menu.open = true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Handle selection event from the Material menu
|
|
123
|
+
menu.addEventListener('close-menu', (e) => {
|
|
124
|
+
// e.detail.item holds the clicked md-menu-item instance
|
|
125
|
+
const selectedItem = e.detail.item;
|
|
126
|
+
if (selectedItem && selectedItem.dataset.value) {
|
|
127
|
+
input.value = selectedItem.dataset.value;
|
|
128
|
+
// Optionally trigger a change event for form tracking
|
|
129
|
+
input.dispatchEvent(new Event('change'));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Close the dropdown cleanly if the user hits "Escape" or "Enter" inside input
|
|
134
|
+
input.addEventListener('keydown', (e) => {
|
|
135
|
+
if (e.key === 'Escape' || e.key === 'Enter') {
|
|
136
|
+
menu.open = false;
|
|
137
|
+
if(e.key === 'Enter') input.blur();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Initial population of the menu
|
|
142
|
+
updateMenu();
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<title>Material Web Autocomplete Combo Box</title>
|
|
7
|
+
|
|
8
|
+
<!-- Material Web components (outlined text field + icon) -->
|
|
9
|
+
<script type="module" src="https://esm.run/@material/web/textfield/outlined-text-field.js"></script>
|
|
10
|
+
<script type="module" src="https://esm.run/@material/web/icon/icon.js"></script>
|
|
11
|
+
|
|
12
|
+
<!-- Roboto + Material Symbols, since Material Web expects these by default -->
|
|
13
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" />
|
|
14
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
:root {
|
|
18
|
+
color-scheme: light;
|
|
19
|
+
font-family: 'Roboto', system-ui, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html, body {
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
background: #f6f6f9;
|
|
29
|
+
/* Leave room at the bottom for the fixed field so page content
|
|
30
|
+
never sits underneath it. */
|
|
31
|
+
padding-bottom: 96px;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main.content {
|
|
36
|
+
max-width: 480px;
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
padding: 24px 20px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h1 {
|
|
42
|
+
font-size: 1.1rem;
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
color: #1c1b1f;
|
|
45
|
+
margin: 0 0 4px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
p.hint {
|
|
49
|
+
font-size: 0.85rem;
|
|
50
|
+
color: #49454f;
|
|
51
|
+
margin: 0 0 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* The field is pinned to the bottom of the *visual* viewport (see JS). */
|
|
55
|
+
/* position: fixed + bottom: 0 alone anchors to the bottom of the */
|
|
56
|
+
/* layout viewport, which on mobile stays put even when a keyboard */
|
|
57
|
+
/* slides up (some browsers just draw the keyboard on top of it, or */
|
|
58
|
+
/* slide the page instead). To keep the field visible directly above */
|
|
59
|
+
/* the keyboard, JS below tracks window.visualViewport and applies a */
|
|
60
|
+
/* translateY offset equal to the keyboard's height. */
|
|
61
|
+
|
|
62
|
+
.combobox {
|
|
63
|
+
position: fixed;
|
|
64
|
+
left: 0;
|
|
65
|
+
right: 0;
|
|
66
|
+
bottom: 0;
|
|
67
|
+
z-index: 10;
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
|
|
70
|
+
background: #f6f6f9;
|
|
71
|
+
box-shadow: 0 -1px 0 rgba(0,0,0,0.08);
|
|
72
|
+
max-width: 480px;
|
|
73
|
+
margin: 0 auto;
|
|
74
|
+
/* set by JS to compensate for the on-screen keyboard */
|
|
75
|
+
transform: translateY(0px);
|
|
76
|
+
will-change: transform;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
md-outlined-text-field {
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
The suggestion list is positioned independently (fixed, computed in
|
|
85
|
+
JS) rather than nested inside .combobox, so its top edge can be
|
|
86
|
+
clamped to the available space above the field regardless of how
|
|
87
|
+
tall the list's content is.
|
|
88
|
+
*/
|
|
89
|
+
.combobox-listbox {
|
|
90
|
+
position: fixed;
|
|
91
|
+
z-index: 20;
|
|
92
|
+
margin: 0;
|
|
93
|
+
padding: 8px 0;
|
|
94
|
+
list-style: none;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
background: #fff;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
box-shadow: 0 2px 6px 2px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.3);
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.combobox-listbox.open {
|
|
103
|
+
display: block;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.combobox-option {
|
|
107
|
+
padding: 10px 16px;
|
|
108
|
+
font-size: 0.95rem;
|
|
109
|
+
color: #1c1b1f;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
text-overflow: ellipsis;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.combobox-option mark {
|
|
117
|
+
background: none;
|
|
118
|
+
color: #6750a4;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.combobox-option:hover,
|
|
123
|
+
.combobox-option.active {
|
|
124
|
+
background: #eaddff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.combobox-empty {
|
|
128
|
+
padding: 10px 16px;
|
|
129
|
+
font-size: 0.9rem;
|
|
130
|
+
color: #79747e;
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
|
|
136
|
+
<main class="content">
|
|
137
|
+
<h1>Country picker</h1>
|
|
138
|
+
<p class="hint">
|
|
139
|
+
Start typing a country name. Use ↑ / ↓ to navigate, Enter to select, Esc to close.
|
|
140
|
+
The field stays pinned to the bottom of the screen, and the suggestion list opens
|
|
141
|
+
upward above it — try this on a phone to see it stay clear of the keyboard.
|
|
142
|
+
</p>
|
|
143
|
+
</main>
|
|
144
|
+
|
|
145
|
+
<div class="combobox">
|
|
146
|
+
<md-outlined-text-field
|
|
147
|
+
id="country-input"
|
|
148
|
+
label="Country"
|
|
149
|
+
role="combobox"
|
|
150
|
+
aria-expanded="false"
|
|
151
|
+
aria-controls="country-listbox"
|
|
152
|
+
aria-autocomplete="list"
|
|
153
|
+
autocomplete="off"
|
|
154
|
+
>
|
|
155
|
+
<md-icon slot="leading-icon">search</md-icon>
|
|
156
|
+
</md-outlined-text-field>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<ul class="combobox-listbox" id="country-listbox" role="listbox"></ul>
|
|
160
|
+
|
|
161
|
+
<script type="module">
|
|
162
|
+
// Wait for the custom element to be defined so its internal <input> exists.
|
|
163
|
+
await customElements.whenDefined('md-outlined-text-field');
|
|
164
|
+
|
|
165
|
+
const DATA = [
|
|
166
|
+
"Argentina","Australia","Austria","Bangladesh","Belgium","Brazil","Canada",
|
|
167
|
+
"Chile","China","Colombia","Denmark","Egypt","Finland","France","Germany",
|
|
168
|
+
"Greece","India","Indonesia","Ireland","Italy","Japan","Kenya","Malaysia",
|
|
169
|
+
"Mexico","Netherlands","New Zealand","Nigeria","Norway","Pakistan","Peru",
|
|
170
|
+
"Philippines","Poland","Portugal","Russia","Saudi Arabia","Singapore",
|
|
171
|
+
"South Africa","South Korea","Spain","Sweden","Switzerland","Thailand",
|
|
172
|
+
"Turkey","Ukraine","United Arab Emirates","United Kingdom","United States",
|
|
173
|
+
"Vietnam"
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const footer = document.querySelector('.combobox');
|
|
177
|
+
const field = document.getElementById('country-input');
|
|
178
|
+
const listbox = document.getElementById('country-listbox');
|
|
179
|
+
|
|
180
|
+
let matches = [];
|
|
181
|
+
let activeIndex = -1;
|
|
182
|
+
|
|
183
|
+
function getValue() {
|
|
184
|
+
return field.value ?? '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function escapeRegExp(str) {
|
|
188
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function highlight(text, query) {
|
|
192
|
+
if (!query) return text;
|
|
193
|
+
const re = new RegExp(`(${escapeRegExp(query)})`, 'ig');
|
|
194
|
+
return text.replace(re, '<mark>$1</mark>');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------
|
|
198
|
+
// Keyboard-aware positioning
|
|
199
|
+
// ---------------------------------------------------------------------
|
|
200
|
+
// window.visualViewport reports the *actually visible* area, shrinking
|
|
201
|
+
// when a mobile keyboard appears. window.innerHeight / layout viewport
|
|
202
|
+
// rects do not shrink the same way, which is why a plain
|
|
203
|
+
// `position: fixed; bottom: 0` element can end up hidden behind the
|
|
204
|
+
// keyboard. We measure the gap between the two and shift the field up
|
|
205
|
+
// by exactly that amount.
|
|
206
|
+
function keyboardInset() {
|
|
207
|
+
const vv = window.visualViewport;
|
|
208
|
+
if (!vv) return 0;
|
|
209
|
+
return Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function repositionField() {
|
|
213
|
+
footer.style.transform = `translateY(-${keyboardInset()}px)`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Positions the listbox so its bottom edge sits just above the field,
|
|
217
|
+
// and clamps its top edge (via max-height) to the visible viewport,
|
|
218
|
+
// so it never runs off-screen above a shrunken (keyboard-open) viewport.
|
|
219
|
+
function repositionListbox() {
|
|
220
|
+
const rect = field.getBoundingClientRect();
|
|
221
|
+
const vv = window.visualViewport;
|
|
222
|
+
const viewportTop = vv ? vv.offsetTop : 0;
|
|
223
|
+
const gap = 4;
|
|
224
|
+
const topMargin = 8;
|
|
225
|
+
|
|
226
|
+
listbox.style.left = `${rect.left}px`;
|
|
227
|
+
listbox.style.width = `${rect.width}px`;
|
|
228
|
+
listbox.style.bottom = `${window.innerHeight - rect.top + gap}px`;
|
|
229
|
+
|
|
230
|
+
const available = rect.top - viewportTop - gap - topMargin;
|
|
231
|
+
listbox.style.maxHeight = `${Math.max(80, available)}px`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function repositionAll() {
|
|
235
|
+
repositionField();
|
|
236
|
+
// Wait a frame so the field's transform has applied before we read
|
|
237
|
+
// its position for the listbox.
|
|
238
|
+
requestAnimationFrame(repositionListbox);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
window.visualViewport?.addEventListener('resize', repositionAll);
|
|
242
|
+
window.visualViewport?.addEventListener('scroll', repositionAll);
|
|
243
|
+
window.addEventListener('resize', repositionAll);
|
|
244
|
+
window.addEventListener('orientationchange', repositionAll);
|
|
245
|
+
repositionAll();
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------
|
|
248
|
+
// Combobox behavior
|
|
249
|
+
// ---------------------------------------------------------------------
|
|
250
|
+
function closeListbox() {
|
|
251
|
+
listbox.classList.remove('open');
|
|
252
|
+
field.setAttribute('aria-expanded', 'false');
|
|
253
|
+
field.removeAttribute('aria-activedescendant');
|
|
254
|
+
activeIndex = -1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function openListbox() {
|
|
258
|
+
listbox.classList.add('open');
|
|
259
|
+
field.setAttribute('aria-expanded', 'true');
|
|
260
|
+
repositionListbox();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function setActive(index) {
|
|
264
|
+
const options = listbox.querySelectorAll('.combobox-option');
|
|
265
|
+
options.forEach(opt => opt.classList.remove('active'));
|
|
266
|
+
activeIndex = index;
|
|
267
|
+
if (index >= 0 && options[index]) {
|
|
268
|
+
options[index].classList.add('active');
|
|
269
|
+
options[index].scrollIntoView({ block: 'nearest' });
|
|
270
|
+
field.setAttribute('aria-activedescendant', options[index].id);
|
|
271
|
+
} else {
|
|
272
|
+
field.removeAttribute('aria-activedescendant');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function selectValue(value) {
|
|
277
|
+
field.value = value;
|
|
278
|
+
closeListbox();
|
|
279
|
+
field.focus();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderMatches(query) {
|
|
283
|
+
listbox.innerHTML = '';
|
|
284
|
+
|
|
285
|
+
if (!query) {
|
|
286
|
+
closeListbox();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
matches = DATA.filter(item =>
|
|
291
|
+
item.toLowerCase().includes(query.toLowerCase())
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (matches.length === 0) {
|
|
295
|
+
const li = document.createElement('li');
|
|
296
|
+
li.className = 'combobox-empty';
|
|
297
|
+
li.textContent = 'No matches found';
|
|
298
|
+
listbox.appendChild(li);
|
|
299
|
+
openListbox();
|
|
300
|
+
setActive(-1);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
matches.forEach((item, i) => {
|
|
305
|
+
const li = document.createElement('li');
|
|
306
|
+
li.className = 'combobox-option';
|
|
307
|
+
li.id = `country-option-${i}`;
|
|
308
|
+
li.setAttribute('role', 'option');
|
|
309
|
+
li.innerHTML = highlight(item, query);
|
|
310
|
+
li.addEventListener('mousedown', (e) => {
|
|
311
|
+
// mousedown (not click) so it fires before the field's blur event
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
selectValue(item);
|
|
314
|
+
});
|
|
315
|
+
listbox.appendChild(li);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
openListbox();
|
|
319
|
+
setActive(-1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
field.addEventListener('input', () => {
|
|
323
|
+
renderMatches(getValue());
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// The keyboard opening/closing changes visualViewport size, which
|
|
327
|
+
// fires our resize listener above and keeps everything positioned
|
|
328
|
+
// correctly without any extra work here.
|
|
329
|
+
field.addEventListener('focus', () => {
|
|
330
|
+
if (getValue()) renderMatches(getValue());
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
field.addEventListener('keydown', (e) => {
|
|
334
|
+
const isOpen = listbox.classList.contains('open');
|
|
335
|
+
const optionCount = listbox.querySelectorAll('.combobox-option').length;
|
|
336
|
+
|
|
337
|
+
switch (e.key) {
|
|
338
|
+
case 'ArrowDown':
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
if (!isOpen) { renderMatches(getValue()); return; }
|
|
341
|
+
if (optionCount > 0) setActive((activeIndex + 1) % optionCount);
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case 'ArrowUp':
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
if (!isOpen) { renderMatches(getValue()); return; }
|
|
347
|
+
if (optionCount > 0) setActive((activeIndex - 1 + optionCount) % optionCount);
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'Enter':
|
|
351
|
+
if (isOpen && activeIndex >= 0 && matches[activeIndex]) {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
selectValue(matches[activeIndex]);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case 'Escape':
|
|
358
|
+
if (isOpen) {
|
|
359
|
+
e.preventDefault();
|
|
360
|
+
closeListbox();
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case 'Tab':
|
|
365
|
+
closeListbox();
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Close when clicking outside the combobox/listbox.
|
|
371
|
+
document.addEventListener('click', (e) => {
|
|
372
|
+
if (!e.target.closest('.combobox') && !e.target.closest('.combobox-listbox')) {
|
|
373
|
+
closeListbox();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
</script>
|
|
377
|
+
|
|
378
|
+
</body>
|
|
379
|
+
</html>
|
package/public/service-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { Request, Response, URL, clients} = self;
|
|
2
2
|
// Little point in trying to automatically pull this through package.json, as we need a byte change in THIS file to trigger a new worker.
|
|
3
|
-
const serviceVersion = '4.5.
|
|
3
|
+
const serviceVersion = '4.5.9';
|
|
4
4
|
|
|
5
5
|
const cacheList = [ // The files we need.
|
|
6
6
|
"/",
|
|
@@ -72,13 +72,31 @@ md-outlined-text-field {
|
|
|
72
72
|
max-height: 80vh;
|
|
73
73
|
overflow: auto;
|
|
74
74
|
}
|
|
75
|
+
/* Safari. Example dimensions are iPhone 17 pro max simulator */
|
|
76
|
+
/* In landscape, regardless of compact/bottom/top setting: */
|
|
77
|
+
/* - There is a thick tabbed address bar above map. */
|
|
78
|
+
/* - Notch partially obscures logo in landscape-right, but that's ok */
|
|
79
|
+
/* - Height should be dvh (956 x 330), as lvh (440) extends below bottom of screen. */
|
|
80
|
+
/* In portrait, regardless of compact/bottom/top setting: */
|
|
81
|
+
/* - There is a white space at top and bottom that is a little larger than the stuff controls displayed in it. */
|
|
82
|
+
/* - If height of map, etc, is dvh (440 x 796), everything works, including keyboard appearance and dismissal. */
|
|
83
|
+
/* Position .watching-hashtags box bottom:18px to leave space for map attribution. */
|
|
84
|
+
/* - If height of map is lvh (440 x 836) and #subPopoverControls remains dvh (.watching-hashtags bottom:0), */
|
|
85
|
+
/* then the map has a nice-ish fade under the bottom controls, without losing so much to whitespace. */
|
|
86
|
+
/* BUT: */
|
|
87
|
+
/* - It still has a hard edge to start of white, that is now directly under controls, which is ugly. */
|
|
88
|
+
/* - The map attribution is under the controls, which isn't fair. */
|
|
89
|
+
/* - I don't know how to make the keyboard work without covering the input box. */
|
|
75
90
|
#map {
|
|
76
|
-
width:
|
|
77
|
-
height:
|
|
91
|
+
width: 100dvw;
|
|
92
|
+
height: 100dvh;
|
|
78
93
|
position: absolute;
|
|
79
94
|
top: 0;
|
|
80
95
|
left: 0;
|
|
81
96
|
}
|
|
97
|
+
#mapCapture {
|
|
98
|
+
height: 100dvh;
|
|
99
|
+
}
|
|
82
100
|
|
|
83
101
|
.info-banner {
|
|
84
102
|
position: absolute;
|
|
@@ -115,9 +133,6 @@ md-outlined-text-field {
|
|
|
115
133
|
transition: opacity 2s;
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
#mapCapture {
|
|
119
|
-
height: 100vh;
|
|
120
|
-
}
|
|
121
136
|
#mapCapture > img {
|
|
122
137
|
position: absolute;
|
|
123
138
|
opacity: 0;
|
|
@@ -155,6 +170,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
|
|
|
155
170
|
height: 100dvh;
|
|
156
171
|
position: fixed;
|
|
157
172
|
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.5));
|
|
173
|
+
--mapAttributionHeight: 18px;
|
|
158
174
|
}
|
|
159
175
|
#subPopoverControls > * > * {
|
|
160
176
|
pointer-events: auto;
|
|
@@ -170,19 +186,64 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
|
|
|
170
186
|
.watching-hashtags {
|
|
171
187
|
position: absolute;
|
|
172
188
|
width: calc(100% - 130px);
|
|
173
|
-
bottom: calc((sign(calc(100vh - 100dvh)) - 1) * -18px);
|
|
189
|
+
/*bottom: calc((sign(calc(100vh - 100dvh)) - 1) * -18px); *//* 18px for attribution in desktop, none if iphone dynamic viewsize. */
|
|
190
|
+
bottom: var(--mapAttributionHeight);
|
|
174
191
|
right: 5px;
|
|
175
192
|
flex-direction: row-reverse;
|
|
176
193
|
flex-wrap: wrap-reverse;
|
|
177
194
|
align-items: flex-end;
|
|
178
195
|
}
|
|
196
|
+
.combobox {
|
|
197
|
+
position: relative;
|
|
198
|
+
--textFieldHeight: 34px;
|
|
199
|
+
--minWidth: 125px;
|
|
200
|
+
}
|
|
201
|
+
.combobox-listbox {
|
|
202
|
+
padding: 5px;
|
|
203
|
+
position: absolute;
|
|
204
|
+
bottom: calc(var(--textFieldHeight) + 2px);
|
|
205
|
+
left: 0;
|
|
206
|
+
max-height: 50dvh; /* leaving room for portrait keyboard. landscape keyboard has no room at all */
|
|
207
|
+
width: fit-content;
|
|
208
|
+
min-width: var(--minWidth);
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
z-index: 9999;
|
|
211
|
+
background: rgba(255, 255, 255, 0.95);
|
|
212
|
+
border-top-left-radius: 12px;
|
|
213
|
+
border-top-right-radius: 12px;
|
|
214
|
+
}
|
|
215
|
+
.combobox-option {
|
|
216
|
+
font-size: 0.95rem;
|
|
217
|
+
color: var(--md-sys-color-primary);
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
.combobox-option minidenticon-svg {
|
|
222
|
+
display: inline-block;
|
|
223
|
+
position: relative;
|
|
224
|
+
top: 5px;
|
|
225
|
+
height: 20px;
|
|
226
|
+
width: 20px;
|
|
227
|
+
}
|
|
228
|
+
.combobox-option mark {
|
|
229
|
+
background: none;
|
|
230
|
+
color: var(--md-sys-color-secondary);
|
|
231
|
+
font-weight: 900;
|
|
232
|
+
}
|
|
233
|
+
.combobox-empty {
|
|
234
|
+
font-style: italic;
|
|
235
|
+
}
|
|
236
|
+
.combobox-option:hover,
|
|
237
|
+
.combobox-option.active {
|
|
238
|
+
background: #B8C2D7;
|
|
239
|
+
}
|
|
179
240
|
.watching-hashtags md-filled-text-field.newtag {
|
|
180
241
|
--md-filled-field-top-space: 0px;
|
|
181
242
|
--md-filled-field-bottom-space: 0px;
|
|
182
243
|
--md-filled-text-field-leading-space: 8px;
|
|
183
244
|
--md-filled-text-field-trailing-space: 0px;
|
|
184
|
-
|
|
185
|
-
|
|
245
|
+
height: var(--textFieldHeight);
|
|
246
|
+
width: var(--minWidth);
|
|
186
247
|
}
|
|
187
248
|
.alert-marker {
|
|
188
249
|
text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
|