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
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Import dependencies
|
|
2
|
+
import i18next from '../../node_modules/i18next/dist/esm/i18next.bundled.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const OpeningHoursTable = {
|
|
4
|
+
export const OpeningHoursTable = {
|
|
5
5
|
|
|
6
6
|
// JS functions for generating the table {{{
|
|
7
7
|
// In English. Localization is done somewhere else (above).
|
|
8
8
|
months: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'],
|
|
9
9
|
weekdays: ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'],
|
|
10
10
|
|
|
11
|
+
getLocalizedWeekday(date) {
|
|
12
|
+
return date.toLocaleString(i18next.language, { weekday: 'short' });
|
|
13
|
+
},
|
|
14
|
+
|
|
11
15
|
formatdate (now, nextchange, from) {
|
|
12
16
|
const now_daystart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
13
17
|
const nextdays = (nextchange.getTime() - now_daystart.getTime()) / 1000 / 60 / 60 / 24;
|
|
@@ -85,8 +89,8 @@ const OpeningHoursTable = {
|
|
|
85
89
|
return i18next.t(trans_base, {count: n});
|
|
86
90
|
},
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
//
|
|
92
|
+
toISODateString (date) {
|
|
93
|
+
// ISO 8601: https://xkcd.com/1179/
|
|
90
94
|
return `${date.getFullYear()}-${
|
|
91
95
|
this.pad(date.getMonth() + 1)}-${
|
|
92
96
|
this.pad(date.getDate())}`;
|
|
@@ -99,34 +103,35 @@ const OpeningHoursTable = {
|
|
|
99
103
|
this.pad(date.getSeconds())}`;
|
|
100
104
|
},
|
|
101
105
|
|
|
102
|
-
drawTable (it, date_today, has_next_change) {
|
|
106
|
+
drawTable (it, date_today, has_next_change, evalDate) {
|
|
103
107
|
date_today = new Date(date_today);
|
|
104
108
|
date_today.setHours(0, 0, 0, 0);
|
|
105
109
|
|
|
106
110
|
const date = new Date(date_today);
|
|
107
|
-
// date.setDate(date.getDate() - date.getDay() + 7);
|
|
108
111
|
date.setDate(date.getDate() - date.getDay() - 1); // start at begin of the week
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
// Calculate current time position for "now" marker (percentage of day)
|
|
114
|
+
// Use evalDate instead of new Date() to show the evaluation time, not browser time
|
|
115
|
+
const now = evalDate || new Date();
|
|
116
|
+
const nowPercent = ((now.getHours() * 60 + now.getMinutes()) / (24 * 60)) * 100;
|
|
117
|
+
|
|
118
|
+
const tableData = [];
|
|
111
119
|
|
|
112
120
|
for (let row = 0; row < 7; row++) {
|
|
113
121
|
date.setDate(date.getDate() + 1);
|
|
114
|
-
// if (date.getDay() === date_today.getDay()) {
|
|
115
|
-
// date.setDate(date.getDate()-7);
|
|
116
|
-
// }
|
|
117
122
|
|
|
118
123
|
it.setDate(date);
|
|
119
124
|
let is_open = it.getState();
|
|
120
125
|
let unknown = it.getUnknown();
|
|
121
|
-
let state_string = it.getStateString(
|
|
126
|
+
let state_string = it.getStateString(true);
|
|
122
127
|
let prevdate = date;
|
|
123
128
|
let curdate = date;
|
|
124
|
-
// console.log(state_string, is_open, unknown, date.toString());
|
|
125
129
|
|
|
126
|
-
|
|
130
|
+
const rowData = {
|
|
127
131
|
date: new Date(date),
|
|
128
|
-
times:
|
|
129
|
-
text: []
|
|
132
|
+
times: [],
|
|
133
|
+
text: [],
|
|
134
|
+
isToday: date.getDay() === date_today.getDay()
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
while (has_next_change && it.advance() && curdate.getTime() - date.getTime() < 24 * 60 * 60 * 1000) {
|
|
@@ -142,55 +147,89 @@ const OpeningHoursTable = {
|
|
|
142
147
|
fr *= 100 / 1000 / 60 / 60 / 24;
|
|
143
148
|
to *= 100 / 1000 / 60 / 60 / 24;
|
|
144
149
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
text += '24:00';
|
|
153
|
-
} else {
|
|
154
|
-
text += this.printTime(curdate);
|
|
155
|
-
}
|
|
150
|
+
const stateClass = is_open ? 'open' : (unknown ? 'unknown' : 'closed');
|
|
151
|
+
// Always use 24h format with HH:MM
|
|
152
|
+
const timeFrom = `${String(prevdate.getHours()).padStart(2, '0')}:${String(prevdate.getMinutes()).padStart(2, '0')}`;
|
|
153
|
+
const timeToDate = prevdate.getDay() !== curdate.getDay() ? null : curdate;
|
|
154
|
+
const timeTo = timeToDate
|
|
155
|
+
? `${String(timeToDate.getHours()).padStart(2, '0')}:${String(timeToDate.getMinutes()).padStart(2, '0')}`
|
|
156
|
+
: '24:00';
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
// Use current state_string for this period (before advancing)
|
|
159
|
+
const currentStateString = state_string;
|
|
160
|
+
const tooltip = `${i18next.t(`words.${currentStateString}`)}: ${timeFrom} - ${timeTo}`;
|
|
161
|
+
|
|
162
|
+
rowData.times.push(
|
|
163
|
+
`<div class="timebar ${stateClass}" style="width:${to - fr}%" title="${tooltip}"></div>`
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (is_open || unknown) {
|
|
167
|
+
const text = `${i18next.t(`words.${currentStateString}`)} ${i18next.t('words.from')} ${timeFrom} ${i18next.t('words.to')} ${timeTo}`;
|
|
168
|
+
rowData.text.push(text);
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
prevdate = curdate;
|
|
161
172
|
is_open = it.getState();
|
|
162
173
|
unknown = it.getUnknown();
|
|
163
|
-
state_string = it.getStateString(
|
|
174
|
+
state_string = it.getStateString(true);
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
if (!has_next_change &&
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
if (!has_next_change && rowData.text.length === 0) { // 24/7
|
|
178
|
+
const stateClass = is_open ? 'open' : (unknown ? 'unknown' : 'closed');
|
|
179
|
+
const tooltip = is_open ? `${i18next.t('words.open')}: 00:00 - 24:00` : '';
|
|
180
|
+
rowData.times.push(
|
|
181
|
+
`<div class="timebar ${stateClass}" style="width:100%" title="${tooltip}"></div>`
|
|
182
|
+
);
|
|
169
183
|
if (is_open) {
|
|
170
|
-
|
|
184
|
+
rowData.text.push(`${i18next.t('words.open')} 00:00 ${i18next.t('words.to')} 24:00`);
|
|
171
185
|
}
|
|
172
186
|
}
|
|
173
|
-
}
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
output += '<table>';
|
|
177
|
-
for (const row in table) {
|
|
178
|
-
const today = table[row].date.getDay() === date_today.getDay();
|
|
179
|
-
const endweek = (table[row].date.getDay() + 1) % 7 === date_today.getDay();
|
|
180
|
-
const cl = today ? ' class="today"' : (endweek ? ' class="endweek"' : '');
|
|
181
|
-
|
|
182
|
-
// if (today && date_today.getDay() !== 1)
|
|
183
|
-
// output += '<tr class="separator"><td colspan="3"></td></tr>';
|
|
184
|
-
output += `<tr${cl}><td class="day ${table[row].date.getDay() % 6 === 0 ? 'weekend' : 'workday'}">`;
|
|
185
|
-
output += this.printDate(table[row].date);
|
|
186
|
-
output += '</td><td class="times">';
|
|
187
|
-
output += table[row].times;
|
|
188
|
-
output += '</td><td>';
|
|
189
|
-
output += table[row].text.join(', ') || ' ';
|
|
190
|
-
output += '</td></tr>';
|
|
188
|
+
tableData.push(rowData);
|
|
191
189
|
}
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
|
|
191
|
+
// Build table HTML
|
|
192
|
+
const headerRow = `
|
|
193
|
+
<tr class="time-scale">
|
|
194
|
+
<td></td>
|
|
195
|
+
<td>
|
|
196
|
+
<div class="scale-labels">
|
|
197
|
+
<span>0h</span>
|
|
198
|
+
<span>6h</span>
|
|
199
|
+
<span>12h</span>
|
|
200
|
+
<span>18h</span>
|
|
201
|
+
<span>24h</span>
|
|
202
|
+
</div>
|
|
203
|
+
</td>
|
|
204
|
+
<td></td>
|
|
205
|
+
</tr>`;
|
|
206
|
+
|
|
207
|
+
const rows = tableData.map(row => {
|
|
208
|
+
const isToday = row.date.getDay() === date_today.getDay();
|
|
209
|
+
const isEndWeek = (row.date.getDay() + 1) % 7 === date_today.getDay();
|
|
210
|
+
const rowClass = isToday ? ' class="today"' : (isEndWeek ? ' class="endweek"' : '');
|
|
211
|
+
const dayClass = row.date.getDay() % 6 === 0 ? 'weekend' : 'workday';
|
|
212
|
+
const weekdayName = this.getLocalizedWeekday(row.date);
|
|
213
|
+
|
|
214
|
+
// Add "now" marker for today
|
|
215
|
+
const nowMarker = isToday
|
|
216
|
+
? `<div class="now-marker" style="left:${nowPercent}%" title="${i18next.t('words.time.now')}"></div>`
|
|
217
|
+
: '';
|
|
218
|
+
|
|
219
|
+
return `<tr${rowClass}>
|
|
220
|
+
<td class="day ${dayClass}">
|
|
221
|
+
<span class="weekday">${weekdayName}</span>
|
|
222
|
+
<span class="date">${this.toISODateString(row.date)}</span>
|
|
223
|
+
</td>
|
|
224
|
+
<td class="times">
|
|
225
|
+
${row.times.join('')}
|
|
226
|
+
${nowMarker}
|
|
227
|
+
</td>
|
|
228
|
+
<td class="description">${row.text.join(', ') || ' '}</td>
|
|
229
|
+
</tr>`;
|
|
230
|
+
}).join('');
|
|
231
|
+
|
|
232
|
+
return `<table class="opening-hours-table">${headerRow}${rows}</table>`;
|
|
194
233
|
},
|
|
195
234
|
|
|
196
235
|
getReadableState (startString, endString, oh, past) {
|
|
@@ -201,43 +240,78 @@ const OpeningHoursTable = {
|
|
|
201
240
|
return `${startString + output + endString}.`;
|
|
202
241
|
},
|
|
203
242
|
|
|
204
|
-
drawTableAndComments (oh, it,
|
|
243
|
+
drawTableAndComments (oh, it, evalDate) {
|
|
205
244
|
const prevdate = it.getDate();
|
|
206
245
|
const unknown = it.getUnknown();
|
|
246
|
+
const currentState = it.getState();
|
|
207
247
|
const state_string_past = it.getStateString(true);
|
|
208
248
|
const comment = it.getComment();
|
|
209
249
|
const has_next_change = it.advance();
|
|
210
250
|
|
|
211
251
|
let output = '';
|
|
212
252
|
|
|
213
|
-
|
|
253
|
+
// 1. Current status
|
|
254
|
+
output += `<p class="${state_string_past} status-info">${
|
|
214
255
|
i18next.t(`texts.${state_string_past} ${has_next_change ? 'now' : 'always'}`)}`;
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
output += i18next.t('texts.depends on', {comment: `"${comment}"`});
|
|
218
|
-
} else {
|
|
219
|
-
output += `, ${i18next.t('words.comment')}: "${comment}"`;
|
|
220
|
-
}
|
|
256
|
+
if (unknown) {
|
|
257
|
+
output += i18next.t('texts.depends on', {comment: `"${comment}"`});
|
|
221
258
|
}
|
|
222
259
|
output += '</p>';
|
|
223
260
|
|
|
261
|
+
// 2. Show reason (comment) if present and not unknown
|
|
262
|
+
if (typeof comment !== 'undefined' && !unknown) {
|
|
263
|
+
output += `<p class="status-reason">↳ ${i18next.t('texts.reason')}: ${comment}</p>`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 3. Find next REAL state change (not just interval boundary)
|
|
224
267
|
if (has_next_change) {
|
|
225
|
-
let
|
|
226
|
-
|
|
227
|
-
time_diff
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
268
|
+
let nextRealChangeDate = null;
|
|
269
|
+
let nextRealStateString = null;
|
|
270
|
+
let time_diff = 0;
|
|
271
|
+
|
|
272
|
+
// Check if immediate next change is a real state change
|
|
273
|
+
if (it.getState() !== currentState) {
|
|
274
|
+
nextRealChangeDate = it.getDate();
|
|
275
|
+
nextRealStateString = it.getStateString(false);
|
|
276
|
+
time_diff = (nextRealChangeDate.getTime() - prevdate.getTime()) / 1000 + 60;
|
|
277
|
+
} else {
|
|
278
|
+
// Keep advancing until we find a real state change
|
|
279
|
+
// Limit iterations to prevent infinite loops with complex values
|
|
280
|
+
const maxIterations = 1000;
|
|
281
|
+
let iterations = 0;
|
|
282
|
+
while (it.advance() && iterations < maxIterations) {
|
|
283
|
+
iterations++;
|
|
284
|
+
if (it.getState() !== currentState) {
|
|
285
|
+
nextRealChangeDate = it.getDate();
|
|
286
|
+
nextRealStateString = it.getStateString(false);
|
|
287
|
+
time_diff = (nextRealChangeDate.getTime() - prevdate.getTime()) / 1000 + 60;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (nextRealChangeDate) {
|
|
294
|
+
const timeString = this.formatdate(prevdate, nextRealChangeDate, true);
|
|
295
|
+
// Use "opens again" or "closes again" based on what will happen
|
|
296
|
+
const translationKey = nextRealStateString === 'open' ? 'texts.opens again' : 'texts.closes again';
|
|
297
|
+
const statusText = i18next.t(translationKey);
|
|
298
|
+
const buttonText = i18next.t('texts.jump to time');
|
|
299
|
+
|
|
300
|
+
const nextStateClass = nextRealStateString === 'open' ? 'opened' : 'closed';
|
|
301
|
+
output += `<p class="${nextStateClass} status-info next-change">
|
|
302
|
+
${statusText}: ${timeString}
|
|
303
|
+
<a href="#" class="time-jump-btn" data-offset="${time_diff}" title="${buttonText}">
|
|
304
|
+
${buttonText}
|
|
305
|
+
</a>
|
|
306
|
+
</p>`;
|
|
307
|
+
}
|
|
238
308
|
}
|
|
239
309
|
|
|
240
|
-
|
|
310
|
+
// Add upcoming changes timeline
|
|
311
|
+
const upcomingChanges = this.generateUpcomingChanges(oh, evalDate, 5);
|
|
312
|
+
output += this.generateUpcomingChangesHTML(upcomingChanges, evalDate);
|
|
313
|
+
|
|
314
|
+
output += this.drawTable(it, prevdate, has_next_change, evalDate);
|
|
241
315
|
|
|
242
316
|
if (oh.isWeekStable()) {
|
|
243
317
|
output += `<p><b>${i18next.t('texts.week stable')}</b></p>`;
|
|
@@ -247,5 +321,90 @@ const OpeningHoursTable = {
|
|
|
247
321
|
|
|
248
322
|
return output;
|
|
249
323
|
},
|
|
324
|
+
|
|
325
|
+
// Generate upcoming changes timeline {{{
|
|
326
|
+
generateUpcomingChanges(oh, currentDate, maxChanges = 5) {
|
|
327
|
+
const changes = [];
|
|
328
|
+
const it = oh.getIterator(currentDate);
|
|
329
|
+
const currentState = it.getState();
|
|
330
|
+
let previousState = currentState;
|
|
331
|
+
|
|
332
|
+
// Collect next changes (all interval boundaries, not just state changes)
|
|
333
|
+
let count = 0;
|
|
334
|
+
while (count < maxChanges && it.advance()) {
|
|
335
|
+
const changeDate = it.getDate();
|
|
336
|
+
const newState = it.getState();
|
|
337
|
+
const comment = it.getComment();
|
|
338
|
+
const stateString = it.getStateString(true); // Use past form for consistency
|
|
339
|
+
|
|
340
|
+
changes.push({
|
|
341
|
+
date: changeDate,
|
|
342
|
+
state: newState,
|
|
343
|
+
stateString: stateString,
|
|
344
|
+
comment: comment,
|
|
345
|
+
isActualStateChange: previousState !== newState
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
previousState = newState;
|
|
349
|
+
count++;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return changes;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
formatUpcomingChangeTime(currentDate, changeDate) {
|
|
356
|
+
const now_daystart = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
|
|
357
|
+
const change_daystart = new Date(changeDate.getFullYear(), changeDate.getMonth(), changeDate.getDate());
|
|
358
|
+
const daysDiff = Math.round((change_daystart.getTime() - now_daystart.getTime()) / (1000 * 60 * 60 * 24));
|
|
359
|
+
|
|
360
|
+
// Always use 24h format with HH:MM
|
|
361
|
+
const hours = String(changeDate.getHours()).padStart(2, '0');
|
|
362
|
+
const minutes = String(changeDate.getMinutes()).padStart(2, '0');
|
|
363
|
+
const timeStr = `${hours}:${minutes}`;
|
|
364
|
+
|
|
365
|
+
if (daysDiff === 0) {
|
|
366
|
+
return `${i18next.t('words.today')} ${timeStr}`;
|
|
367
|
+
} else if (daysDiff === 1) {
|
|
368
|
+
return `${i18next.t('words.tomorrow')} ${timeStr}`;
|
|
369
|
+
} else if (daysDiff === -1) {
|
|
370
|
+
return `${i18next.t('words.yesterday')} ${timeStr}`;
|
|
371
|
+
} else {
|
|
372
|
+
// For dates further away, show date + time
|
|
373
|
+
const dateStr = this.toISODateString(changeDate);
|
|
374
|
+
return `${dateStr} ${timeStr}`;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
generateUpcomingChangesHTML(changes, currentDate) {
|
|
379
|
+
if (changes.length === 0) return '';
|
|
380
|
+
|
|
381
|
+
let html = `<details class="upcoming-changes">
|
|
382
|
+
<summary>${i18next.t('texts.interval boundaries')}</summary>
|
|
383
|
+
<p class="timeline-hint">${i18next.t('texts.interval boundaries hint')}</p>
|
|
384
|
+
<ul class="timeline">`;
|
|
385
|
+
|
|
386
|
+
for (const change of changes) {
|
|
387
|
+
const timeStr = this.formatUpcomingChangeTime(currentDate, change.date);
|
|
388
|
+
const stateClass = change.state ? 'opened' : 'closed';
|
|
389
|
+
// Visual distinction: filled circle for real changes, empty for boundaries
|
|
390
|
+
const changeIcon = change.isActualStateChange ? '●' : '○';
|
|
391
|
+
const changeType = change.isActualStateChange ? 'state-change' : 'boundary-only';
|
|
392
|
+
const stateText = i18next.t(`words.${change.stateString}`);
|
|
393
|
+
const commentText = typeof change.comment === 'string'
|
|
394
|
+
? ` <span class="timeline-comment">(${change.comment})</span>`
|
|
395
|
+
: '';
|
|
396
|
+
|
|
397
|
+
html += `<li class="timeline-item ${stateClass} ${changeType}">
|
|
398
|
+
<span class="timeline-icon">${changeIcon}</span>
|
|
399
|
+
<span class="timeline-time">${timeStr}</span>
|
|
400
|
+
<span class="timeline-arrow">→</span>
|
|
401
|
+
<span class="timeline-state">${stateText}</span>${commentText}
|
|
402
|
+
</li>`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
html += '</ul></details>';
|
|
406
|
+
return html;
|
|
407
|
+
},
|
|
408
|
+
// }}}
|
|
250
409
|
// }}}
|
|
251
410
|
};
|
package/site/js/theme.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Kristjan ESPERANTO <https://github.com/KristjanESPERANTO>
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: LGPL-3.0-only
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Theme management: localStorage -> browser preference -> fallback (light)
|
|
8
|
+
(function() {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'theme-preference';
|
|
12
|
+
|
|
13
|
+
function getThemePreference() {
|
|
14
|
+
// 1. Check localStorage
|
|
15
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
16
|
+
if (stored) {
|
|
17
|
+
return stored;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. Check browser preference
|
|
21
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
22
|
+
return 'dark';
|
|
23
|
+
}
|
|
24
|
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
25
|
+
return 'light';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 3. Fallback to light
|
|
29
|
+
return 'light';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setTheme(theme) {
|
|
33
|
+
document.body.setAttribute('data-theme', theme);
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toggleTheme() {
|
|
38
|
+
const currentTheme = document.body.getAttribute('data-theme') || getThemePreference();
|
|
39
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
40
|
+
setTheme(newTheme);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Initialize theme immediately to avoid FOUC
|
|
44
|
+
const theme = getThemePreference();
|
|
45
|
+
setTheme(theme);
|
|
46
|
+
|
|
47
|
+
// Set up toggle button when DOM is ready
|
|
48
|
+
if (document.readyState === 'loading') {
|
|
49
|
+
document.addEventListener('DOMContentLoaded', initToggleButton);
|
|
50
|
+
} else {
|
|
51
|
+
initToggleButton();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function initToggleButton() {
|
|
55
|
+
const toggleBtn = document.getElementById('theme-toggle');
|
|
56
|
+
if (toggleBtn) {
|
|
57
|
+
toggleBtn.addEventListener('click', toggleTheme);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Listen for system theme changes (only if no localStorage preference)
|
|
61
|
+
if (!localStorage.getItem(STORAGE_KEY)) {
|
|
62
|
+
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
63
|
+
darkModeQuery.addEventListener('change', (e) => {
|
|
64
|
+
if (!localStorage.getItem(STORAGE_KEY)) {
|
|
65
|
+
setTheme(e.matches ? 'dark' : 'light');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})();
|
package/site/js/yohours_model.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* The days in a week
|
|
31
31
|
*/
|
|
32
|
-
DAYS = {
|
|
32
|
+
const DAYS = {
|
|
33
33
|
MONDAY: 0,
|
|
34
34
|
TUESDAY: 1,
|
|
35
35
|
WEDNESDAY: 2,
|
|
@@ -42,42 +42,42 @@ DAYS = {
|
|
|
42
42
|
/**
|
|
43
43
|
* The days in OSM
|
|
44
44
|
*/
|
|
45
|
-
OSM_DAYS = [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ];
|
|
45
|
+
const OSM_DAYS = [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ];
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* The days IRL
|
|
49
49
|
*/
|
|
50
|
-
IRL_DAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ];
|
|
50
|
+
const IRL_DAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ];
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* The month in OSM
|
|
54
54
|
*/
|
|
55
|
-
OSM_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
|
|
55
|
+
const OSM_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* The months IRL
|
|
59
59
|
*/
|
|
60
|
-
IRL_MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
|
|
60
|
+
const IRL_MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* The last day of month
|
|
64
64
|
*/
|
|
65
|
-
MONTH_END_DAY = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
|
|
65
|
+
const MONTH_END_DAY = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* The maximal minute that an interval can have
|
|
69
69
|
*/
|
|
70
|
-
MINUTES_MAX = 1440;
|
|
70
|
+
const MINUTES_MAX = 1440;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* The maximal value of days
|
|
74
74
|
*/
|
|
75
|
-
DAYS_MAX = 6;
|
|
75
|
+
const DAYS_MAX = 6;
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* The weekday ID for PH
|
|
79
79
|
*/
|
|
80
|
-
PH_WEEKDAY = -2;
|
|
80
|
+
const PH_WEEKDAY = -2;
|
|
81
81
|
|
|
82
82
|
/*
|
|
83
83
|
* ========== CLASSES ==========
|
|
@@ -717,7 +717,7 @@ var Day = function() {
|
|
|
717
717
|
this._intervals = [];
|
|
718
718
|
for(var i=0; i < intervals.length; i++) {
|
|
719
719
|
if(intervals[i] != undefined && intervals[i].getStartDay() == 0 && intervals[i].getEndDay() == 0) {
|
|
720
|
-
this._intervals.push(
|
|
720
|
+
this._intervals.push(structuredClone(intervals[i]));
|
|
721
721
|
}
|
|
722
722
|
}
|
|
723
723
|
|
|
@@ -1045,7 +1045,7 @@ var Week = function() {
|
|
|
1045
1045
|
this._intervals = [];
|
|
1046
1046
|
for(var i=0; i < intervals.length; i++) {
|
|
1047
1047
|
if(intervals[i] != undefined) {
|
|
1048
|
-
this._intervals.push(
|
|
1048
|
+
this._intervals.push(structuredClone(intervals[i]));
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
};
|
|
@@ -2177,7 +2177,7 @@ var OpeningHoursParser = function() {
|
|
|
2177
2177
|
|
|
2178
2178
|
var block, tokens, currentToken, ruleModifier, timeSelector, weekdaySelector, wideRangeSelector;
|
|
2179
2179
|
var singleTime, from, to, times;
|
|
2180
|
-
var singleWeekday, wdStart, wdEnd, holidays, weekdays;
|
|
2180
|
+
var singleWeekday, wdStart, wdEnd, wdFrom, wdTo, holidays, weekdays;
|
|
2181
2181
|
var monthSelector, weekSelector, weeks, singleWeek, weekFrom, weekTo, singleMonth, months, monthFrom, monthTo;
|
|
2182
2182
|
var dateRanges, dateRange, drObj, foundDateRange, resDrId;
|
|
2183
2183
|
|
|
@@ -2638,7 +2638,7 @@ var OpeningHoursParser = function() {
|
|
|
2638
2638
|
//Check added interval are OK for days
|
|
2639
2639
|
if(typical instanceof Day) {
|
|
2640
2640
|
if(weekdays.from != 0 || (weekdays.to != 0 && times.from <= times.to)) {
|
|
2641
|
-
weekdays =
|
|
2641
|
+
weekdays = Object.assign({}, weekdays);
|
|
2642
2642
|
weekdays.from = 0;
|
|
2643
2643
|
weekdays.to = (times.from <= times.to) ? 0 : 1;
|
|
2644
2644
|
}
|
|
@@ -2728,10 +2728,10 @@ var OpeningHoursParser = function() {
|
|
|
2728
2728
|
*/
|
|
2729
2729
|
OpeningHoursParser.prototype._tokenize = function(block) {
|
|
2730
2730
|
var result = block.trim().split(' ');
|
|
2731
|
-
var position =
|
|
2731
|
+
var position = result.indexOf("");
|
|
2732
2732
|
while( ~position ) {
|
|
2733
2733
|
result.splice(position, 1);
|
|
2734
|
-
position =
|
|
2734
|
+
position = result.indexOf("");
|
|
2735
2735
|
}
|
|
2736
2736
|
return result;
|
|
2737
2737
|
};
|
|
@@ -2784,3 +2784,5 @@ var YoHoursChecker = function() {
|
|
|
2784
2784
|
|
|
2785
2785
|
return result;
|
|
2786
2786
|
};
|
|
2787
|
+
|
|
2788
|
+
export { OpeningHoursBuilder, OpeningHoursParser, YoHoursChecker };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import opening_hours_resources from './opening_hours_resources.yaml';
|
|
2
|
+
|
|
3
|
+
const resources = opening_hours_resources;
|
|
4
|
+
|
|
5
|
+
// Simple i18n object compatible with the minimal features used in src/index.js
|
|
6
|
+
const i18n = {
|
|
7
|
+
language: 'en',
|
|
8
|
+
isInitialized: true,
|
|
9
|
+
|
|
10
|
+
t: function(key, variables) {
|
|
11
|
+
return this._translate(this.language, key, variables);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
getFixedT: function(locale) {
|
|
15
|
+
const self = this;
|
|
16
|
+
return function(key, variables) {
|
|
17
|
+
return self._translate(locale, key, variables);
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
_translate: function(locale, key, variables) {
|
|
22
|
+
// Handle array of keys (fallback mechanism)
|
|
23
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
24
|
+
|
|
25
|
+
for (const k of keys) {
|
|
26
|
+
// Parse namespace:path notation (e.g., "opening_hours:pretty.off")
|
|
27
|
+
const parts = k.split(':');
|
|
28
|
+
const namespace = parts.length > 1 ? parts[0] : 'opening_hours';
|
|
29
|
+
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
30
|
+
|
|
31
|
+
// Try to get translation
|
|
32
|
+
const translation = this._getNestedValue(resources, [locale, namespace, ...path.split('.')]);
|
|
33
|
+
|
|
34
|
+
if (translation !== undefined) {
|
|
35
|
+
// Replace variables like {{variable}} or {{-variable}}
|
|
36
|
+
// The minus prefix means "don't escape HTML" (compatibility feature)
|
|
37
|
+
if (typeof translation === 'string' && variables) {
|
|
38
|
+
return translation.replace(/{{-?([^{}]*)}}/g, function (match, varName) {
|
|
39
|
+
const trimmed = varName.trim();
|
|
40
|
+
return typeof variables[trimmed] !== 'undefined' ? variables[trimmed] : match;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return translation;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback: return the last key if no translation found
|
|
48
|
+
const lastKey = keys[keys.length - 1];
|
|
49
|
+
return lastKey.includes(':') ? lastKey.split(':')[1] : lastKey;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
_getNestedValue: function(obj, path) {
|
|
53
|
+
let current = obj;
|
|
54
|
+
for (const key of path) {
|
|
55
|
+
if (current === undefined || current === null) return undefined;
|
|
56
|
+
current = current[key];
|
|
57
|
+
}
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default i18n;
|
package/src/locales/core.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
|
-
export default i18next;
|
|
3
|
-
|
|
4
|
-
import opening_hours_resources from './opening_hours_resources.yaml';
|
|
5
|
-
|
|
6
|
-
if (!i18next.isInitialized) {
|
|
7
|
-
i18next.init({
|
|
8
|
-
fallbackLng: 'en',
|
|
9
|
-
// lngWhitelist: ['en', 'de'],
|
|
10
|
-
resources: opening_hours_resources,
|
|
11
|
-
getAsync: true,
|
|
12
|
-
useCookie: true,
|
|
13
|
-
// debug: true,
|
|
14
|
-
});
|
|
15
|
-
} else {
|
|
16
|
-
// compat with an app that already initializes i18n
|
|
17
|
-
for (const lang in opening_hours_resources) {
|
|
18
|
-
i18next.addResourceBundle(lang, 'opening_hours', opening_hours_resources[lang]['opening_hours'], true);
|
|
19
|
-
}
|
|
20
|
-
}
|