PageSourceSearch

https://www.maxmotors.com/app/plugins/mabel-business-hours-indicator-pro/public/js/public.js?ver=3.3.9

js maxmotors.com collected 2026-09-26 14:18:52 UTC 45,576 bytes, 929 lines download raw bytes

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.