opening_hours 3.10.0 → 3.11.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 +70 -0
- package/Makefile +7 -2
- package/README.md +14 -18
- package/build/opening_hours.esm.mjs +14896 -19393
- package/build/opening_hours.esm.mjs.map +1 -1
- package/build/opening_hours.js +14900 -19396
- package/package.json +2 -4
- package/site/js/countryToLanguageMapping.js +2 -3
- package/site/js/helpers.js +474 -313
- package/site/js/i18n-resources.js +554 -70
- package/site/js/main.js +379 -0
- package/site/js/opening_hours_table.js +233 -74
- package/site/js/theme.js +70 -0
- package/site/js/yohours_model.js +17 -15
- package/src/locales/i18n.js +62 -0
- package/src/locales/core.js +0 -20
package/site/js/main.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// Import all required modules
|
|
2
|
+
import i18next from '../../node_modules/i18next/dist/esm/i18next.bundled.js';
|
|
3
|
+
import { resources, detectLanguage, getUserSelectTranslateHTMLCode, changeLanguage } from './i18n-resources.js';
|
|
4
|
+
import { Evaluate, EX, josm, toggle, dateAtWeek, newValue, currentDateTime } from './helpers.js';
|
|
5
|
+
|
|
6
|
+
// Configuration constants
|
|
7
|
+
window.default_lat = 48.7769;
|
|
8
|
+
window.default_lon = 9.1844;
|
|
9
|
+
window.repo_url = 'https://github.com/opening-hours/opening_hours.js';
|
|
10
|
+
|
|
11
|
+
// Helper function to generate time navigation buttons
|
|
12
|
+
function generateTimeButtons() {
|
|
13
|
+
const buttons = [
|
|
14
|
+
[ 3600 * 24 * 365, 1, 'words.time.year' , 'year' ],
|
|
15
|
+
[ null , 1, 'words.time.month' , 'month' ], // Special handling for month
|
|
16
|
+
[ 3600 * 24 , 1, 'words.time.day' , 'day' ],
|
|
17
|
+
[ 3600 , 1, 'words.time.hour' , 'hour' ],
|
|
18
|
+
[ 60 , 1, 'words.time.minute' , 'minute' ],
|
|
19
|
+
[ 3600 * 24 * 7, 1, 'words.time.week' , 'week' ],
|
|
20
|
+
[ 0 , 0, 'words.time.now' , null ],
|
|
21
|
+
];
|
|
22
|
+
let html = '';
|
|
23
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
24
|
+
if (buttons[i][1] !== 0) {
|
|
25
|
+
const offset = buttons[i][0];
|
|
26
|
+
const field = buttons[i][3];
|
|
27
|
+
const isEditable = field === 'year' || field === 'day' || field === 'hour' || field === 'minute';
|
|
28
|
+
const labelText = i18next.t(buttons[i][2]);
|
|
29
|
+
const dataAttr = field === 'month' ? 'data-month-offset' : 'data-offset';
|
|
30
|
+
const minusVal = field === 'month' ? -1 : -offset;
|
|
31
|
+
const plusVal = field === 'month' ? 1 : offset;
|
|
32
|
+
|
|
33
|
+
html += `<div class="time-btn-wrapper">
|
|
34
|
+
<label class="time-btn-label-top">${labelText}</label>
|
|
35
|
+
<div class="time-btn-group">
|
|
36
|
+
<button type="button" class="time-btn time-btn-minus" ${dataAttr}="${minusVal}" title="-1 ${labelText}">−</button>
|
|
37
|
+
<span class="time-btn-label${isEditable ? ' editable' : ''}" id="time-btn-value-${field}" ${isEditable ? 'contenteditable="true" spellcheck="false"' : ''} data-field="${field}"></span>
|
|
38
|
+
<button type="button" class="time-btn time-btn-plus" ${dataAttr}="${plusVal}" title="+1 ${labelText}">+</button>
|
|
39
|
+
</div>`;
|
|
40
|
+
// Add weekday display below day button
|
|
41
|
+
if (buttons[i][3] === 'day') {
|
|
42
|
+
html += '<span class="time-btn-label-bottom" id="time-display-wday"></span>';
|
|
43
|
+
}
|
|
44
|
+
html += '</div>';
|
|
45
|
+
} else {
|
|
46
|
+
html += `<button type="button" class="time-btn time-btn-now">${i18next.t(buttons[i][2])}</button>`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return html;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Helper function to update time button labels with current values
|
|
53
|
+
|
|
54
|
+
export function updateTimeButtonLabels(date) {
|
|
55
|
+
const yearLabel = document.getElementById('time-btn-value-year');
|
|
56
|
+
const monthLabel = document.getElementById('time-btn-value-month');
|
|
57
|
+
const dayLabel = document.getElementById('time-btn-value-day');
|
|
58
|
+
const hourLabel = document.getElementById('time-btn-value-hour');
|
|
59
|
+
const minuteLabel = document.getElementById('time-btn-value-minute');
|
|
60
|
+
const weekLabel = document.getElementById('time-btn-value-week');
|
|
61
|
+
const wdayDisplay = document.getElementById('time-display-wday');
|
|
62
|
+
|
|
63
|
+
function u2(v) { return v >= 0 && v < 10 ? `0${v}` : v; }
|
|
64
|
+
|
|
65
|
+
if (yearLabel) yearLabel.textContent = currentDateTime.year;
|
|
66
|
+
if (monthLabel) {
|
|
67
|
+
monthLabel.textContent = new Date(2018, currentDateTime.month, 1)
|
|
68
|
+
.toLocaleString(i18next.language, {month: 'short'});
|
|
69
|
+
}
|
|
70
|
+
if (dayLabel) dayLabel.textContent = u2(currentDateTime.day);
|
|
71
|
+
if (hourLabel) hourLabel.textContent = u2(currentDateTime.hour);
|
|
72
|
+
if (minuteLabel) minuteLabel.textContent = u2(currentDateTime.minute);
|
|
73
|
+
|
|
74
|
+
if (weekLabel && date) {
|
|
75
|
+
weekLabel.textContent = `W${u2(dateAtWeek(date, 0) + 1)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (wdayDisplay && date) {
|
|
79
|
+
wdayDisplay.textContent = date.toLocaleString(i18next.language, {weekday: 'short'});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Helper function to generate mode selector options
|
|
84
|
+
function generateModeOptions() {
|
|
85
|
+
let options = '';
|
|
86
|
+
for (let i = 0; i <= 2; i++) {
|
|
87
|
+
options += `<option value="${i}">${i18next.t(`texts.mode ${i}`)}</option>`;
|
|
88
|
+
}
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Populate all dynamic content with localized text
|
|
93
|
+
function initializeUI() {
|
|
94
|
+
// Page title
|
|
95
|
+
document.getElementById('page-title').textContent = i18next.t('texts.title');
|
|
96
|
+
|
|
97
|
+
// Language selector
|
|
98
|
+
document.getElementById('language-selector').innerHTML = getUserSelectTranslateHTMLCode();
|
|
99
|
+
|
|
100
|
+
// Date and time inputs with navigation buttons only
|
|
101
|
+
document.getElementById('date-time-inputs').innerHTML = `
|
|
102
|
+
<h2>${i18next.t('words.date')} ${i18next.t('words.and')} ${i18next.t('words.time.time')}</h2>
|
|
103
|
+
` + generateTimeButtons();
|
|
104
|
+
|
|
105
|
+
// Position inputs
|
|
106
|
+
document.getElementById('position-inputs').innerHTML = `
|
|
107
|
+
<h2>${i18next.t('words.position')}</h2>
|
|
108
|
+
${i18next.t('words.lat')} <input type="number" class="input__coordinate" id="lat" value="${window.default_lat}" />
|
|
109
|
+
${i18next.t('words.lon')} <input type="number" class="input__coordinate" id="lon" value="${window.default_lon}" />
|
|
110
|
+
${i18next.t('words.country')} <input size="3" id="cc" readonly="readonly" />
|
|
111
|
+
${i18next.t('words.state')} <input size="20" id="state" readonly="readonly" /><br />
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
// Mode selector
|
|
115
|
+
document.getElementById('mode-selector').innerHTML = `
|
|
116
|
+
<h2>${i18next.t('words.mode')}</h2>
|
|
117
|
+
<select id="mode" name="mode" style="max-width:100%;">
|
|
118
|
+
${generateModeOptions()}
|
|
119
|
+
</select>
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// Value section heading
|
|
123
|
+
document.getElementById('value-section-heading').textContent = i18next.t('texts.value for') + ' „opening_hours“';
|
|
124
|
+
|
|
125
|
+
// Value label (hide - redundant with section heading)
|
|
126
|
+
document.getElementById('value-label').style.display = 'none';
|
|
127
|
+
|
|
128
|
+
// Actions section
|
|
129
|
+
document.getElementById('actions-heading').textContent = i18next.t('words.actions');
|
|
130
|
+
document.getElementById('compare-label').textContent = i18next.t('texts.value to compare');
|
|
131
|
+
document.getElementById('action-permalink-label').textContent = i18next.t('texts.share config');
|
|
132
|
+
document.getElementById('permalink-with-timestamp-label').textContent = i18next.t('texts.with timestamp');
|
|
133
|
+
document.getElementById('permalink-without-timestamp-label').textContent = i18next.t('texts.without timestamp');
|
|
134
|
+
|
|
135
|
+
// Set title attributes for all buttons with data-i18n-title
|
|
136
|
+
document.querySelectorAll('[data-i18n-title]').forEach(btn => {
|
|
137
|
+
const key = btn.getAttribute('data-i18n-title');
|
|
138
|
+
btn.title = i18next.t('texts.' + key);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Copy button handlers for permalink copy buttons
|
|
142
|
+
document.querySelectorAll('.copy-permalink-btn').forEach(btn => {
|
|
143
|
+
btn.addEventListener('click', function() {
|
|
144
|
+
const targetId = this.getAttribute('data-target');
|
|
145
|
+
const targetLink = document.getElementById(targetId);
|
|
146
|
+
if (targetLink) {
|
|
147
|
+
navigator.clipboard.writeText(targetLink.href).catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Copy and clear buttons for expression input
|
|
153
|
+
document.getElementById('copy-expression-btn').addEventListener('click', function() {
|
|
154
|
+
const expressionInput = document.getElementById('expression');
|
|
155
|
+
navigator.clipboard.writeText(expressionInput.value).catch(() => {});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
document.getElementById('clear-expression-btn').addEventListener('click', function() {
|
|
159
|
+
document.getElementById('expression').value = '';
|
|
160
|
+
Evaluate();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
document.getElementById('clear-diff-btn').addEventListener('click', function() {
|
|
164
|
+
document.getElementById('diff_value').value = '';
|
|
165
|
+
Evaluate();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Results section heading
|
|
169
|
+
document.getElementById('results-heading').textContent = i18next.t('words.results');
|
|
170
|
+
|
|
171
|
+
// Examples header
|
|
172
|
+
document.getElementById('examples').textContent = i18next.t('words.examples');
|
|
173
|
+
|
|
174
|
+
// Year ranges documentation link
|
|
175
|
+
const yearRangesDocu = document.getElementById('year-ranges-docu');
|
|
176
|
+
yearRangesDocu.href = `${window.repo_url}/tree/main#year-ranges`;
|
|
177
|
+
yearRangesDocu.textContent = i18next.t('words.docu');
|
|
178
|
+
|
|
179
|
+
// Example hints
|
|
180
|
+
document.getElementById('hint-error-correction-1').textContent = `(${i18next.t('texts.check out error correction, prettify')})`;
|
|
181
|
+
document.getElementById('hint-error-correction-2').textContent = `(${i18next.t('texts.check out error correction, prettify')})`;
|
|
182
|
+
document.getElementById('hint-ph-mo-fr').textContent = `(${i18next.t('texts.if PH is between Mo and Fr')})`;
|
|
183
|
+
document.getElementById('hint-sh-ph').textContent = `(${i18next.t('texts.SH,PH or PH,SH')})`;
|
|
184
|
+
|
|
185
|
+
// Footer content
|
|
186
|
+
document.getElementById('footer').innerHTML = i18next.t('texts.more information',
|
|
187
|
+
{ href: 'https://wiki.openstreetmap.org/wiki/Key:opening_hours' }) + '<br />' +
|
|
188
|
+
i18next.t('texts.this website', { url: window.repo_url, hoster: 'GitHub' });
|
|
189
|
+
|
|
190
|
+
document.body.parentElement.lang = i18next.language;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Set up event listeners using event delegation (only for repetitive handlers)
|
|
194
|
+
function setupEventListeners() {
|
|
195
|
+
const main = document.getElementById('user');
|
|
196
|
+
|
|
197
|
+
// Input field listeners
|
|
198
|
+
document.getElementById('expression').addEventListener('keyup', () => Evaluate());
|
|
199
|
+
document.getElementById('expression').addEventListener('blur', () => Evaluate());
|
|
200
|
+
document.getElementById('diff_value').addEventListener('keyup', () => Evaluate());
|
|
201
|
+
document.getElementById('diff_value').addEventListener('blur', () => Evaluate());
|
|
202
|
+
document.getElementById('lat').addEventListener('blur', () => Evaluate());
|
|
203
|
+
document.getElementById('lon').addEventListener('blur', () => Evaluate());
|
|
204
|
+
document.getElementById('mode').addEventListener('change', () => Evaluate());
|
|
205
|
+
|
|
206
|
+
// Language selector
|
|
207
|
+
document.getElementById('language-select').addEventListener('change', function() {
|
|
208
|
+
changeLanguage(this.value);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Use mousedown instead of click for prettified value elements
|
|
212
|
+
// This prevents interference with browser's text selection behavior
|
|
213
|
+
main.addEventListener('mousedown', (e) => {
|
|
214
|
+
// Prettified value display (code element)
|
|
215
|
+
if (e.target.closest('.prettified-value-display')) {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopImmediatePropagation();
|
|
218
|
+
const code = e.target.closest('.prettified-value-display');
|
|
219
|
+
newValue(code.dataset.value);
|
|
220
|
+
}
|
|
221
|
+
// Copy button for prettified value
|
|
222
|
+
else if (e.target.closest('.copy-prettified-btn')) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
e.stopImmediatePropagation();
|
|
225
|
+
const btn = e.target.closest('.copy-prettified-btn');
|
|
226
|
+
navigator.clipboard.writeText(btn.dataset.value).catch(() => {});
|
|
227
|
+
}
|
|
228
|
+
// Copy prettified value link
|
|
229
|
+
else if (e.target.closest('.copy-prettified-value')) {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
e.stopImmediatePropagation();
|
|
232
|
+
const link = e.target.closest('.copy-prettified-value');
|
|
233
|
+
newValue(link.dataset.value);
|
|
234
|
+
}
|
|
235
|
+
// Time jump links/buttons (from opening_hours_table.js)
|
|
236
|
+
else if (e.target.closest('.time-jump, .time-jump-btn')) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
e.stopImmediatePropagation();
|
|
239
|
+
const link = e.target.closest('.time-jump, .time-jump-btn');
|
|
240
|
+
Evaluate(parseInt(link.dataset.offset, 10), false);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
main.addEventListener('click', (e) => {
|
|
245
|
+
// Examples toggle
|
|
246
|
+
if (e.target.closest('#examples-toggle')) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
const toggleBtn = e.target.closest('#examples-toggle');
|
|
249
|
+
toggleBtn.classList.toggle('collapsed');
|
|
250
|
+
toggle('user_examples');
|
|
251
|
+
}
|
|
252
|
+
// Example links (60+ handlers → 1 listener)
|
|
253
|
+
else if (e.target.closest('.example-link')) {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
EX(e.target.closest('.example-link'));
|
|
256
|
+
}
|
|
257
|
+
// Time buttons
|
|
258
|
+
else if (e.target.closest('.time-btn')) {
|
|
259
|
+
const btn = e.target.closest('.time-btn');
|
|
260
|
+
if (btn.classList.contains('time-btn-now')) {
|
|
261
|
+
Evaluate(0, true);
|
|
262
|
+
} else if (btn.hasAttribute('data-month-offset')) {
|
|
263
|
+
// Month: use Date to handle day overflow automatically
|
|
264
|
+
const dt = currentDateTime;
|
|
265
|
+
const targetMonth = dt.month + parseInt(btn.dataset.monthOffset, 10);
|
|
266
|
+
const lastDay = new Date(dt.year, targetMonth + 1, 0).getDate();
|
|
267
|
+
const newDate = new Date(dt.year, targetMonth, Math.min(dt.day, lastDay), dt.hour, dt.minute);
|
|
268
|
+
currentDateTime.year = newDate.getFullYear();
|
|
269
|
+
currentDateTime.month = newDate.getMonth();
|
|
270
|
+
currentDateTime.day = newDate.getDate();
|
|
271
|
+
currentDateTime.hour = newDate.getHours();
|
|
272
|
+
currentDateTime.minute = newDate.getMinutes();
|
|
273
|
+
Evaluate();
|
|
274
|
+
} else {
|
|
275
|
+
Evaluate(parseInt(btn.dataset.offset, 10));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// JOSM link
|
|
279
|
+
else if (e.target.closest('.josm-link')) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
const link = e.target.closest('.josm-link');
|
|
282
|
+
josm(link.dataset.url);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Editable time values
|
|
287
|
+
main.addEventListener('keydown', (e) => {
|
|
288
|
+
if (e.target.classList.contains('time-btn-label') && e.target.hasAttribute('contenteditable')) {
|
|
289
|
+
// Allow Enter to commit the value
|
|
290
|
+
if (e.key === 'Enter') {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
e.target.blur();
|
|
293
|
+
}
|
|
294
|
+
// Allow Escape to cancel
|
|
295
|
+
else if (e.key === 'Escape') {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
e.target.textContent = e.target.dataset.originalValue || e.target.textContent;
|
|
298
|
+
e.target.blur();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
main.addEventListener('focus', (e) => {
|
|
304
|
+
if (e.target.classList.contains('time-btn-label') && e.target.hasAttribute('contenteditable')) {
|
|
305
|
+
// Store original value for Escape key
|
|
306
|
+
e.target.dataset.originalValue = e.target.textContent;
|
|
307
|
+
// Select all text for easy replacement
|
|
308
|
+
const range = document.createRange();
|
|
309
|
+
range.selectNodeContents(e.target);
|
|
310
|
+
const selection = window.getSelection();
|
|
311
|
+
selection.removeAllRanges();
|
|
312
|
+
selection.addRange(range);
|
|
313
|
+
}
|
|
314
|
+
}, true);
|
|
315
|
+
|
|
316
|
+
main.addEventListener('blur', (e) => {
|
|
317
|
+
if (e.target.classList.contains('time-btn-label') && e.target.hasAttribute('contenteditable')) {
|
|
318
|
+
const field = e.target.dataset.field;
|
|
319
|
+
const value = e.target.textContent.trim();
|
|
320
|
+
|
|
321
|
+
// Validate and update based on field type
|
|
322
|
+
if (field === 'year') {
|
|
323
|
+
const year = parseInt(value, 10);
|
|
324
|
+
if (!isNaN(year) && year >= 1970 && year <= 2100) {
|
|
325
|
+
currentDateTime.year = year;
|
|
326
|
+
Evaluate();
|
|
327
|
+
} else {
|
|
328
|
+
updateTimeButtonLabels();
|
|
329
|
+
}
|
|
330
|
+
} else if (field === 'day') {
|
|
331
|
+
const day = parseInt(value, 10);
|
|
332
|
+
if (!isNaN(day) && day >= 1 && day <= 31) {
|
|
333
|
+
currentDateTime.day = day;
|
|
334
|
+
Evaluate();
|
|
335
|
+
} else {
|
|
336
|
+
updateTimeButtonLabels();
|
|
337
|
+
}
|
|
338
|
+
} else if (field === 'hour') {
|
|
339
|
+
const hour = parseInt(value, 10);
|
|
340
|
+
if (!isNaN(hour) && hour >= 0 && hour <= 23) {
|
|
341
|
+
currentDateTime.hour = hour;
|
|
342
|
+
Evaluate();
|
|
343
|
+
} else {
|
|
344
|
+
updateTimeButtonLabels();
|
|
345
|
+
}
|
|
346
|
+
} else if (field === 'minute') {
|
|
347
|
+
const minute = parseInt(value, 10);
|
|
348
|
+
if (!isNaN(minute) && minute >= 0 && minute <= 59) {
|
|
349
|
+
currentDateTime.minute = minute;
|
|
350
|
+
Evaluate();
|
|
351
|
+
} else {
|
|
352
|
+
updateTimeButtonLabels();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
delete e.target.dataset.originalValue;
|
|
357
|
+
}
|
|
358
|
+
}, true);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Initialize application (ES6 modules execute after DOM parsing) */
|
|
362
|
+
await i18next.init({
|
|
363
|
+
lng: detectLanguage(),
|
|
364
|
+
fallbackLng: 'en',
|
|
365
|
+
resources: resources,
|
|
366
|
+
debug: false
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Update specification_url after i18next is ready
|
|
370
|
+
window.specification_url = `https://wiki.openstreetmap.org/wiki/${i18next.language === 'de' ? 'DE:' : ''}Key:opening_hours/specification`;
|
|
371
|
+
|
|
372
|
+
// Set page title
|
|
373
|
+
if (document.title !== i18next.t('texts.title')) {
|
|
374
|
+
document.title = i18next.t('texts.title');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
initializeUI();
|
|
378
|
+
setupEventListeners();
|
|
379
|
+
Evaluate();
|