rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1/*! Fiesta 1.0.0 | MPL-2.0 | https://fiesta.rastrillo.org */
2(function (global) {
3 "use strict";
4
5
6 function firstDayOfWeek(locale) {
7 var info = null, loc;
8 try {
9 loc = new Intl.Locale(locale || "en");
10 info = typeof loc.getWeekInfo === "function" ? loc.getWeekInfo() : loc.weekInfo;
11 } catch (e) {
12 info = null;
13 }
14 if (info && typeof info.firstDay === "number" && info.firstDay >= 1 && info.firstDay <= 7) {
15 return info.firstDay % 7;
16 }
17 return 1;
18 }
19
20 function monthGrid(year, month, first) {
21 var start = new Date(year, month, 1);
22 var back = (start.getDay() - first + 7) % 7;
23 var out = [], i;
24 for (i = 0; i < 42; i++) out.push(new Date(year, month, 1 - back + i));
25 return out;
26 }
27
28 function weekdayNames(locale, first) {
29 var out = [], i, day, probe, shortFmt = null, longFmt = null;
30 try {
31 shortFmt = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
32 longFmt = new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" });
33 } catch (e) {
34 shortFmt = null;
35 longFmt = null;
36 }
37 for (i = 0; i < 7; i++) {
38 day = (first + i) % 7;
39 probe = new Date(Date.UTC(2026, 1, 1 + day));
40 out.push({
41 day: day,
42 short: shortFmt ? shortFmt.format(probe) : String(day),
43 long: longFmt ? longFmt.format(probe) : String(day)
44 });
45 }
46 return out;
47 }
48
49
50 if (typeof document !== "undefined") {
51
52 var CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
53 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' +
54 '<path d="M15 5l-7 7 7 7"/></svg>';
55
56
57 function pad(n) { return (n < 10 ? "0" : "") + n; }
58
59 function dayKey(d) {
60 return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
61 }
62
63 function sameDay(a, b) { return !!a && !!b && dayKey(a) === dayKey(b); }
64
65 function shift(d, days) {
66 return new Date(d.getFullYear(), d.getMonth(), d.getDate() + days);
67 }
68
69 function shiftMonths(d, months) {
70 var y = d.getFullYear(), m = d.getMonth() + months, day = d.getDate();
71 var last = new Date(y, m + 1, 0).getDate();
72 return new Date(y, m, day > last ? last : day);
73 }
74
75 function fmt(locale, opts) {
76 try {
77 return new Intl.DateTimeFormat(locale, opts);
78 } catch (e) {
79 return null;
80 }
81 }
82
83 function makePanel(cfg) {
84 var locale = cfg.locale;
85 var first = firstDayOfWeek(locale);
86 var heads = weekdayNames(locale, first);
87 var titleFmt = fmt(locale, { month: "long", year: "numeric" });
88 var fullFmt = fmt(locale, { weekday: "long", day: "numeric", month: "long", year: "numeric" });
89 var numFmt;
90 try {
91 numFmt = new Intl.NumberFormat(locale, { useGrouping: false });
92 } catch (e) {
93 numFmt = null;
94 }
95
96 var view = null;
97 var cursor = null;
98 var preview = null;
99 var cells = [];
100
101 var el = document.createElement("div");
102 el.setAttribute("rst-cal", "");
103 el.hidden = true;
104 el.setAttribute("role", "dialog");
105 el.setAttribute("aria-label", cfg.labels.calendar);
106
107 var head = document.createElement("div");
108 head.setAttribute("rst-cal-head", "");
109
110 function navButton(label, which) {
111 var b = document.createElement("button");
112 b.type = "button";
113 b.setAttribute("rst-cal-nav", which);
114 b.setAttribute("aria-label", label);
115 b.innerHTML = CHEVRON;
116 return b;
117 }
118
119 var prevBtn = navButton(cfg.labels.prev, "prev");
120 var nextBtn = navButton(cfg.labels.next, "next");
121
122 var title = document.createElement("div");
123 title.setAttribute("rst-cal-title", "");
124 title.id = cfg.id + "-cal-title";
125 title.setAttribute("aria-live", "polite");
126
127 head.appendChild(prevBtn);
128 head.appendChild(title);
129 head.appendChild(nextBtn);
130
131 var table = document.createElement("table");
132 table.setAttribute("rst-cal-grid", "");
133 table.setAttribute("role", "grid");
134 table.setAttribute("aria-labelledby", title.id);
135
136 var thead = document.createElement("thead");
137 var headRow = document.createElement("tr");
138 headRow.setAttribute("role", "row");
139 for (var h = 0; h < heads.length; h++) {
140 var th = document.createElement("th");
141 th.setAttribute("scope", "col");
142 th.setAttribute("role", "columnheader");
143 var eye = document.createElement("span");
144 eye.setAttribute("aria-hidden", "true");
145 eye.textContent = heads[h].short;
146 var said = document.createElement("span");
147 said.className = "rst-sr-only";
148 said.textContent = heads[h].long;
149 th.appendChild(eye);
150 th.appendChild(said);
151 headRow.appendChild(th);
152 }
153 thead.appendChild(headRow);
154
155 var tbody = document.createElement("tbody");
156 table.appendChild(thead);
157 table.appendChild(tbody);
158 el.appendChild(head);
159 el.appendChild(table);
160
161 function outOfBounds(d) {
162 var b = cfg.bounds ? cfg.bounds() : null;
163 if (!b) return false;
164 if (b.min && dayKey(d) < dayKey(b.min)) return true;
165 if (b.max && dayKey(d) > dayKey(b.max)) return true;
166 return false;
167 }
168
169 function monthReachable(d) {
170 var b = cfg.bounds ? cfg.bounds() : null;
171 if (!b) return true;
172 var lo = new Date(d.getFullYear(), d.getMonth(), 1);
173 var hi = new Date(d.getFullYear(), d.getMonth() + 1, 0);
174 if (b.min && dayKey(hi) < dayKey(b.min)) return false;
175 if (b.max && dayKey(lo) > dayKey(b.max)) return false;
176 return true;
177 }
178
179 function render() {
180 var today = new Date();
181 var chosen = cfg.value ? cfg.value() : null;
182 var days = monthGrid(view.getFullYear(), view.getMonth(), first);
183 var i, row, td, d, focusable = -1;
184
185 title.textContent = titleFmt ? titleFmt.format(view)
186 : view.getFullYear() + "-" + pad(view.getMonth() + 1);
187
188 prevBtn.disabled = !monthReachable(shiftMonths(new Date(view.getFullYear(), view.getMonth(), 1), -1));
189 nextBtn.disabled = !monthReachable(shiftMonths(new Date(view.getFullYear(), view.getMonth(), 1), 1));
190
191 tbody.textContent = "";
192 cells = [];
193 for (i = 0; i < days.length; i++) {
194 if (i % 7 === 0) {
195 row = document.createElement("tr");
196 row.setAttribute("role", "row");
197 tbody.appendChild(row);
198 }
199 d = days[i];
200 td = document.createElement("td");
201 td.setAttribute("rst-cal-day", "");
202 td.setAttribute("role", "gridcell");
203 td.tabIndex = -1;
204 td.textContent = numFmt ? numFmt.format(d.getDate()) : String(d.getDate());
205 td.setAttribute("aria-label", fullFmt ? fullFmt.format(d) : dayKey(d));
206 td.setAttribute("data-rst-day", dayKey(d));
207
208 if (d.getMonth() !== view.getMonth()) td.classList.add("is-outside");
209 if (sameDay(d, today)) {
210 td.classList.add("is-today");
211 td.setAttribute("aria-current", "date");
212 }
213 td.setAttribute("aria-selected", sameDay(d, chosen) ? "true" : "false");
214 if (sameDay(d, chosen)) td.classList.add("is-selected");
215 if (sameDay(d, preview)) td.classList.add("is-preview");
216 if (outOfBounds(d)) {
217 td.classList.add("is-disabled");
218 td.setAttribute("aria-disabled", "true");
219 }
220 if (sameDay(d, cursor)) focusable = i;
221 row.appendChild(td);
222 cells.push({ td: td, date: d });
223 }
224 if (focusable < 0) {
225 for (i = 0; i < cells.length; i++) {
226 if (cells[i].date.getMonth() === view.getMonth()) { focusable = i; break; }
227 }
228 if (focusable >= 0) cursor = cells[focusable].date;
229 }
230 if (focusable >= 0) cells[focusable].td.tabIndex = 0;
231 }
232
233 function cellFor(d) {
234 var key = dayKey(d), i;
235 for (i = 0; i < cells.length; i++) if (dayKey(cells[i].date) === key) return cells[i].td;
236 return null;
237 }
238
239 function moveTo(d, focus) {
240 cursor = d;
241 if (d.getFullYear() !== view.getFullYear() || d.getMonth() !== view.getMonth()) {
242 view = new Date(d.getFullYear(), d.getMonth(), 1);
243 }
244 render();
245 if (focus) {
246 var td = cellFor(d);
247 if (td) td.focus();
248 }
249 }
250
251 function pick(d) {
252 if (outOfBounds(d)) return;
253 if (cfg.onPick) cfg.onPick(new Date(d.getFullYear(), d.getMonth(), d.getDate()));
254 }
255
256 function dayAt(node) {
257 while (node && node !== tbody) {
258 if (node.getAttribute && node.getAttribute("data-rst-day")) return node;
259 node = node.parentNode;
260 }
261 return null;
262 }
263
264 tbody.addEventListener("mousedown", function (e) {
265 var td = dayAt(e.target);
266 if (td) e.preventDefault();
267 });
268
269 tbody.addEventListener("click", function (e) {
270 var td = dayAt(e.target), i;
271 if (!td) return;
272 for (i = 0; i < cells.length; i++) if (cells[i].td === td) return pick(cells[i].date);
273 });
274
275 function rtl() {
276 try {
277 return getComputedStyle(el).direction === "rtl";
278 } catch (e) {
279 return false;
280 }
281 }
282
283 table.addEventListener("keydown", function (e) {
284 var step = rtl() ? -1 : 1, at = cursor || view;
285 switch (e.key) {
286 case "ArrowLeft": e.preventDefault(); return moveTo(shift(at, -step), true);
287 case "ArrowRight": e.preventDefault(); return moveTo(shift(at, step), true);
288 case "ArrowUp": e.preventDefault(); return moveTo(shift(at, -7), true);
289 case "ArrowDown": e.preventDefault(); return moveTo(shift(at, 7), true);
290 case "Home":
291 e.preventDefault();
292 return moveTo(shift(at, -((at.getDay() - first + 7) % 7)), true);
293 case "End":
294 e.preventDefault();
295 return moveTo(shift(at, 6 - ((at.getDay() - first + 7) % 7)), true);
296 case "PageUp":
297 e.preventDefault();
298 return moveTo(shiftMonths(at, e.shiftKey ? -12 : -1), true);
299 case "PageDown":
300 e.preventDefault();
301 return moveTo(shiftMonths(at, e.shiftKey ? 12 : 1), true);
302 case "Enter":
303 case " ":
304 e.preventDefault();
305 return pick(at);
306 case "Escape":
307 e.preventDefault();
308 if (cfg.onDismiss) cfg.onDismiss();
309 return;
310 }
311 });
312
313 prevBtn.addEventListener("mousedown", function (e) { e.preventDefault(); });
314 nextBtn.addEventListener("mousedown", function (e) { e.preventDefault(); });
315 prevBtn.addEventListener("click", function () {
316 view = shiftMonths(new Date(view.getFullYear(), view.getMonth(), 1), -1);
317 render();
318 });
319 nextBtn.addEventListener("click", function () {
320 view = shiftMonths(new Date(view.getFullYear(), view.getMonth(), 1), 1);
321 render();
322 });
323
324 return {
325 el: el,
326
327 open: function (at) {
328 var start = at || (cfg.value ? cfg.value() : null) || new Date();
329 preview = at ? new Date(at.getFullYear(), at.getMonth(), at.getDate()) : null;
330 cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
331 view = new Date(start.getFullYear(), start.getMonth(), 1);
332 render();
333 el.hidden = false;
334 },
335
336 close: function () {
337 el.hidden = true;
338 preview = null;
339 },
340
341 isOpen: function () { return !el.hidden; },
342
343 show: function (d) {
344 if (el.hidden) return;
345 preview = d ? new Date(d.getFullYear(), d.getMonth(), d.getDate()) : null;
346 if (preview) moveTo(preview, false);
347 else render();
348 },
349
350 focusGrid: function () {
351 if (el.hidden) return false;
352 var td = cursor ? cellFor(cursor) : null;
353 if (!td) return false;
354 td.focus();
355 return true;
356 }
357 };
358 }
359
360 global.fiestaCalendar = makePanel;
361 }
362
363 if (typeof module !== "undefined" && module && module.exports) {
364 module.exports = {
365 firstDayOfWeek: firstDayOfWeek,
366 monthGrid: monthGrid,
367 weekdayNames: weekdayNames
368 };
369 }
370})(typeof globalThis !== "undefined" ? globalThis : this);
371