PageSourceSearch

https://www.grantforward.com/public/js/lib/duet_date_picker.js?v=285

js grantforward.com collected 2026-10-02 04:29:17 UTC 15,613 bytes, 362 lines download raw bytes

1(function ($) {
2    function formatLocalDate(date) {
3        if (!(date instanceof Date) || isNaN(date.getTime())) return "";
4        const yyyy = date.getFullYear();
5        const mm = String(date.getMonth() + 1).padStart(2, "0");
6        const dd = String(date.getDate()).padStart(2, "0");
7        return `${yyyy}-${mm}-${dd}`;
8    }
9
10    function pad(v) {
11        return v.toString().padStart(2, "0");
12    }
13
14    function formatDate(date, format) {
15        if (!(date instanceof Date)) return "";
16        const d = pad(date.getDate());
17        const m = pad(date.getMonth() + 1);
18        const y = date.getFullYear();
19        return format
20            .replace("DD", d)
21            .replace("MM", m)
22            .replace("YYYY", y);
23    }
24
25    function parseDate(value, format) {
26        if (!value) return null;
27        const map = {
28            DD: null,
29            MM: null,
30            YYYY: null
31        };
32        let regex = format;
33        Object.keys(map).forEach(k => {
34            regex = regex.replace(k, "(\\d+)");
35        });
36        const match = value.match(new RegExp("^" + regex + "$"));
37        if (!match) return null;
38        const order = [];
39        let index = 1;
40        format.replace(/DD|MM|YYYY/g, token => {
41            order.push({ token, index: index++ });
42        });
43        let day, month, year;
44        order.forEach(o => {
45            const v = parseInt(match[o.index], 10);
46            if (o.token === "DD") day = v;
47            if (o.token === "MM") month = v;
48            if (o.token === "YYYY") year = v;
49        });
50        return new Date(year, month - 1, day);
51    }
52
53    /**
54     * jQuery plugin to wrap Duet Date Picker
55     * Supports options, method calls, and custom date formatting.
56     */
57    $.fn.duet_date_picker = function (optionsOrMethod, ...args) {
58        // Handle method calls
59        if (typeof optionsOrMethod === "string") {
60            const method = optionsOrMethod;
61
62            // Set options dynamically
63            if (method === "option") {
64                const [key, value] = args;
65                return this.each(function () {
66                    const $input = $(this);
67                    const duet = $input.next("duet-date-picker")[0];
68                    if (!duet) return;
69
70                    switch (key) {
71                        case "minDate":
72                            if (value === null) {
73                                duet.setAttribute("min", "");
74                            } else if (value instanceof Date && !isNaN(value.getTime())) {
75                                duet.setAttribute("min", formatLocalDate(value));
76                            } else {
77                                duet.setAttribute("min", value);
78                            }
79                            break;
80
81                        case "maxDate":
82                            if (value === null) {
83                                duet.setAttribute("max", "");
84                            } else if (value instanceof Date && !isNaN(value.getTime())) {
85                                duet.setAttribute("max", formatLocalDate(value));
86                            } else {
87                                duet.setAttribute("max", value);
88                            }
89                            break;
90                        default:
91                            duet.setAttribute(key, value);
92                            break;
93                    }
94                });
95            }
96            // Get selected date(s)
97            else if (method === "getDate") {
98                const results = [];
99                this.each(function () {
100                    const duet = $(this).next("duet-date-picker")[0];
101                    if (duet) {
102                        const displayValue = duet.value
103                            ? moment(duet.value, "YYYY-MM-DD").format(duet.getAttribute("format"))
104                            : "";
105                        results.push(displayValue || "");
106                    }
107                });
108                return results.length === 1 ? results[0] : results;
109            } else if (method === "clear") {
110                return this.each(function () {
111                    const input = $(this);
112                    const duet = input.next("duet-date-picker")[0];
113                    if (!duet) return;
114
115                    input.val('').trigger("change");
116                    duet.dispatchEvent(
117                        new CustomEvent('duetChange', {
118                            detail: { value: null },
119                            bubbles: true
120                        })
121                    );
122                })
123            }
124
125            return this;
126        }
127
128        // Handle initialization
129        const options = optionsOrMethod || {};
130        const enableTime = !!options.enableTime;
131        const deltaTime = options.deltaTime ?? 60; // Minute
132        const dateFormat = options.dateFormat || (enableTime ? "DD/MM/YYYY HH:mm" : "DD/MM/YYYY");
133        const allowClear = typeof options.allowClear === "boolean" ? options.allowClear : true;
134
135        return this.each(function () {
136            const $input = $(this);
137            if ($input.next("duet-date-picker").length > 0) {
138                return;
139            }
140
141            const id = $input.attr("id") || "duet-" + Math.random().toString(36).substr(2, 9);
142            $input.attr("id", id);
143
144            const placeholder = $input.attr("placeholder") || "";
145            const aria_label = $input.attr("aria-label") || "Input date";
146
147            // Create Duet date picker element
148            const duet = document.createElement("duet-date-picker");
149            duet.identifier = id + "_input";
150            duet.setAttribute("name", $input.attr("name") || id);
151            duet.setAttribute("value", $input.val() || "");
152            duet.setAttribute("format", dateFormat);
153            duet.setAttribute("placeholder", placeholder);
154
155            // Wait for inner input to exist and set placeholder
156            const setPlaceholderInterval = setInterval(() => {
157                const innerInput = duet.querySelector(".duet-date__input");
158                if (innerInput) {
159                    innerInput.setAttribute("placeholder", placeholder);
160                    innerInput.setAttribute("readonly", "true");
161                    innerInput.removeAttribute("aria-autocomplete");
162                    innerInput.setAttribute("aria-label", aria_label);
163                    innerInput.setAttribute("tabindex", -1);
164
165                    // Allow clicking on input to open calendar
166                    const clearCalendarValue = function () {
167                        $input.val('').trigger('change');
168                        duet.dispatchEvent(
169                            new CustomEvent('duetChange', {
170                                detail: { value: null },
171                                bubbles: true
172                            })
173                        );
174                    };
175
176                    // Allow clicking or keydown on input to open calendar
177                    // innerInput.addEventListener("click", openCalendar);
178                    // innerInput.addEventListener("keydown", function(e) {
179                    //     if (e.key === "Enter" || e.key === " ") {
180                    //         e.preventDefault();
181                    //         openCalendar();
182                    //     }
183                    // });
184
185                    // Allow keydown on input to reset calendar
186                    if (allowClear) {
187                        innerInput.addEventListener("keydown", function (e) {
188                            if (e.key === "Backspace" || e.key === "Delete") {
189                                e.preventDefault();
190                                clearCalendarValue();
191                            }
192                        });
193                    }
194
195                    clearInterval(setPlaceholderInterval);
196                }
197            }, 100);
198
199            if (enableTime) {
200                // Wait for inner dialog to exist and setup time
201                const setupDialogInterval = setInterval(() => {
202                    const innerDialog = duet.querySelector(".duet-date__dialog");
203                    if (innerDialog) {
204                        const selects = innerDialog.querySelectorAll(".duet-date__select");
205                        const lastSelect = selects[selects.length - 1];
206                        const id = Math.random().toString(36).substr(2, 9);
207
208                        const now = new Date();
209                        const currentMinutes = now.getHours() * 60 + now.getMinutes();
210
211                        const nextMinutes = Math.ceil(currentMinutes / deltaTime) * deltaTime;
212
213                        let h = Math.floor(nextMinutes / 60);
214                        let m = nextMinutes % 60;
215
216                        if (nextMinutes >= 1440) {
217                            h = 23;
218                            m = 59;
219                        }
220
221                        const nextTime = `${String(h).padStart(2, "0")}
221:${String(m).padStart(2, "0")}`;
222
223                        duet.setAttribute("time", nextTime);
224                        lastSelect.insertAdjacentHTML("afterend",
225                            `<label for="DuetDateTime-${id}" class="duet-date__vhidden">Time</label><div class="duet-date__select"><select id="DuetDateTime-${id}" class="duet-date__select--time"></select><div class="duet-date__select-label" aria-hidden="true"><span>${nextTime}</span><svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"></path></svg></div></div>`);
226
227                        const timeSelect = document.querySelector(`#DuetDateTime-${id}`);
228                        let optionsHtml = "";
229
230                        for (let total = 0; total < 1440; total += deltaTime) {
231                            const h = Math.floor(total / 60).toString().padStart(2, "0");
232                            const m = (total % 60).toString().padStart(2, "0");
233                            optionsHtml += `<option>${h}:${m}</option>`;
234                        }
235
236                        timeSelect.innerHTML = optionsHtml;
237
238                        timeSelect.addEventListener("change", function () {
239                            const value = this.value;
240
241                            timeSelect.nextElementSibling.querySelector("span").innerHTML = value;
242                            duet.setAttribute("time", value);
243                        });
244
245                        clearInterval(setupDialogInterval);
246                    }
247                }, 100);
248            }
249
250            duet.dateAdapter = {
251                parse(value) {
252                    return parseDate(value, dateFormat);
253                },
254                format(date) {
255                    return formatDate(date, dateFormat);
256                }
257            };
258
259            // Update jQuery input when Duet value changes
260            duet.addEventListener("duetChange", e => {
261                const isoValue = e.detail.value;
262
263                var displayValue = isoValue
264                    ? moment(isoValue, "YYYY-MM-DD").format(dateFormat)
265                    : null;
266
267                if (isoValue && enableTime && duet.hasAttribute("time")) {
268                    displayValue = moment(
269                        isoValue + " " + duet.getAttribute("time"),
270                        "YYYY-MM-DD HH:mm"
271                    ).format(dateFormat);
272                }
273
274                $input.val(displayValue);
275
276                const innerInput = duet.querySelector(".duet-date__input");
277                innerInput.value = displayValue;
278
279                if (typeof options.onChange === "function") {
280                    options.onChange.call($input[0], displayValue);
281                }
282            });
283
284            // Update Duet picker when jQuery input changes
285            $input.on("change", function () {
286                const value = $(this).val();
287
288                const parsed = value ? moment(value, dateFormat).format("YYYY-MM-DD") : null;
289
290                // if (enableTime && value) {
291                //     const timePart = moment(value, dateFormat).format("HH:mm");
292                //     duet.setAttribute("time", timePart);
293                // }
294
295                duet.value = parsed;
296
297                // Wait for inner input to exist and set value
298                const setValueInterval = setInterval(() => {
299                    const innerInput = duet.querySelector(".duet-date__input");
300                    if (innerInput) {
301                        innerInput.value = value;
302                        clearInterval(setValueInterval);
303                    }
304                }, 100);
305            });
306
307            // Apply remaining options as attributes
308            const ignoreKeys = ["onChange", "dateFormat"];
309            Object.keys(options)
310                .filter(k => !ignoreKeys.includes(k))
311                .forEach(k => duet.setAttribute(k, options[k]));
312
313            duet.addEventListener("duetOpen", () => {
314                if (window.matchMedia("(max-width: 35.9375em)").matches) {
315                    duet.classList.remove("duet-open-up");
316                    return;
317                }
318
319                requestAnimationFrame(() => {
320                    requestAnimationFrame(() => {
321                        const rect = duet.getBoundingClientRect();
322                        const dialog = duet.querySelector(".duet-date__dialog");
323                        if (!dialog) return;
324
325                        const header = document.querySelector(".header");
326                        const headNav = document.querySelector(".head-nav");
327                        const secondaryNav = document.querySelector(".secondary-nav");
328                        const headerHeight = header ? header.offsetHeight : 0;
329                        const headNavHeight = headNav ? headNav.offsetHeight : 0;
330                        const secondaryNavHeight = secondaryNav ? secondaryNav.offsetHeight : 0;
331
332                        const footer = document.querySelector(".footer");
333                        const footerRect = footer.getBoundingClientRect();
334
335                        const absoluteBottom = rect.bottom + window.scrollY;
336                        const absoluteTop = rect.top + window.scrollY;
337
338                        const popupHeight = dialog.scrollHeight;
339                        const absoluteSpaceAbove = absoluteTop - (headerHeight + headNavHeight + secondaryNavHeight);
340                        const absoluteSpaceBelow = footerRect.top + window.scrollY - absoluteBottom;
341                        const viewportSpaceBelow = window.innerHeight - rect.bottom;
342
343                        if (absoluteSpaceAbove < popupHeight) {
344                            // down
345                            duet.classList.remove("duet-open-up");
346                        } else if (absoluteSpaceBelow < popupHeight || viewportSpaceBelow < popupHeight) {
347                            // up
348                            duet.classList.add("duet-open-up");
349                        } else {
350                            // down
351                            duet.classList.remove("duet-open-up");
352                        }
353                    });
354                });
355            });
356
357            // Insert Duet element after input and hide original input
358            $input.after(duet);
359            $input.hide();
360        });
361    };
362})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.