1// ========================= 2// Range Datepicker in Dropdown (Bootstrap4 + jQuery UI) 3// UXæ¹åçï¼ 4// - 1æ¥ã ãé¸ã¶å ´åï¼æ¥ä»ã1åé¸ãã§ã鏿ãããæ¼ãã°OKï¼éå§ã®ã¿ã§ç¢ºå®ï¼ 5// - éå§ã®ã¿ã®ã¨ããã«ã¬ã³ãã¼ä¸ã§éå§æ¥ããã¤ã©ã¤ã 6// - çµäºå ¥åãæ¼ãã¦ãéå§ãæªè¨å®ãªãéå§ã«æ»ãï¼è¿·å鲿¢ï¼ 7// - ã鏿ããããã¿ã³ã¯éå§æ¥ãç¡ãã¨æ¼ããªã 8// - æ¢åå ¥åå¤ãããã°éããæã«åãè¾¼ãï¼readonlyã§ãä¿éºï¼ 9// ========================= 10(function () { 11 // ========================= 12 // elements 13 // ========================= 14 const $menu = $('#menuDate'); 15 const $toggle = $('#toggleDate'); 16 const $from = $('#dateFrom'); 17 const $to = $('#dateTo'); 18 const $cal = $('#rangeCalendar'); 19 const $btnClear = $('#headerDateClear'); 20 const $btnApply = $menu.find('.dropdown-close'); 21 22 const mq = window.matchMedia('(min-width: 768px)'); 23 24 // ========================= 25 // state 26 // ========================= 27 let startDate = null; // Date (stripTimeæ¸ã¿æ¨å¥¨) 28 let endDate = null; // Date (stripTimeæ¸ã¿æ¨å¥¨) 29 let activeField = 'from'; // 'from' | 'to' 30 31 // ========================= 32 // helpers 33 // ========================= 34 function pad2(n) { 35 return String(n).padStart(2, '0'); 36 } 37 38 function stripTime(d) { 39 return new Date(d.getFullYear(), d.getMonth(), d.getDate()); 40 } 41 42 function parseYMD(str) { 43 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str); 44 if (!m) return null; 45 const y = Number(m[1]), mo = Number(m[2]) - 1, da = Number(m[3]); 46 const dt = new Date(y, mo, da); 47 if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== da) return null; 48 return stripTime(dt); 49 } 50 51 function toYMD(date) { 52 return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`; 53 } 54 55 function firstDayOfThisMonth() { 56 const now = new Date(); 57 return new Date(now.getFullYear(), now.getMonth(), 1); 58 } 59 60 function getToday() { 61 return stripTime(new Date()); 62 } 63 64 function sameDay(a, b) { 65 if (!a || !b) return false; 66 return a.getTime() === b.getTime(); 67 } 68 69 function reflectInputs() { 70 $from.val(startDate ? toYMD(startDate) : ''); 71 $to.val(endDate ? toYMD(endDate) : ''); 72 } 73 74 function updateToggleLabel() { 75 // 1æ¥ã ãï¼éå§ã®ã¿ï¼ã§ãæ¤ç´¢ã§ããæ³å®ï¼ã©ãã«ã¯ãYYYY-MM-DDãã¾ãã¯ãYYYY-MM-DDãã 76 if (startDate && endDate) { 77 $toggle.text(`${toYMD(startDate)}ã${toYMD(endDate)}`); 78 } else if (startDate && !endDate) { 79 // 好ã¿ã§ããããä»ãããå ´åã¯ä¸ã®è¡ã«å¤æ´ 80 $toggle.text(`${toYMD(startDate)}ã`); 81 // $toggle.text(`${toYMD(startDate)}`); 82 } else { 83 $toggle.text('鏿'); 84 } 85 } 86 87 function updateActiveInput() { 88 $from.removeClass('is-active'); 89 $to.removeClass('is-active'); 90 if (activeField === 'from') $from.addClass('is-active'); 91 if (activeField === 'to') $to.addClass('is-active'); 92 } 93 94 // TODO: ãã¿ã³ã¯å¸¸ã«æå¹ãªããç¾å¨ãã®é¢æ°ã¯ä½ããã¦ããªããåé¤åè£ã 95 function updateApplyState() { 96 $btnApply.prop('disabled', false); 97 $btnApply.removeClass('disabled'); 98 } 99 100 // æªé¸æãªã®ã«ã仿¥ãã current-day/active ã«ãªãçãå¼·å¶é¤å» 101 function stripImplicitActive() { 102 if (startDate || endDate) return; 103 104 const $cur = $cal.find('td.ui-datepicker-current-day'); 105 $cur.removeClass('ui-datepicker-current-day'); 106 $cur.find('a.ui-state-active').removeClass('ui-state-active'); 107 } 108 109 function refreshAndStrip() { 110 if (!$cal.hasClass('hasDatepicker')) return; 111 $cal.datepicker('refresh'); 112 113 requestAnimationFrame(() => { 114 stripImplicitActive(); 115 }); 116 } 117 118 function syncStateFromInputs() { 119 // readonlyã§ããå¤é¨ããvalãå ¥ãå¯è½æ§ã®ä¿éºï¼åæå¤ã復å ï¼ 120 const from = parseYMD($from.val()); 121 const to = parseYMD($to.val()); 122 123 startDate = from; 124 endDate = to; 125 126 // 䏿£ãªçµã¿åããè£æ£ 127 if (startDate && endDate && endDate < startDate) { 128 const tmp = startDate; 129 startDate = endDate; 130 endDate = tmp; 131 } 132 if (!startDate) endDate = null; 133 } 134 135 function clearAll() { 136 startDate = null; 137 endDate = null; 138 activeField = 'from'; 139 140 reflectInputs(); 141 updateToggleLabel(); 142 updateActiveInput(); 143 updateApplyState(); 144 145 rebuildDatepicker(); 146 } 147 148 // ========================= 149 // Holidays (year cache) 150 // ========================= 151 const holidayMap = {}; 152 const holidayYearLoaded = {}; 153 154 function fetchHolidayYear(year) { 155 if (holidayYearLoaded[year]) return Promise.resolve(); 156 holidayYearLoaded[year] = true; 157 158 const url = `https://holidays-jp.github.io/api/v1/${year}/date.json`; 159 return fetch(url, { cache: 'force-cache' }) 160 .then(r => { 161 if (!r.ok) throw new Error(`holiday fetch failed: ${r.status}`); 162 return r.json(); 163 }) 164 .then(obj => { 165 Object.assign(holidayMap, obj); 166 }) 167 .catch(err => { 168 console.warn(err); 169 }); 170 } 171 172 // ========================= 173 // Datepicker build 174 // ========================= 175 function rebuildDatepicker() { 176 if ($cal.hasClass('hasDatepicker')) { 177 $cal.datepicker('destroy'); 178 } 179 180 const months = mq.matches ? 2 : 1; 181 182 const baseYear = (startDate || new Date()).getFullYear(); 183 Promise.all([ 184 fetchHolidayYear(baseYear - 1), 185 fetchHolidayYear(baseYear), 186 fetchHolidayYear(baseYear + 1), 187 ]).then(() => { 188 requestAnimationFrame(refreshAndStrip); 189 }); 190 191 $cal.datepicker({ 192 numberOfMonths: months,
193 showOtherMonths: false, 194 selectOtherMonths: false, 195 196 firstDay: 0, 197 dateFormat: 'yy-mm-dd', 198 199 prevText: 'ï¼å', 200 nextText: '次ï¼', 201 202 beforeShow: function () { 203 requestAnimationFrame(() => { 204 stripImplicitActive(); 205 }); 206 }, 207 208 onChangeMonthYear: function (year) { 209 fetchHolidayYear(year).then(() => { 210 refreshAndStrip(); 211 }); 212 }, 213 214 beforeShowDay: function (date) { 215 const day = date.getDay(); 216 let cls = ''; 217 if (day === 0) cls += ' is-sun'; 218 if (day === 6) cls += ' is-sat'; 219 220 const key = toYMD(date); 221 const holidayName = holidayMap[key]; 222 if (holidayName) cls += ' is-holiday'; 223 224 const t = stripTime(date).getTime(); 225 226 // éå§ã®ã¿ã§ãéå§æ¥ããã¤ã©ã¤ãï¼1æ¥é¸æã®ä½¿ãåæUPï¼ 227 if (startDate && !endDate) { 228 const s = startDate.getTime(); 229 if (t === s) return [true, `ui-state-range-start${cls}`, holidayName || '']; 230 } 231 232 // ç¯å²ãã¤ã©ã¤ã 233 if (startDate && endDate) { 234 const s = startDate.getTime(); 235 const e = endDate.getTime(); 236 237 if (t === s) return [true, `ui-state-range-start${cls}`, holidayName || '']; 238 if (t === e) return [true, `ui-state-range-end${cls}`, holidayName || '']; 239 if (t > s && t < e) return [true, `ui-state-range-middle${cls}`, holidayName || '']; 240 } 241 242 return [true, cls.trim(), holidayName || '']; 243 }, 244 245 onSelect: function (dateText) { 246 const picked = parseYMD(dateText); 247 if (!picked) return; 248 249 if (activeField === 'from') { 250 // éå§ãé¸ã³ç´ãããçµäºã¯ã¯ãªã¢ï¼åãããããåªå ï¼ 251 startDate = picked; 252 endDate = null; 253 activeField = 'to'; 254 } else { 255 // çµäºå ¥åä¸ 256 if (!startDate) { 257 startDate = picked; 258 endDate = null; 259 activeField = 'to'; 260 } else { 261 // 忥ãªãã1æ¥ã ãã®ç¯å²ãã¨ãã¦ç¢ºå®ï¼éå§=çµäºï¼ 262 if (sameDay(picked, startDate)) { 263 endDate = picked; 264 } else if (picked < startDate) { 265 endDate = startDate; 266 startDate = picked; 267 } else { 268 endDate = picked; 269 } 270 activeField = 'from'; 271 } 272 } 273 274 reflectInputs(); 275 updateToggleLabel(); 276 updateActiveInput(); 277 updateApplyState(); 278 279 $cal.datepicker('refresh'); 280 } 281 }); 282 283 // åæè¡¨ç¤ºï¼å¤ãããã°éå§æ¥ã«å¯ããï¼ãªããã°å½æ1æ¥ 284 if (startDate) { 285 $cal.datepicker('setDate', startDate); 286 $cal.datepicker('refresh'); 287 } else { 288 $cal.datepicker('setDate', firstDayOfThisMonth()); 289 $cal.datepicker('refresh'); 290 } 291 292 updateActiveInput(); 293 updateApplyState(); 294 295 requestAnimationFrame(() => { 296 stripImplicitActive(); 297 }); 298 } 299 300 // ========================= 301 // dropdown behavior 302 // ========================= 303 $menu.on('click', function (e) { 304 e.stopPropagation(); 305 }); 306 307 $toggle.on('shown.bs.dropdown', function () { 308 // éããã¿ã¤ãã³ã°ã§å ¥åå¤ãåãè¾¼ãï¼å¤é¨ããã®å¾©å /åæå¤å¯¾å¿ï¼ 309 syncStateFromInputs(); 310 311 if ($cal.hasClass('hasDatepicker')) { 312 refreshAndStrip(); 313 } else { 314 rebuildDatepicker(); 315 } 316 317 reflectInputs(); 318 updateToggleLabel(); 319 updateActiveInput(); 320 updateApplyState(); 321 }); 322 323 $from.on('click', function () { 324 activeField = 'from'; 325 326 // éå§ã触ã£ãããçµäºã¯ä¸æ¦æ¶ãã¦éå§ããåãç´ããã«å¯ãã 327 endDate = null; 328 329 updateActiveInput(); 330 reflectInputs(); 331 updateToggleLabel(); 332 updateApplyState(); 333 334 refreshAndStrip(); 335 }); 336 337 $to.on('click', function () { 338 // éå§ãç¡ãã®ã«çµäºãæ¼ããããéå§ã«æ»ãï¼è¿·å鲿¢ï¼ 339 if (!startDate) { 340 activeField = 'from'; 341 updateActiveInput(); 342 refreshAndStrip(); 343 return; 344 } 345 activeField = 'to'; 346 updateActiveInput(); 347 }); 348 349 // ========================= 350 // buttons 351 // ========================= 352 $btnClear.on('click', function (e) { 353 e.preventDefault(); 354 clearAll(); 355 }); 356 357 $btnApply.on('click', function () {
358 // æ¥ä»ã鏿ããã¦ããå ´åã®ã¿ã©ãã«/å ¥åãåæãã 359 if (startDate) { 360 reflectInputs(); 361 updateToggleLabel(); 362 updateApplyState(); 363 } 364 // æ¥ä»ã®é¸æç¶æ ã«é¢ããã常ã«ãããããã¦ã³ãéãã 365 $menu.removeClass('show'); 366 $toggle.attr('aria-expanded', 'false'); 367 }); 368 369 // ========================= 370 // init 371 // ========================= 372 document.addEventListener('DOMContentLoaded', function () { 373 syncStateFromInputs(); 374 rebuildDatepicker(); 375 reflectInputs(); 376 updateToggleLabel(); 377 updateActiveInput(); 378 updateApplyState(); 379 }); 380 381 mq.addEventListener('change', function () { 382 rebuildDatepicker(); 383 }); 384})();
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.