PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-toolk…/searchbar-pro-date-rules.js?ver=3.309.18

js mountaincabinsutah.com collected 2026-10-10 02:01:45 UTC 16,394 bytes, 376 lines download raw bytes

1/**
2 * Searchbar Pro — date rules for the date picker.
3 *
4 * Pure functions extracted so the logic is testable in Node
5 * (tests/js/searchbar-pro-date-rules.test.mjs), same contract as
6 * edms-empty.js: UMD-ish wrapper, ES5 syntax, dependency-free.
7 *
8 * Mirrors what the plugin's own explorer range calendar enforces
9 * (homerunner-plugin range-calendar.js dayClicked over calendar-utils.js,
10 * as patched by the toolkit's edms-frontend.js and anb-frontend.js):
11 *
12 *   - booking window: dates after today + max_num_days are unavailable
13 *     (calendar-utils.js _calculateUnavailability);
14 *   - advance notice: check-in dates before the server-computed earliest
15 *     check-in are blocked (anb-frontend.js, Advance Notice Blocking);
16 *   - minimum stay: the first selectable check-out is check-in + minimum,
17 *     where the minimum is the per-date inventory aggregate when the
18 *     Explorer Dynamic Min Stay module supplies one, else the site-wide
19 *     Explorer minimum, else 1 night.
20 *
21 * blockReason() applies them in the core picker's precedence.
22 */
23(function (root, factory) {
24	if (typeof module === 'object' && module.exports) {
25		module.exports = factory();
26	} else {
27		root.hrtkSbpDateRules = factory();
28	}
29})(typeof self !== 'undefined' ? self : this, function () {
30
31	var ISO_RE = /^\d{4}-\d{2}-\d{2}$/;
32	var DAY_MS = 86400000;
33	// calendar-utils.js caps _calculateMinstay at 100 nights; edms-frontend.js
34	// applies the same cap to the aggregate.
35	var MAX_MIN_STAY = 100;
36
37	/**
38	 * Strict ISO calendar date: the `YYYY-MM-DD` shape AND a real day of
39	 * that month. Shape alone is not enough — Date.UTC silently rolls
40	 * `2026-13-01` into January and `2026-02-30` into March, and the URL
41	 * hydration path hands this visitor-shaped input to the formatter.
42	 */
43	function isIso(v) {
44		if (!ISO_RE.test(v || '')) { return false; }
45		var y = parseInt(v.slice(0, 4), 10);
46		var m = parseInt(v.slice(5, 7), 10);
47		var d = parseInt(v.slice(8, 10), 10);
48		var t = new Date(Date.UTC(y, m - 1, d));
49		return !isNaN(t.getTime()) && t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
50	}
51
52	/** Positive integer night count from a localized value (may be a string), else 0. */
53	function nights(value) {
54		var n = parseInt(value, 10);
55		return n > 0 ? Math.min(n, MAX_MIN_STAY) : 0;
56	}
57
58	/**
59	 * Minimum stay for a check-in date.
60	 *
61	 * @param {string} checkin  ISO check-in date.
62	 * @param {{minStay?: number|string, byDate?: Object<string, number|string>}} cfg
63	 * @returns {number} At least 1.
64	 */
65	function resolve(checkin, cfg) {
66		cfg = cfg || {};
67		var byDate = cfg.byDate;
68		if (checkin && byDate && typeof byDate === 'object' && Object.prototype.hasOwnProperty.call(byDate, checkin)) {
69			var perDate = nights(byDate[checkin]);
70			if (perDate > 0) { return perDate; }
71		}
72		return nights(cfg.minStay) || 1;
73	}
74
75	/** ISO date `n` days after `iso` (UTC math, so no DST off-by-one); null for non-ISO input. */
76	function addDays(iso, n) {
77		if (!isIso(iso)) { return null; }
78		var d = new Date(iso + 'T00:00:00Z');
79		if (isNaN(d.getTime())) { return null; }
80		var out = new Date(d.getTime() + n * DAY_MS);
81		// An absurd `n` (a corrupted booking-window option) overflows the
82		// timestamp range; toISOString() on that throws, so answer null.
83		if (isNaN(out.getTime())) { return null; }
84		return out.toISOString().slice(0, 10);
85	}
86
87	/** First check-out the minimum allows: check-in + min stay (at least 1 night). */
88	function firstCheckout(checkin, minStay) {
89		return addDays(checkin, nights(minStay) || 1);
90	}
91
92	/**
93	 * Whether `iso` is a night the visitor cannot check out on: strictly
94	 * after the check-in and strictly before the first valid check-out.
95	 */
96	function inWindow(iso, checkin, minStay) {
97		if (!isIso(iso) || !isIso(checkin)) { return false; }
98		var first = firstCheckout(checkin, minStay);
99		return !!first && iso > checkin && iso < first;
100	}
101
102	/**
103	 * Last selectable date: today + maxDays (the core's max_num_days, which
104	 * Explorer.php also passes as max_stay). Null when no positive window is
105	 * configured, so the picker keeps its own navigation limit.
106	 */
107	function maxDate(today, maxDays) {
108		var n = parseInt(maxDays, 10);
109		if (!(n > 0) || !isIso(today)) { return null; }
110		return addDays(today, n);
111	}
112
113	/**
114	 * Why `iso` cannot be picked right now, in the core picker's order:
115	 * 'window' (after the booking window), 'notice' (before the earliest
116	 * check-in), 'min-stay' (a check-out short of check-in + minimum while a
117	 * check-out is pending), or null. Past dates return null — the picker
118	 * already renders those as its own non-interactive disabled state.
119	 *
120	 * @param {string} iso
121	 * @param {{today?: string, maxDate?: string|null, minCheckin?: string|null,
122	 *          checkin?: string|null, checkout?: string|null, minStay?
122: number|string}} ctx
123	 * @returns {'window'|'notice'|'min-stay'|null}
124	 */
125	function blockReason(iso, ctx) {
126		ctx = ctx || {};
127		if (!isIso(iso)) { return null; }
128		if (isIso(ctx.today) && iso < ctx.today) { return null; }
129		if (isIso(ctx.maxDate) && iso > ctx.maxDate) { return 'window'; }
130		if (isIso(ctx.minCheckin) && iso < ctx.minCheckin) { return 'notice'; }
131		if (ctx.checkin && !ctx.checkout && inWindow(iso, ctx.checkin, ctx.minStay)) { return 'min-stay'; }
132		return null;
133	}
134
135	/** Fill the core calendar's `{night}` token; the strings keep it verbatim for translators. */
136	function message(template, n) {
137		return String(template == null ? '' : template).replace(/\{night\}/g, String(n));
138	}
139
140	/** Generic `{key}` substitution (the localized strings keep documented tokens, not printf). */
141	function fill(template, tokens) {
142		return String(template == null ? '' : template).replace(/\{(\w+)\}/g, function (m, key) {
143			return Object.prototype.hasOwnProperty.call(tokens, key) ? String(tokens[key]) : m;
144		});
145	}
146
147	/**
148	 * The nearest valid stay for a min-stay rejection while picking a
149	 * check-out: check-in + minimum (calendar-hover-tips.js suggestRangeStay,
150	 * which for the explorer range calendar is exactly this clamp).
151	 *
152	 * @returns {{checkin: string, checkout: string, nights: number}|null}
153	 */
154	function suggestStay(checkin, minStay) {
155		if (!isIso(checkin)) { return null; }
156		var n = nights(minStay) || 1;
157		var checkout = addDays(checkin, n);
158		return checkout ? { checkin: checkin, checkout: checkout, nights: n } : null;
159	}
160
161	/**
162	 * One-tap button label for a suggestion — "Select Sep 05 – Sep 08 · 3
163	 * nights" — from the Calendar Hover Tips strings (`select`, `nightOne`,
164	 * `nightOther`) and the picker's own day formatter.
165	 *
166	 * @param {{checkin: string, checkout: string, nights: number}} suggestion
167	 * @param {{select?: string, nightOne?: string, nightOther?: string}} i18n
168	 * @param {function(string): string} formatDay
169	 */
170	function applyLabel(suggestion, i18n, formatDay) {
171		if (!suggestion) { return ''; }
172		var s = i18n || {};
173		var nightsText = fill(suggestion.nights === 1 ? (s.nightOne || '{n} night') : (s.nightOther || '{n} nights'), { n: suggestion.nights });
174		return fill(s.select || 'Select {from} – {to} · {nights}', {
175			from: formatDay(suggestion.checkin),
176			to: formatDay(suggestion.checkout),
177			nights: nightsText,
178		});
179	}
180
181	var EN_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
182	var EN_DAYS   = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
183
184	function pad2(n) {
185		return (n < 10 ? '0' : '') + n;
186	}
187
188	/**
189	 * A numeric token the way Luxon renders it: through the locale's own
190	 * number formatter, so a locale whose default numbering system is not
191	 * Latin (ar-EG, bn-BD) gets its digits — "سبتمبر ٠٦" for `LLL dd` — and
192	 * every Latin locale gets the plain padded ASCII it always got.
193	 */
194	function num(n, pad, locale) {
195		try {
196			if (typeof Intl !== 'undefined' && Intl.NumberFormat) {
197				return new Intl.NumberFormat(locale || undefined, { useGrouping: false, minimumIntegerDigits: pad || 1 }).format(n);
198			}
199		} catch (e) { /* fall through to ASCII */ }
200		return pad === 2 ? pad2(n) : String(n);
201	}
202
203	/** Intl-formatted piece of a UTC date for the locale, with an English fallback. */
204	function intlPart(date, opts, locale, fallback) {
205		try {
206			if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
207				var o = { timeZone: 'UTC' };
208				for (var k in opts) { if (Object.prototype.hasOwnProperty.call(opts, k)) { o[k] = opts[k]; } }
209				return new Intl.DateTimeFormat(locale || undefined, o).format(date);
210			}
211		} catch (e) { /* fall through to English */ }
212		return fallback;
213	}
214
215	/**
216	 * A month or weekday name from Intl in Luxon's two grammatical forms:
217	 * standalone (`L`/`c` tokens — the bare name, e.g. Russian "сентябрь")
218	 * or format context (`M`/`E` tokens — the name as it reads inside a
219	 * date, "6 сентября" → "сентября"). Luxon builds the context form by
220	 * formatting a fuller date and extracting the part; formatToParts does
221	 * the same here, with the standalone Intl call as the fallback (the two
222	 * forms are identical in English anyway).
223	 */
224	function intlName(date, type, length, formatContext, locale, fallback) {
225		var standalone = {};
226		standalone[type] = length;
227		if (!formatContext) { return intlPart(date, standalone, locale, fallback); }
228		var opts = { timeZone: 'UTC' };
229		opts[type] = length;
230		if (type === 'month') { opts.day = 'numeric'; }
231		else { opts.month = 'long'; opts.day = 'numeric'; }
232		try {
233			if (typeof Intl !== 'undefined' && Intl.DateTimeFormat && Intl.DateTimeFormat.prototype.formatToParts) {
234				var parts = new Intl.DateTimeFormat(locale || undefined, opts).formatToParts(date);
235				for (var i = 0; i < parts.length; i++) {
236					if (parts[i].type === type) { return parts[i].value; }
237				}
238			}
239		} catch (e) { /* fall through to the standalone form */ }
240		return intlPart(date, standalone, locale, fallback);
241	}
242
243	/**
244	 * Format an ISO date with a Luxon token string — the subset that applies
245	 * to a date-only value: d/dd, L/M families (number, padded, short, long,
246	 * narrow), E/c weekday families (ISO number, short, long, narrow),
247	 * y/yy/yyyy, the D/DD/DDD/DDDD localized macros, and single-quoted
248	 * literals. The Explorer's "Date format (js)" setting
249	 * (homelocal_explorer_date_format) is written in these tokens. Names come
250	 * from Intl for `locale` (the browser default when omitted), so they match
251	 * what Luxon renders for the core search bar; digits go through
252	 * Intl.NumberFormat for the same reason (ar-EG gets ٠٦). `L`/`c` are Luxon's
253	 * standalone name forms and `M`/`E` its format-context forms — the same
254	 * in English, distinct in e.g. Russian or Polish. Unknown tokens pass
255	 * through unchanged; a non-ISO or impossible (`2026-13-01`) input
256	 * returns ''.
257	 */
258	function formatDate(iso, format, locale) {
259		if (!isIso(iso)) { return ''; }
260		var y = parseInt(iso.slice(0, 4), 10);
261		var m = parseInt(iso.slice(5, 7), 10);
262		var d = parseInt(iso.slice(8, 10), 10);
263		var date = new Date(Date.UTC(y, m - 1, d));
264		if (isNaN(date.getTime())) { return ''; }
265		var fmt = String(format == null || format === '' ? 'LLL dd' : format);
266		var dow = date.getUTCDay();
267		var isoDow = dow === 0 ? 7 : dow;
268
269		function token(t) {
270			switch (t) {
271				case 'd':      return num(d, 1, locale);
272				case 'dd':     return num(d, 2, locale);
273				case 'L': case 'M':          return num(m, 1, locale);
274				case 'LL': case 'MM':        return num(m, 2, locale);
275				case 'LLL':    return intlName(date, 'month', 'short', false, locale, EN_MONTHS[m - 1].slice(0, 3));
276				case 'MMM':    return intlName(date, 'month', 'short', true, locale, EN_MONTHS[m - 1].slice(0, 3));
277				case 'LLLL':   return intlName(date, 'month', 'long', false, locale, EN_MONTHS[m - 1]);
278				case 'MMMM':   return intlName(date, 'month', 'long', true, locale, EN_MONTHS[m - 1]);
279				case 'LLLLL':  return intlName(date, 'month', 'narrow', false, locale, EN_MONTHS[m - 1].charAt(0));
280				case 'MMMMM':  return intlName(date, 'month', 'narrow', true, locale, EN_MONTHS[m - 1].charAt(0));
281				case 'E': case 'c':          return num(isoDow, 1, locale);
282				case 'ccc':    return intlName(date, 'weekday', 'short', false, locale, EN_DAYS[dow].slice(0, 3));
283				case 'EEE':    return intlName(date, 'weekday', 'short', true, locale, EN_DAYS[dow].slice(0, 3));
284				case 'cccc':   return intlName(date, 'weekday', 'long', false, locale, EN_DAYS[dow]);
285				case 'EEEE':   return intlName(date, 'weekday', 'long', true, locale, EN_DAYS[dow]);
286				case 'ccccc':  return intlName(date, 'weekday', 'narrow', false, locale, EN_DAYS[dow].charAt(0));
287				case 'EEEEE':  return intlName(date, 'weekday', 'narrow', true, locale, EN_DAYS[dow].charAt(0));
288				case 'y':      return num(y, 1, locale);
289				case 'yyyy':   return num(y, 4, locale);
290				case 'yy':     return num(y % 100, 2, locale);
291				case 'D':      return intlPart(date, { year: 'numeric', month: 'numeric', day: 'numeric' }, locale, m + '/' + d + '/' + y);
292				case 'DD':     return intlPart(date, { year: 'numeric', month: 'short', day: 'numeric' }, locale, EN_MONTHS[m - 1].slice(0, 3) + ' ' + d + ', ' + y);
293				case 'DDD':    return intlPart(date, { year: 'numeric', month: 'long', day: 'numeric' }, locale, EN_MONTHS[m - 1] + ' ' + d + ', ' + y);
294				case 'DDDD':   return intlPart(date, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }, locale, EN_DAYS[dow] + ', ' + EN_MONTHS[m - 1] + ' ' + d + ', ' + y);
295				default:       return t;
296			}
297		}
298
299		var out = '';
300		var i = 0;
301		while (i < fmt.length) {
302			var ch = fmt.charAt(i);
303			if (ch === "'") {
304				// Luxon (3.7): a doubled quote outside a literal is an apostrophe
305				// ("MMM d ''yy" → "Sep 6 '26"); inside a literal it closes and
306				// reopens ("'o''clock'" → "oclock"); an unterminated quote runs
307				// to the end of the format.
308				if (fmt.charAt(i + 1) === "'") { out += "'"; i += 2; continue; }
309				var end = fmt.indexOf("'", i + 1);
310				if (end === -1) { out += fmt.slice(i + 1); break; }
311				out += fmt.slice(i + 1, end);
312				i = end + 1;
313				continue;
314			}
315			if (!/[a-zA-Z]/.test(ch)) { out += ch; i++; continue; }
316			var j = i;
317			while (j < fmt.length && fmt.charAt(j) === ch) { j++; }
318			out += token(fmt.slice(i, j));
319			i = j;
320		}
321		return out;
322	}
323
324	/**
325	 * Dates an Explorer shortcode presets for its own page (v3.289.1, #858).
326	 *
327	 * A page can pin its grid to a stay with `[homerunner-explorer checkin=…
328	 * checkout=…]`. The server filters by that pair and hands it to the front
329	 * end as `shortcode_params` in the Explorer config, with nothing in the
330	 * URL. The search bar and the Explorer bridge read dates from the URL
331	 * only, so such a page showed a filtered grid under an empty date field
332	 * and base "Starting at" pricing. Callers consult this when the URL
333	 * carries no dates.
334	 *
335	 * Accepts one Explorer config, one `{ selector, config }` entry, or a list
336	 * of either (window.homerunner_explorer_shortcodes). Returns the first
337	 * pair that passes the URL hydration checks: real ISO dates, check-in
338	 * before check-out, check-in not before `today`.
339	 *
340	 * @param {Object|Array} configs Explorer config(s).
341	 * @param {string}       today   YYYY-MM-DD; anything else skips the past check.
342	 * @return {{checkin: string, checkout: string}|null}
343	 */
344	function presetRange(configs, today) {
345		var list = Object.prototype.toString.call(configs) === '[object Array]' ? configs : [ configs ];
346		for (var i = 0; i < list.length; i++) {
347			var entry = list[i];
348			if (!entry || typeof entry !== 'object') { continue; }
349			var cfg = (entry.config && typeof entry.config === 'object') ? entry.config : entry;
350			var sp  = cfg.shortcode_params;
351			if (!sp || typeof sp !== 'object') { continue; }
352			var ci = typeof sp.checkin  === 'string' ? sp.checkin  : '';
353			var co = typeof sp.checkout === 'string' ? sp.checkout : '';
354			if (isIso(ci) && isIso(co) && ci < co && (!isIso(today) || ci >= today)) {
355				return { checkin: ci, checkout: co };
356			}
357		}
358		return null;
359	}
360
361	return {
362		isIso: isIso,
363		resolve: resolve,
364		addDays: addDays,
365		firstCheckout: firstCheckout,
366		inWindow: inWindow,
367		maxDate: maxDate,
368		blockReason: blockReason,
369		message: message,
370		fill: fill,
371		suggestStay: suggestStay,
372		applyLabel: applyLabel,
373		formatDate: formatDate,
374		presetRange: presetRange,
375	};
376});

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.