1(function($) { 2 'use strict'; 3 4 window.BHIPro = { 5 data: null, 6 7 init: function() { 8 this.data = window.mabel_script_vars || {}; 9 if (!this.data.locations) return; 10 this.loadAll(); 11 12 $(window).on('elementor/popup/show', () => this.loadAll()); 13 }, 14 15 loadAll: function() { 16 var self = this; 17 $('[data-fetch-shortcode]').each(function() { 18 var $el = $(this); 19 var code = $el.attr('data-code'); 20 var argsStr = $el.attr('data-arguments'); 21 var args = self.parseArgs(argsStr); 22 var result = self.render(code, args); 23 if (result !== null) { 24 var $newEl = $(result); 25 $el.replaceWith($newEl); 26 } 27 }); 28 29 $('.mbhi-if-wrapper').each(function() { 30 var $el = $(this); 31 var code = $el.attr('data-code'); 32 var locName = $el.attr('data-loc'); 33 var isOpen = self.isOpen(locName); 34 35 if ((code === 'mbhi_ifopen' && isOpen) || (code === 'mbhi_ifclosed' && !isOpen)) { 36 $el.show().contents().unwrap(); 37 } else { 38 $el.remove(); 39 } 40 }); 41 }, 42 43 parseArgs: function(argStr) { 44 var args = {}; 45 var regex = /([a-z0-9_]+)="([^"]*)"/gi; 46 var m; 47 var txt = document.createElement('textarea'); 48 49 while ((m = regex.exec(argStr)) !== null) { 50 var val = m[2]; 51 if (val === 'true') val = true; 52 else if (val === 'false') val = false; 53 else if (val !== '') { 54 txt.innerHTML = val; 55 val = txt.value; 56 } 57 args[m[1]] = val; 58 } 59 60 this.applyArgAliases(args); 61 62 return args; 63 }, 64 65 // Mirror Shortcode::apply_attribute_aliases - accept the singular or the plural 66 // spelling of any attribute. Pages in the wild carry includeholiday="true" where 67 // this reads args.includeholidays. Nothing here knows the canonical list, so every 68 // parsed key also answers to its opposite spelling; the read sites only ever ask 69 // for the real names. 70 // 71 // An EMPTY attribute counts as not supplied, both as a source and as a target. 72 // The pages this rescues were built from a template that emits the whole 73 // attribute list with blanks, so includeholidays="" sits beside the hand-added 74 // includeholiday="true" - treat the blank as a real choice and nothing changes on 75 // exactly the pages that need it. A non-empty value still always wins. 76 applyArgAliases: function(args) { 77 var self = this; 78 Object.keys(args).forEach(function(key) { 79 if (args[key] === '') return; 80 self.singularPluralVariants(key).forEach(function(alias) { 81 if (!(alias in args) || args[alias] === '') args[alias] = args[key]; 82 }); 83 }); 84 }, 85 86 // Flip each underscore-separated word between singular and plural, not just the 87 // last one - that is what catches included_specials_format. 88 singularPluralVariants: function(key) { 89 var parts = key.split('_'); 90 var variants = []; 91 parts.forEach(function(part, i) { 92 if (part === '') return; 93 var swapped = parts.slice(); 94 swapped[i] = part.slice(-1) === 's' ? part.slice(0, -1) : part + 's'; 95 var variant = swapped.join('_'); 96 if (variant !== key) variants.push(variant); 97 }); 98 return variants; 99 }, 100 101 render: function(code, args) { 102 if (code === 'mbhi') { 103 return this.renderIndicator(args); 104 } else if (code === 'mbhi_hours' || code === 'mbhi_hours_today') { 105 return this.renderHoursList(args, code === 'mbhi_hours_today'); 106 } 107 return null; 108 }, 109 110 isOpen: function(locName) { 111 var loc = this.findLocation(locName); 112 if (!loc) return false;
113 return this.checkIsOpen(loc).open; 114 }, 115 116 findLocation: function(name) { 117 if (!this.data.locations || this.data.locations.length === 0) return null; 118 if (!name) return this.data.locations[0]; 119 return this.data.locations.find(function(l) { return l.Name === name; }) || this.data.locations[0]; 120 }, 121 122 getBusinessTime: function(timezone) { 123 var now = new Date(); 124 if (!timezone) timezone = this.data.settings.timezone; 125 try { 126 // Get time in business timezone 127 var formatter = new Intl.DateTimeFormat('en-US', { 128 timeZone: timezone, 129 year: 'numeric', month: 'numeric', day: 'numeric', 130 hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false 131 }); 132 var parts = formatter.formatToParts(now); 133 var d = {}; 134 parts.forEach(function(p) { if(p.type !== 'literal') d[p.type] = p.value; }); 135 return new Date(d.year, d.month - 1, d.day, d.hour, d.minute, d.second); 136 } catch (e) { 137 return now; 138 } 139 }, 140 141 checkIsOpen: function(loc) { 142 var tz = loc.Timezone || this.data.settings.timezone; 143 var now = this.getBusinessTime(tz); 144 145 // Vacations 146 if (loc.Vacations) { 147 for (var i = 0; i < loc.Vacations.length; i++) { 148 var v = loc.Vacations[i]; 149 var from = this.parseDayMonth(v.FromDay, v.From, tz, 0, 0, 0); 150 var to = this.parseDayMonth(v.ToDay, v.To, tz, 23, 59, 59); 151 if (to < from) to.setFullYear(to.getFullYear() + 1); 152 if (now >= from && now <= to) return { open: false, type: 'vacation', name: v.Name, next: to }; 153 } 154 } 155 156 // Specials 157 if (loc.Holidays) { 158 for (var i = 0; i < loc.Holidays.length; i++) { 159 var h = loc.Holidays[i]; 160 var hDate = this.parseDayMonth(h.Day, h.Month, tz, 0, 0, 0, h.Year); 161 if (hDate.toDateString() === now.toDateString()) { 162 if (!h.Hours || h.Hours.length === 0) return { open: false, type: 'special', name: h.Name }; 163 var openPeriods = this.getOpenPeriods(h.Hours, now); 164 var res = this.checkPeriods(openPeriods, now, 'special', h.Name); 165 if (res.open || res.next) return res; 166 } 167 } 168 } 169 170 // Normal days 171 var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 172 var dayName = dayNames[now.getDay()]; 173 var dayData = loc.Days.find(function(d) { return d.Day === dayName; }); 174 if (dayData && dayData.Hours) { 175 var openPeriods = this.getOpenPeriods(dayData.Hours, now); 176 var res = this.checkPeriods(openPeriods, now, 'normal'); 177 if (res.open || res.next) return res; 178 } 179 180 // If not open today, find next opening day 181 return { open: false, type: 'normal', next: this.findNextOpening(loc, now) }; 182 }, 183 184 findNextOpening: function(loc, now) { 185 var self = this; 186 var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 187 for (var i = 1; i <= 366; i++) { 188 var nextDate = new Date(now); 189 nextDate.setDate(now.getDate() + i); 190 nextDate.setHours(0, 0, 0, 0); 191 var dayName = dayNames[nextDate.getDay()]; 192 193 // Check if nextDate is a vacation 194 var isVacation = false; 195 if (loc.Vacations) { 196 for (var j = 0; j < loc.Vacations.length; j++) { 197 var v = loc.Vacations[j]; 198 var vFrom = this.parseDayMonth(v.FromDay, v.From, null, 0, 0, 0); 199 var vTo = this.parseDayMonth(v.ToDay, v.To, null, 23, 59, 59); 200 if (vTo < vFrom) vTo.setFullYear(vTo.getFullYear() + 1); 201 if (nextDate >= vFrom && nextDate <= vTo) { isVacation = true; break; } 202 } 203 } 204 if (isVacation) continue; 205
206 // Check specials 207 if (loc.Holidays) { 208 var h = loc.Holidays.find(function(h) { 209 return h.Day == nextDate.getDate() && self.monthIndex(h.Month) === nextDate.getMonth() && (!h.Year || h.Year == nextDate.getFullYear()); 210 }); 211 if (h) { 212 if (h.Hours) { 213 var periods = this.getOpenPeriods(h.Hours, nextDate); 214 if (periods.length > 0) return periods[0].from; 215 } 216 continue; 217 } 218 } 219 220 var dayData = loc.Days.find(function(d) { return d.Day === dayName; }); 221 if (dayData && dayData.Hours) { 222 var periods = this.getOpenPeriods(dayData.Hours, nextDate); 223 if (periods.length > 0) return periods[0].from; 224 } 225 } 226 return null; 227 }, 228 229 // Stored entries carry abbreviated month names (the admin editor's 230 // dropdown and dynamic-holiday resolution both emit "Jan".."Dec"), 231 // but older data may carry full names; resolve either spelling. 232 monthIndex: function(month) { 233 var idx = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
233November", "December"].indexOf(month); 234 if (idx === -1) idx = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].indexOf(month); 235 return idx; 236 }, 237 238 parseDayMonth: function(day, month, timezone, h, m, s, year) { 239 var monthIdx = this.monthIndex(month); 240 var now = new Date(); // Use local year unless the entry pins one 241 var date = new Date(year || now.getFullYear(), monthIdx, day, h || 0, m || 0, s || 0); 242 return date; 243 }, 244 245 getOpenPeriods: function(hours, date) { 246 var self = this; 247 return hours.filter(function(h) { 248 return h.From != 0 && h.To != 0; 249 }).map(function(h) { 250 var from = new Date(date); 251 var tFrom = self.parseTime(h.From, h.FromIndication); 252 from.setHours(tFrom.h, tFrom.m, 0, 0); 253 254 var to = new Date(date); 255 var tTo = self.parseTime(h.To, h.ToIndication); 256 to.setHours(tTo.h, tTo.m, 0, 0); 257 258 if (to <= from) { 259 to.setDate(to.getDate() + 1); 260 } 261 return { from: from, to: to }; 262 }); 263 }, 264 265 parseTime: function(timeStr, indication) { 266 var parts = timeStr.split(':'); 267 var h = parseInt(parts[0]); 268 var m = parts.length > 1 ? parseInt(parts[1]) : 0; 269 if (indication === 'PM' && h < 12) h += 12; 270 if (indication === 'AM' && h === 12) h = 0; 271 return { h: h, m: m }; 272 }, 273 274 checkPeriods: function(periods, now, type, name) { 275 periods.sort(function(a, b) { return a.from - b.from; }); 276 for (var i = 0; i < periods.length; i++) { 277 var p = periods[i]; 278 if (now >= p.from && now < p.to) return { open: true, type: type, name: name, next: p.to }; 279 if (now < p.from) return { open: false, type: type, name: name, next: p.from }; 280 } 281 return { open: false, type: type, name: name }; 282 }, 283 284 renderIndicator: function(args) { 285 var loc = this.findLocation(args.location); 286 if (!loc) return null; 287 var state = this.checkIsOpen(loc); 288 289 var msg = state.open ? args.openmessage : args.closedmessage; 290 291 // Warning logic 292 var diff = state.next ? (state.next - this.getBusinessTime(loc.Timezone)) / 1000 : null; 293 if (state.open && diff !== null && diff <= args.closingsoonmessagetime) { 294 msg = args.closingsoonmessage; 295 } else if (!state.open && diff !== null && diff <= args.openingsoonmessagetime) { 296 msg = args.openingsoonmessage; 297 } 298 299 // Special date / Vacation messages 300 if (state.type === 'special') { 301 if (state.open && args.specialdate_message_open) msg = args.specialdate_message_open; 302 else if (!state.open && args.specialdate_message_closed) msg = args.specialdate_message_closed; 303 } else if (state.type === 'vacation') { 304 if (!state.open && args.vacation_message_closed) msg = args.vacation_message_closed; 305 } 306 307 msg = this.replacePlaceholders(msg, loc, state, args); 308 309 var cls = 'mb-bhi-display mb-bhi-' + (state.open ? 'open' : 'closed') + (args.extra_classes ? ' ' + args.extra_classes : ''); 310 return '<span class="' + cls + '"><span class="mb-bhi-oc-text">' + msg + '</span></span>'; 311 }, 312 313 replacePlaceholders: function(msg, loc, state, args) { 314 var now = this.getBusinessTime(loc.Timezone); 315 var next = state.next || now; 316 var fmt = args.format || this.data.settings.time_format; 317 var removeZeroes = args.removezeroes !== undefined ? args.removezeroes : this.data.settings.removezeroes; 318 319 var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
319November", "December"]; 320 var monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 321 322 var countdownSeconds = state.next ? Math.max(0, Math.floor((state.next - now) / 1000)) : 0; 323 324 var replacements = { 325 '{location}': loc.Name, 326 '{day}': this.data.i18n.days[this.getDayName(now)], 327 '{day_short}': this.data.i18n.days_short[this.getDayName(now)], 328 '{day_of_month}': now.getDate(), 329 '{month}': this.data.i18n.months[monthNames[now.getMonth()]], 330 '{month_short}': this.data.i18n.months_short[monthNamesShort[now.getMonth()]], 331 '{time}': this.formatTime(now, fmt, removeZeroes), 332 '{countdown}': this.getCountdownText(countdownSeconds), 333 '{minutes}': Math.ceil(countdownSeconds / 60), 334 '{x}': Math.ceil(countdownSeconds / 60), 335 336 '{next_open_time}': this.formatTime(next, fmt, removeZeroes), 337 '{next_open_day}': this.data.i18n.days[this.getDayName(next)], 338 '{next_open_day_short}': this.data.i18n.days_short[this.getDayName(next)], 339 '{next_open_month}': this.data.i18n.months[monthNames[next.getMonth()]], 340 '{next_open_month_short}': this.data.i18n.months_short[monthNamesShort[next.getMonth()]], 341 '{next_open_day_of_month}': next.getDate(), 342 343 '{name}': state.name || '', 344 '{specialdate_name}': state.type === 'special' ? state.name : '', 345 '{vacation_name}': state.type === 'vacation' ? state.name : '' 346 }; 347 348 for (var key in replacements) { 349 msg = msg.split(key).join(replacements[key]); 350 } 351 352 if (msg.indexOf('{from') !== -1 || msg.indexOf('{to') !== -1) { 353 var dayName = this.getDayName(now); 354 var dayData = loc.Days.find(function(d) { return d.Day === dayName; }); 355 var hours = (dayData && dayData.Hours) ? dayData.Hours.filter(function(h) { return h.From != 0 && h.To != 0; }) : []; 356 357 var demarcatedRegex = /\[(.*?)\][|]{2}\[(.*?)\]/; 358 var demarcatedMatch = msg.match(demarcatedRegex); 359 if (demarcatedMatch && demarcatedMatch.length === 3) { 360 msg = msg.split(demarcatedMatch[0]).join(hours.length > 1 ? demarcatedMatch[1] : demarcatedMatch[2]); 361 } 362 363 var self = this; 364 msg = msg.replace(/\{(from|to)([0-9]*)\}/g, function(match, type, num) { 365 var idx = num ? parseInt(num) - 1 : 0; 366 if (type === 'from') { 367 if (hours[idx]) { 368 var t = self.parseTime(hours[idx].From, hours[idx].FromIndication); 369 var d = new Date(now); d.setHours(t.h, t.m, 0, 0); 370 return self.formatTime(d, fmt, removeZeroes); 371 } 372 } else if (type === 'to') { 373 if (num) { 374 if (hours[idx]) { 375 var t = self.parseTime(hours[idx].To, hours[idx].ToIndication); 376 var d = new Date(now); d.setHours(t.h, t.m, 0, 0); 377 return self.formatTime(d, fmt, removeZeroes); 378 } 379 } else { 380 if (state.open) return self.formatTime(state.next, fmt, removeZeroes); 381 return self.formatTime(self.getNextClosingTime(loc, now), fmt, removeZeroes); 382 } 383 } 384 return ''; 385 }); 386 } 387 388 return msg; 389 }, 390 391 getNextClosingTime: function(loc, now) { 392 var tz = loc.Timezone || this.data.settings.timezone; 393 var smallest = null; 394 var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 395 396 for (var i = 0; i <= 7; i++) { 397 var date = new Date(now); 398 date.setDate(now.getDate() + i); 399 var dayName = dayNames[date.getDay()]; 400 var dayData = loc.Days.find(function(d) { return d.Day === dayName; }); 401 402 if (dayData && dayData.Hours) { 403 var periods = this.getOpenPeriods(dayData.Hours, date); 404 for (var j = 0; j < periods.length; j++) { 405 if (periods[j].to > now && (smallest === null || periods[j].to < smallest)) { 406 smallest = periods[j].to; 407 } 408 } 409 } 410 411 if (loc.Holidays) { 412 for (var j = 0; j < loc.Holidays.length; j++) { 413 var h = loc.Holidays[j]; 414 var hDate = this.parseDayMonth(h.Day, h.Month, tz, 0, 0, 0, h.Year); 415 if (hDate.toDateString() === date.toDateString()) { 416 if (h.Hours && h.Hours.length > 0) { 417 var periods = this.getOpenPeriods(h.Hours, date); 418 for (var k = 0; k < periods.length; k++) { 419 if (periods[k].to > now && (smallest === null || periods[k].to < smallest)) { 420 smallest = periods[k].to; 421 } 422 } 423 } 424 } 425 } 426 } 427 } 428 429 if (loc.Vacations) { 430 for (var i = 0; i < loc.Vacations.length; i++) { 431 var v = loc.Vacations[i]; 432 var vFrom = this.parseDayMonth(v.FromDay, v.From, tz, 0, 0, 0); 433 if (vFrom > now && (smallest === null || vFrom < smallest)) { 434 smallest = vFrom; 435 } 436 } 437 } 438 return smallest || now; 439 }, 440 441 getCountdownText: function(seconds) { 442 var days = Math.floor(seconds / 86400); 443 var hours = Math.floor((seconds % 86400) / 3600); 444 var minutes = Math.ceil((seconds % 3600) / 60); 445 446 if (minutes === 60) { minutes = 0; hours++; } 447 if (hours === 24) { hours = 0; days++; } 448 449 var parts = []; 450 if (days > 0) parts.push(days + ' ' + (days === 1 ? this.data.i18n.day : this.data.i18n.days_text)); 451 if (hours > 0) parts.push(hours + ' ' + (hours === 1 ? this.data.i18n.hour : this.data.i18n.hours)); 452 if (minutes > 0) parts.push(minutes + ' ' + (minutes === 1 ? this.data.i18n.minute : this.data.i18n.minutes)); 453 454 if (parts.length === 0) return '1 ' + this.data.i18n.minute; 455 return parts.join(', '); 456 }, 457 458 getDayName: function(date) { 459 return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()]; 460 }, 461 462 formatTime: function(date, format, removeZeroes) { 463 var h = date.getHours(); 464 var m = date.getMinutes(); 465 var ampm = h >= 12 ? 'PM' : 'AM'; 466 if (format == 12) { 467 h = h % 12; 468 h = h ? h : 12; 469 } 470 var mStr = m < 10 ? '0' + m : m; 471 var time = format == 12 ? h + ':' + mStr + ' ' + ampm : (h < 10 ? '0' + h : h) + ':' + mStr; 472 return removeZeroes ? time.replace(':00', '') : time; 473 }, 474 475 renderHoursList: function(args, isTodayOnly) { 476 var loc = this.findLocation(args.location); 477 if (!loc) return null; 478 var tz = loc.Timezone || this.data.settings.timezone; 479 var now = this.getBusinessTime(tz); 480 481 var isTable = args.output === 'table' || args.output == 1; 482 483 // Determine the view type, mirroring create_hours_list_model(). The 484 // legacy showonlytoday flag also suppresses the current-day highlight. 485 var showOnlyToday = isTodayOnly || args.showonlytoday === true || args.showonlytoday === 'true' || args.showonlytoday === 1 || args.showonlytoday === '1'; 486 var active = args.parent_code || 'mbhi_hours'; 487 var type; 488 if (active !== 'mbhi_hours' && active !== 'mbhi_hours_today') { 489 type = 'normal'; 490 } else { 491 type = args.view || 'normal'; 492 if (showOnlyToday) type = 'today'; 493 } 494 495 var showSpecials = args.includeholidays == true && type !== 'today'; 496 var showVacations = args.includevacations == true && type !== 'today'; 497 var consolidated = args.display == 1 || String(args.display).toLowerCase() === 'consolidated'; 498 499 // Build the normal-hours sets (one per displayed day) for this view. 500 var self = this; 501 var normalEntries = []; 502 if (!(args.hide_hours == true)) { 503 var sets = this.getListDates(now, type, args.startonsunday).map(function(d) { 504 return self.resolveSet(loc, d, now, args); 505 }); 506 normalEntries = consolidated ? this.consolidateAndOutput(sets, args) : this.setsToOutput(sets, args); 507 } 508 509 // Rollover only applies within rollover_from days of year end. 510 var rollover = args.rollover == true; 511 if (rollover) { 512 var lastDay = new Date(now.getFullYear(), 11, 31); 513 var diffDays = Math.round((this.dateOnly(lastDay) - this.dateOnly(now)) / 86400000); 514 if (diffDays > parseInt(args.rollover_from || 0, 10)) rollover = false;
515 } 516 517 var specialEntries = showSpecials ? this.specialsAsOutput(loc, now, consolidated, rollover, args) : []; 518 var vacationEntries = showVacations ? this.vacationsAsOutput(loc, now, consolidated, rollover, args) : []; 519 520 // Which section owns "today": 1 = normal, 2 = specials, 3 = vacations. 521 var hasCurrent = 1; 522 if (showSpecials && specialEntries.some(function(e) { return e.current; })) hasCurrent = 2; 523 if (showVacations && vacationEntries.some(function(e) { return e.current; })) hasCurrent = 3; 524 525 var containerCls = (args.extra_classes ? args.extra_classes + ' ' : '') + (isTable ? 'mabel-bhi-businesshours' : 'mabel-bhi-businesshours-inline'); 526 var html = isTable ? '<table class="' + containerCls + '">' : '<div class="' + containerCls + '">'; 527 528 html += this.renderEntries(normalEntries, isTable, hasCurrent === 1, 'normal', args); 529 530 if (showSpecials && specialEntries.length > 0) { 531 if (!isTable && normalEntries.length > 0) html += (args.entryseparator || ''); 532 html += this.renderEntries(specialEntries, isTable, hasCurrent === 2, 'holiday', args); 533 } 534 535 if (showVacations && vacationEntries.length > 0) { 536 if (!isTable && normalEntries.length > 0) html += (args.entryseparator || ''); 537 html += this.renderEntries(vacationEntries, isTable, hasCurrent === 3, 'vacations', args); 538 } 539 540 html += isTable ? '</table>' : '</div>'; 541 return html; 542 }, 543 544 // Mirror specials_as_output â every special date, optionally consolidated by hours. 545 specialsAsOutput: function(loc, now, consolidated, rollover, args) { 546 if (!loc.Holidays || loc.Holidays.length === 0) return []; 547 var self = this; 548 var monthFirst = args.date_format !== 'day first'; 549 var format = this.transformFormat(args.included_specials_format, args.date_format, args.abbreviatedays); 550 var midnight = this.dateOnly(now); 551 552 var specials = loc.Holidays.map(function(h) { 553 var date = self.parseDayMonth(h.Day, h.Month, null, 0, 0, 0, h.Year); 554 var hours = (h.Hours || []).filter(function(x) { return x.From != 0 && x.To != 0; }); 555 return { type: 'special', date: date, dayName: self.getDayName(date), hours: hours, name: h.Name || '', isToday: self.sameDate(date, now) }; 556 }).sort(function(a, b) { return a.date - b.date; }); 557 558 var entries = []; 559 specials.forEach(function(s) { 560 if (!args.dates_in_past && s.date < midnight) return; 561 entries.push({ range: self.getEntryRange(s, format, monthFirst), hours: self.entryHours(s, args), current: s.isToday, type: 'special' }); 562 }); 563 564 if (rollover) { 565 var max = new Date(now.getFullYear(), 0, 1); 566 max.setDate(max.getDate() + parseInt(args.rollover_to || 0, 10)); 567 specials.forEach(function(s) { 568 if (s.date <= max) entries.push({ range: self.getEntryRange(s, format, monthFirst), hours: self.entryHours(s, args), current: s.isToday, type: 'special' }); 569 }); 570 } 571 572 return consolidated ? this.consolidateEntriesByHours(entries, 'special') : entries; 573 }, 574 575 // Mirror vacations_as_output â every vacation (all merged into one row when consolidated). 576 vacationsAsOutput: function(loc, now, consolidated, rollover, args) { 577 if (!loc.Vacations || loc.Vacations.length === 0) return []; 578 var self = this; 579 var closed = this.data.i18n.closed || 'Closed'; 580 var format = this.transformFormat(args.included_vacations_format, args.date_format, args.abbreviatedays); 581 var midnight = this.dateOnly(now); 582 583 var vacs = loc.Vacations.map(function(v) { 584 var from = self.parseDayMonth(v.FromDay, v.From, null, 0, 0, 0); 585 var to = self.parseDayMonth(v.ToDay, v.To, null, 23, 59, 59); 586 if (to < from) to.setFullYear(to.getFullYear() + 1); 587 return { from: from, to: to, name: v.Name || '', spansToday: from <= now && now <= to }; 588 }).sort(function(a, b) { return a.from - b.from; }); 589 590 var entries = []; 591 vacs.forEach(function(v) { 592 if (!args.dates_in_past && v.to < midnight) return;
593 entries.push({ range: self.vacationRange(v, format), hours: closed, current: v.spansToday, type: 'vacation' }); 594 }); 595 596 if (rollover) { 597 var max = new Date(now.getFullYear(), 0, 1); 598 max.setDate(max.getDate() + parseInt(args.rollover_to || 0, 10)); 599 vacs.forEach(function(v) { 600 if (v.from <= max) entries.push({ range: self.vacationRange(v, format), hours: closed, current: v.spansToday, type: 'vacation' }); 601 }); 602 } 603 604 if (!consolidated || entries.length === 0) return entries; 605 606 return [{ 607 range: entries.map(function(e) { return e.range; }).join(', '), 608 hours: closed, 609 current: entries.some(function(e) { return e.current; }), 610 type: 'vacation' 611 }]; 612 }, 613 614 // Mirror the special consolidation: group entries sharing a range or hours string. 615 consolidateEntriesByHours: function(entries, type) { 616 var out = []; 617 entries.forEach(function(entry) { 618 var seen = out.some(function(x) { return x.range === entry.range || entry.hours === x.hours; }); 619 if (seen) return; 620 var row = entries.filter(function(x) { return x.range === entry.range || entry.hours === x.hours; }); 621 out.push({ 622 range: row.map(function(x) { return x.range; }).join(', '), 623 hours: entry.hours, 624 current: row.some(function(x) { return x.current; }), 625 type: type 626 }); 627 }); 628 return out; 629 }, 630 631 // Fill vacation range tokens, mirroring vacations_as_output. 632 vacationRange: function(v, format) { 633 var i18n = this.data.i18n; 634 var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
634November", "December"]; 635 var monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 636 return (format || '') 637 .split('{from_day_of_month}').join(v.from.getDate()) 638 .split('{from_month_short}').join(i18n.months_short[monthNamesShort[v.from.getMonth()]]) 639 .split('{from_month}').join(i18n.months[monthNames[v.from.getMonth()]]) 640 .split('{to_day_of_month}').join(v.to.getDate()) 641 .split('{to_month_short}').join(i18n.months_short[monthNamesShort[v.to.getMonth()]]) 642 .split('{to_month}').join(i18n.months[monthNames[v.to.getMonth()]]) 643 .split('{name}').join(v.name || ''); 644 }, 645 646 // Build the list of midnight Date objects for the requested view. 647 getListDates: function(now, type, startOnSunday) { 648 if (type === 'today') { 649 return [this.dateOnly(now)]; 650 } 651 if (type === 'future') { 652 var out = []; 653 for (var i = 0; i < 7; i++) { 654 var d = new Date(now); 655 d.setDate(now.getDate() + i); 656 d.setHours(0, 0, 0, 0); 657 out.push(d); 658 } 659 return out; 660 } 661 return this.datesForThisWeek(now, startOnSunday); 662 }, 663 664 // Mirror DateTime_Service::getDatesForThisWeek â the calendar week around today. 665 datesForThisWeek: function(now, startOnSunday) { 666 var order = startOnSunday ? 667 ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] : 668 ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; 669 var todayPos = order.indexOf(this.getDayName(now)); 670 var out = []; 671 for (var i = 0; i < order.length; i++) { 672 var d = new Date(now); 673 d.setDate(now.getDate() + (i - todayPos)); 674 d.setHours(0, 0, 0, 0); 675 out.push(d); 676 } 677 return out; 678 }, 679 680 dateOnly: function(d) { 681 var n = new Date(d); 682 n.setHours(0, 0, 0, 0); 683 return n; 684 }, 685 686 sameDate: function(a, b) { 687 return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); 688 }, 689 690 // Resolve a single date to its opening-hours set, mirroring get_set_on: 691 // a vacation or special date can replace the normal hours for that day. 692 resolveSet: function(loc, date, now, args) { 693 var dayName = this.getDayName(date); 694 var isToday = this.sameDate(date, now); 695 696 if (args.replace_with_vacations) { 697 var vac = this.findVacationOn(loc, date); 698 if (vac) { 699 return { type: 'vacation', hours: [], date: date, dayName: dayName, isToday: isToday, name: vac.Name || '' }; 700 } 701 } 702 703 if (args.replace_with_specials) { 704 var special = this.findSpecialOn(loc, date); 705 if (special) { 706 var sHours = (special.Hours || []).filter(function(h) { return h.From != 0 && h.To != 0; }); 707 return { type: 'special', hours: sHours, date: date, dayName: dayName, isToday: isToday, name: special.Name || '' }; 708 } 709 } 710 711 var dayData = loc.Days.find(function(d) { return d.Day === dayName; }); 712 var hours = (dayData && dayData.Hours) ? dayData.Hours.filter(function(h) { 713 return h.From != 0 && h.To != 0; 714 }) : []; 715 return { type: 'normal', hours: hours, date: date, dayName: dayName, isToday: isToday, name: '' }; 716 }, 717 718 // Mirror is_vacation â sliding +/- 1 year window, from <= date < end-of-ToDay. 719 findVacationOn: function(loc, date) { 720 if (!loc.Vacations) return null; 721 for (var i = 0; i < loc.Vacations.length; i++) { 722 var v = loc.Vacations[i]; 723 var from = this.parseDayMonth(v.FromDay, v.From, null, 0, 0, 0); 724 var to = this.parseDayMonth(v.ToDay, v.To, null, 23, 59, 59); 725 if (to < from) to.setFullYear(to.getFullYear() + 1); 726 for (var y = -1; y <= 1; y++) { 727 var f = new Date(from); f.setFullYear(f.getFullYear() + y); 728 var t = new Date(to); t.setFullYear(t.getFullYear() + y); 729 if (f <= date && date < t) return v; 730 } 731 } 732 return null; 733 }, 734 735 // Mirror is_special_date â a special whose day/month (and, for resolved 736 // dynamic entries, year) matches the date. 737 findSpecialOn: function(loc, date) { 738 if (!loc.Holidays) return null; 739 var self = this; 740 var monthIdx = date.getMonth(); 741 var day = date.getDate(); 742 var year = date.getFullYear(); 743 return loc.Holidays.find(function(h) { 744 return h.Day == day && self.monthIndex(h.Month) === monthIdx && (!h.Year || h.Year == year); 745 }) || null; 746 }, 747 748 // Mirror Helper_Service::transform â expand {from_date}/{to_date}/{date}/{day} shortcuts. 749 transformFormat: function(format, dateFormat, abbrevDays) { 750 var dayFirst = dateFormat === 'day first'; 751 return (format || '') 752 .split('{from_date}').join(dayFirst ? '{from_day_of_month} {from_month_short}' : '{from_month_short} {from_day_of_month}') 753 .split('{to_date}').join(dayFirst ? '{to_day_of_month} {to_month_short}' : '{to_month_short} {to_day_of_month}') 754 .split('{date}').join(dayFirst ? '{day_of_month} {month_short}' : '{month_short} {day_of_month}') 755 .split('{day}').join(abbrevDays ? '{day_short}' : '{day}'); 756 }, 757 758 // Mirror get_entry_range â the date/name label for a special or vacation set. 759 getEntryRange: function(set, format, monthFirst) { 760 var i18n = this.data.i18n; 761 var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
761November", "December"]; 762 var monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 763 var day = set.date.getDate(); 764 var monthShort = i18n.months_short[monthNamesShort[set.date.getMonth()]]; 765 var dateFormatted = monthFirst ? monthShort + ' ' + day : day + ' ' + monthShort; 766 return (format || '') 767 .split('{date}').join(dateFormatted) 768 .split('{name}').join(set.name || '') 769 .split('{day_short}').join(i18n.days_short[set.dayName]) 770 .split('{day_of_month}').join(day) 771 .split('{month_short}').join(monthShort) 772 .split('{month}').join(i18n.months[monthNames[set.date.getMonth()]]) 773 .split('{day}').join(i18n.days[set.dayName]); 774 }, 775 776 // Turn a list of sets into rendered entries (non-consolidated). 777 setsToOutput: function(sets, args) { 778 var self = this; 779 var monthFirst = args.date_format !== 'day first'; 780 return sets.map(function(set) { 781 var range; 782 if (set.type === 'special') { 783 range = self.getEntryRange(set, self.transformFormat(args.replaced_specials_format, args.date_format, args.abbreviatedays), monthFirst); 784 } else if (set.type === 'vacation') { 785 range = self.getEntryRange(set, self.transformFormat(args.replaced_vacations_format, args.date_format, args.abbreviatedays), monthFirst); 786 } else { 787 range = self.formatDayPart(set.date, set.dayName, args.day_format); 788 } 789 return { 790 range: range, 791 hours: self.entryHours(set, args), 792 current: set.isToday, 793 type: set.type 794 }; 795 }); 796 }, 797 798 // Group consecutive equal normal sets, then render each group as one entry. 799 consolidateAndOutput: function(sets, args) { 800 var self = this; 801 var monthFirst = args.date_format !== 'day first'; 802 return this.consolidateSets(sets).map(function(row) { 803 var base = row[0]; 804 var range; 805 if (base.type === 'special') { 806 range = self.getEntryRange(base, args.replaced_specials_format, monthFirst); 807 } else if (base.type === 'vacation') { 808 range = self.getEntryRange(base, args.replaced_vacations_format, monthFirst); 809 } else { 810 range = self.formatDayPart(base.date, base.dayName, args.day_format); 811 if (row.length > 1) { 812 var last = row[row.length - 1]; 813 range += (args.consolidationseparator || '') + self.formatDayPart(last.date, last.dayName, args.day_format); 814 } 815 } 816 return { 817 range: range, 818 hours: self.entryHours(base, args), 819 current: row.some(function(s) { return s.isToday; }), 820 type: base.type 821 }; 822 }); 823 }, 824 825 // Mirror consolidate_sets â only consecutive, equal, normal sets merge. 826 consolidateSets: function(sets) { 827 var consolidated = []; 828 var row = []; 829 for (var i = 0; i < sets.length; i++) { 830 var set = sets[i]; 831 if (row.length === 0) { row = [set]; continue; } 832 var isSame = true; 833 for (var j = 0; j < row.length; j++) { 834 if (set.type !== 'normal' || !this.setsEqual(set, row[j])) { isSame = false; break; } 835 } 836 if (isSame) { 837 row.push(set); 838 } else { 839 consolidated.push(row); 840 row = [set]; 841 } 842 } 843 consolidated.push(row); 844 return consolidated; 845 }, 846 847 // Two sets are equal when their opening-hour slots match (both closed counts as equal). 848 setsEqual: function(a, b) { 849 var ah = a.hours || [], bh = b.hours || []; 850 if (ah.length !== bh.length) return false;
851 if (ah.length === 0) return true; 852 for (var i = 0; i < ah.length; i++) { 853 var af = this.parseTime(ah[i].From, ah[i].FromIndication); 854 var at = this.parseTime(ah[i].To, ah[i].ToIndication); 855 var bf = this.parseTime(bh[i].From, bh[i].FromIndication); 856 var bt = this.parseTime(bh[i].To, bh[i].ToIndication); 857 if (af.h !== bf.h || af.m !== bf.m || at.h !== bt.h || at.m !== bt.m) return false; 858 } 859 return true; 860 }, 861 862 // Mirror format_day_part â resolve day-label tokens against a set's date. 863 formatDayPart: function(date, dayName, dayFormat) { 864 var i18n = this.data.i18n; 865 var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 866 var monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 867 var f = dayFormat || '{day}'; 868 return f 869 .split('{day_of_month}').join(date.getDate()) 870 .split('{month_short}').join(i18n.months_short[monthNamesShort[date.getMonth()]]) 871 .split('{month}').join(i18n.months[monthNames[date.getMonth()]]) 872 .split('{day_short}').join(i18n.days_short[dayName]) 873 .split('{day}').join(i18n.days[dayName]); 874 }, 875 876 // The hours portion of a set: "Closed" or the joined opening-hour slots. 877 entryHours: function(set, args) { 878 if (!set.hours || set.hours.length === 0) { 879 return this.data.i18n.closed || 'Closed'; 880 } 881 var breakChar = (args.mhbr && args.output != 2) ? '<br/>' : ' '; 882 var self = this; 883 return set.hours.map(function(h) { 884 return self.formatTimeStr(h, args.format, args.removezeroes, args.hourseparator); 885 }).join(breakChar); 886 }, 887 888 // Render a section of entries as table rows or inline spans, mirroring list.php. 889 renderEntries: function(entries, isTable, showCurrent, section, args) { 890 if (!entries || entries.length === 0) return ''; 891 var daySep = args.dayentryseparator || this.data.settings.dayentryseparator || ' '; 892 var inlineSep = isTable ? '' : (args.entryseparator || ''); 893 var html = ''; 894 for (var i = 0; i < entries.length; i++) { 895 var entry = entries[i]; 896 if (i > 0 && inlineSep) html += inlineSep; 897 var isCurrent = entry.current && showCurrent; 898 899 if (section === 'normal') { 900 if (isTable) { 901 html += '<tr' + (isCurrent ? ' class="mbhi-is-current"' : '') + '><td class="mabel-bhi-day">' + entry.range + '</td><td>' + entry.hours + '</td></tr>'; 902 } else { 903 html += '<span' + (isCurrent ? ' class="mbhi-is-current"' : '') + '><span class="mabel-bhi-day">' + entry.range + daySep + '</span> ' + entry.hours + '</span>'; 904 } 905 } else { 906 // 'holiday' or 'vacations' â the current flag is folded into the class attribute. 907 var extraCls = isCurrent ? ' mbhi-is-current' : ''; 908 if (isTable) { 909 html += '<tr class="mb-bhi-' + section + extraCls + '"><td>' + entry.range + '</td><td>' + entry.hours + '</td></tr>'; 910 } else { 911 html += '<span class="mb-bhi-' + section + extraCls + '">' + entry.range + daySep + ' ' + entry.hours + '</span>'; 912 } 913 } 914 } 915 return html; 916 }, 917 918 formatTimeStr: function(h, format, removeZeroes, hourSeparator) { 919 var f = this.parseTime(h.From, h.FromIndication); 920 var t = this.parseTime(h.To, h.ToIndication); 921 var df = new Date(); df.setHours(f.h, f.m); 922 var dt = new Date(); dt.setHours(t.h, t.m);
923 return this.formatTime(df, format, removeZeroes) + (hourSeparator || ' - ') + this.formatTime(dt, format, removeZeroes); 924 } 925 }; 926 927 $(document).ready(function() { BHIPro.init(); }); 928 929})(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.