1/** 2 * Opening Hours Module - Handles parsing, validation, formatting, and UI interactions for opening hours 3 */ 4const OUMOpeningHours = (function() { 5 // Private variables 6 let observer = null; 7 8 /** 9 * English fallbacks used when locale data has not been localized yet 10 */ 11 const fallbackDayAbbrs = { 12 mo: 'Mo', 13 tu: 'Tu', 14 we: 'We', 15 th: 'Th', 16 fr: 'Fr', 17 sa: 'Sa', 18 su: 'Su' 19 }; 20 const fallbackDayAliases = { 21 mo: 'mo', 22 tu: 'tu', 23 we: 'we', 24 th: 'th', 25 fr: 'fr', 26 sa: 'sa', 27 su: 'su' 28 }; 29 30 /** 31 * Locale weekday aliases and abbreviations from wp_localize_script 32 */ 33 function getDayConfig() { 34 if (typeof oum_opening_hours !== 'undefined' && oum_opening_hours) { 35 return { 36 aliases: oum_opening_hours.day_aliases || fallbackDayAliases, 37 abbrs: oum_opening_hours.day_abbrs || fallbackDayAbbrs 38 }; 39 } 40 41 return { 42 aliases: fallbackDayAliases, 43 abbrs: fallbackDayAbbrs 44 }; 45 } 46 47 /** 48 * Normalize a weekday token for alias lookup (trim, strip dots, lowercase, fold accents) 49 * @param {string} alias 50 * @returns {string} 51 */ 52 function normalizeDayAlias(alias) { 53 if (!alias || typeof alias !== 'string') { 54 return ''; 55 } 56 57 let value = alias.trim().replace(/\.+$/, '').toLowerCase(); 58 59 try { 60 value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 61 } catch (e) { 62 // ignore if normalize() is unavailable 63 } 64 65 return value; 66 } 67 68 /** 69 * Resolve a typed weekday token to an internal day key (mo, tu, ...) 70 * @param {string} token 71 * @returns {string|null} 72 */ 73 function resolveDayKey(token) { 74 const normalized = normalizeDayAlias(token); 75 const aliases = getDayConfig().aliases; 76 77 if (normalized && aliases[normalized]) { 78 return aliases[normalized]; 79 } 80 81 return null; 82 } 83 84 /** 85 * Converts 12-hour format time to 24-hour format 86 * @param {string} time12 - Time in 12-hour format (e.g., "9:00 AM" or "11:30 PM") 87 * @returns {string|null} - Time in 24-hour format (HH:MM) or null if invalid 88 */ 89 function convert12To24(time12) { 90 if (!time12 || typeof time12 !== 'string') { 91 return null; 92 } 93 94 // Pattern: H:MM or HH:MM followed by AM/PM (case insensitive) 95 const pattern = /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i; 96 const match = time12.trim().match(pattern); 97 98 if (!match) { 99 return null; 100 } 101 102 let hour = parseInt(match[1], 10); 103 const minute = parseInt(match[2], 10); 104 const period = match[3].toUpperCase(); 105 106 // Validate minute 107 if (minute < 0 || minute > 59) { 108 return null; 109 } 110 111 // Convert to 24-hour format 112 if (period === 'AM') { 113 if (hour === 12) { 114 hour = 0; // 12:00 AM = 00:00 115 } 116 } else { // PM 117 if (hour !== 12) { 118 hour += 12; // 1:00 PM = 13:00, but 12:00 PM = 12:00 119 } 120 } 121 122 // Validate hour 123 if (hour < 0 || hour > 23) { 124 return null; 125 } 126 127 return String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0'); 128 } 129 130 /** 131 * Validates the opening hours input format 132 * Format: Mo 09:00-18:00 | Di 9:00 AM-5:00 PM | Do 1:00 PM-6:00 PM 133 * Accepts English weekday abbreviations plus the current WordPress locale (e.g. Do for Thursday) 134 * Supports both 24-hour (HH:MM) and 12-hour (H:MM AM/PM) formats 135 * @param {string} input - The input string to validate 136 * @returns {boolean} - True if valid, false otherwise 137 */ 138 function validateFormat(input) { 139 if (!input || typeof input !== 'string') { 140 return false; 141 } 142 143 const trimmed = input.trim(); 144 if (trimmed === '') { 145 return false; 146 } 147 148 // Split by pipe and validate each block 149 const blocks = trimmed.split('|'); 150 // Day token is language-agnostic; aliases are resolved via resolveDayKey() 151 const pattern24 = /^(\S+)\s+(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/; 152 const pattern12 = /^(\S+)\s+(\d{1,2}):(\d{2})\s*(AM|PM)-(\d{1,2}):(\d{2})\s*(AM|PM)$/i; 153 154 for (let block of blocks) { 155 block = block.trim(); 156 if (block === '') { 157 continue; 158 } 159 160 const match12 = block.match(pattern12); 161 if (match12) { 162 if (!resolveDayKey(match12[1])) { 163 return false;
164 } 165 166 // Validate 12-hour format times 167 const startTime12 = match12[2] + ':' + match12[3] + ' ' + match12[4]; 168 const endTime12 = match12[5] + ':' + match12[6] + ' ' + match12[7]; 169 const startTime24 = convert12To24(startTime12); 170 const endTime24 = convert12To24(endTime12); 171 172 if (!startTime24 || !endTime24) { 173 return false; 174 } 175 } else { 176 // Validate 24-hour format times 177 const match = block.match(pattern24); 178 if (!match || !resolveDayKey(match[1])) { 179 return false; 180 } 181 182 const startHour = parseInt(match[2], 10); 183 const startMin = parseInt(match[3], 10); 184 const endHour = parseInt(match[4], 10); 185 const endMin = parseInt(match[5], 10); 186 187 // Check valid time ranges 188 if (startHour < 0 || startHour > 23 || startMin < 0 || startMin > 59 || 189 endHour < 0 || endHour > 23 || endMin < 0 || endMin > 59) { 190 return false; 191 } 192 } 193 } 194 195 return true; 196 } 197 198 /** 199 * Converts 24-hour format time to 12-hour format 200 * @param {string} time24 - Time in 24-hour format (HH:MM) 201 * @returns {string} - Time in 12-hour format (H:MM AM/PM) 202 */ 203 function convert24To12(time24) { 204 if (!time24 || !time24.match(/^\d{2}:\d{2}$/)) { 205 return time24; // Return as-is if invalid format 206 } 207 208 const parts = time24.split(':'); 209 let hour = parseInt(parts[0], 10); 210 const minute = parts[1]; 211 let period = 'AM'; 212 213 if (hour === 0) { 214 hour = 12; // 00:00 = 12:00 AM 215 } else if (hour === 12) { 216 period = 'PM'; // 12:00 = 12:00 PM 217 } else if (hour > 12) { 218 hour -= 12; 219 period = 'PM'; 220 } 221 222 return hour + ':' + minute + ' ' + period; 223 } 224 225 /** 226 * Converts opening hours JSON back to input format 227 * @param {Object} data - The opening hours JSON object 228 * @param {boolean} use12hour - Whether to use 12-hour format 229 * @returns {string} - Input format string 230 */ 231 function formatForInput(data, use12hour = false) { 232 if (!data || !data.week) { 233 return ''; 234 } 235 236 const dayAbbrs = getDayConfig().abbrs; 237 238 const blocks = []; 239 const days = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']; 240 241 for (const day of days) { 242 const dayAbbr = dayAbbrs[day]; 243 if (!dayAbbr) { 244 continue; 245 } 246 247 const timeBlocks = data.week[day] || []; 248 249 for (const block of timeBlocks) { 250 if (use12hour) { 251 const startTime = convert24To12(block.start); 252 const endTime = convert24To12(block.end); 253 blocks.push(`${dayAbbr} ${startTime}-${endTime}`); 254 } else { 255 blocks.push(`${dayAbbr} ${block.start}-${block.end}`); 256 } 257 } 258 } 259 260 return blocks.join(' | '); 261 } 262 263 /** 264 * Setup toggle functionality for opening hours headers 265 * @param {HTMLElement} container - Container element to search for headers 266 */ 267 function setupToggle(container) { 268 if (!container) return; 269 270 const headers = container.querySelectorAll('.oum-opening-hours-header'); 271 headers.forEach(function(header) { 272 // Skip if already has event listener (check for data attribute) 273 if (header.dataset.oumToggleSetup === 'true') { 274 return; 275 } 276 277 // Mark as setup 278 header.dataset.oumToggleSetup = 'true'; 279 280 // Add click handler 281 header.addEventListener('click', function(e) { 282 e.preventDefault(); 283 toggleOpeningHours(header); 284 }); 285 286 // Add keyboard support 287 header.addEventListener('keydown', function(e) { 288 if (e.key === 'Enter' || e.key === ' ') { 289 e.preventDefault(); 290 toggleOpeningHours(header); 291 } 292 }); 293 }); 294 } 295 296 /** 297 * Toggle the visibility of opening hours wrapper 298 * @param {HTMLElement} header - The header element containing the toggle 299 */ 300 function toggleOpeningHours(header) { 301 const wrapper = header.nextElementSibling; 302 if (wrapper && wrapper.classList.contains('oum-opening-hours-wrapper')) { 303 const isExpanded = wrapper.classList.contains('oum-opening-hours-expanded'); 304 if (isExpanded) { 305 wrapper.classList.remove('oum-opening-hours-expanded'); 306 wrapper.style.display = 'none'; 307 header.classList.remove('oum-opening-hours-expanded'); 308 } else { 309 wrapper.classList.add('oum-opening-hours-expanded'); 310 wrapper.style.display = 'block'; 311 header.classList.add('oum-opening-hours-expanded'); 312 } 313 } 314 } 315 316 /** 317 * Initialize MutationObserver for dynamically added content 318 */ 319 function initializeObserver() { 320 if (observer) { 321 return; // Already initialized 322 } 323 324 observer = new MutationObserver(function(mutations) { 325 mutations.forEach(function(mutation) { 326 if (mutation.addedNodes.length) {
327 mutation.addedNodes.forEach(function(node) { 328 if (node.nodeType === 1) { // Element node 329 // Check if this node or its children contain opening hours header 330 if (node.querySelector && node.querySelector('.oum-opening-hours-header')) { 331 setupToggle(node); 332 } 333 // Also check if the node itself is an opening hours header 334 if (node.classList && node.classList.contains('oum-opening-hours-header')) { 335 setupToggle(node.parentElement); 336 } 337 } 338 }); 339 } 340 }); 341 }); 342 343 // Start observing 344 observer.observe(document.body, { 345 childList: true, 346 subtree: true 347 }); 348 } 349 350 /** 351 * Initialize opening hours UI for existing content 352 */ 353 function init() { 354 setupToggle(document.body); 355 initializeObserver(); 356 } 357 358 // Public interface 359 return { 360 validateFormat: validateFormat, 361 formatForInput: formatForInput, 362 // UI methods 363 init: init, 364 setupToggle: setupToggle 365 }; 366})(); 367
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.