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.