PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-toolkit/assets/js/edms-frontend.js?ver=3.309.18

js mountaincabinsutah.com collected 2026-10-10 02:02:19 UTC 9,246 bytes, 276 lines download raw bytes

1/**
2 * HomeRunner Toolkit — Explorer Dynamic Min Stay (frontend).
3 *
4 * Patches HomeLocalCalendarUtils._calculateMinstay so that whenever
5 * a calendar instance has no per-date data for the day being queried
6 * (which is the Explorer's situation, since the inventory-wide picker
7 * never loads a property-specific calendar), the lookup falls through
8 * to a server-computed aggregate of `MIN(min_stay)` across the
9 * inventory for that date.
10 *
11 * Property booking widgets are not affected: they load a per-property
12 * calendar that populates `dateOptions(day)`, and our patch only
13 * consults the aggregate when `dateOptions(day)` is empty.
14 *
15 * Class binding: HomeLocalCalendarUtils is declared as a top-level
16 * `class` in homerunner-plugin/assets/js/calendar-utils.js, which
17 * creates a lexical global, NOT a property of `window`. Bare-name
18 * reference is the working pattern; same lesson as ANB v3.51.2.
19 *
20 * Second concern (3.195.0): a min-stay-aware zero-results state. The
21 * calendar patch above only helps visitors who open the picker; anyone
22 * arriving with checkin/checkout already in the URL (shared link,
23 * bookmark, back button, old email) fires the search unvalidated and
24 * lands on core's generic "No properties found" element. When the
25 * searched nights are below the aggregate minimum for the checkin date
26 * (which guarantees zero results), replace that element with an
27 * explanation and a one-click longer-stay suggestion. Decision logic
28 * lives in edms-empty.js (hrtkEdmsEmpty, a registered dependency);
29 * this file owns only the DOM.
30 *
31 * Config (window.hrtkExplorerMinStay):
32 *   - aggregate: { 'YYYY-MM-DD': minStayInt, ... }
33 *   - i18n: { title, tryLonger, chipNights } (optional, tokens documented
34 *     in Core.php; fallbacks below keep the feature alive without them)
35 */
36(function () {
37	'use strict';
38
39	var cfg = window.hrtkExplorerMinStay || {};
40	var aggregate = (cfg && cfg.aggregate) || {};
41	if (!aggregate || typeof aggregate !== 'object') {
42		return;
43	}
44
45	function toIsoDate(day) {
46		if (typeof day === 'string') {
47			// Already ISO-ish (YYYY-MM-DD) or close to it.
48			return /^\d{4}-\d{2}-\d{2}/.test(day) ? day.slice(0, 10) : null;
49		}
50		if (day && typeof day.toFormat === 'function') {
51			// Luxon DateTime support.
52			try {
53				return day.toFormat('yyyy-LL-dd');
54			} catch (e) {
55				/* fall through */
56			}
57		}
58		if (day && typeof day.getFullYear === 'function') {
59			var y = day.getFullYear();
60			var m = String(day.getMonth() + 1).padStart(2, '0');
61			var d = String(day.getDate()).padStart(2, '0');
62			return y + '-' + m + '-' + d;
63		}
64		return null;
65	}
66
67	function aggregateFor(day) {
68		var key = toIsoDate(day);
69		if (!key || !Object.prototype.hasOwnProperty.call(aggregate, key)) {
70			return 0;
71		}
72		var v = parseInt(aggregate[key], 10);
73		return v > 0 ? v : 0;
74	}
75
76	function patchCalendarUtils() {
77		if (typeof HomeLocalCalendarUtils !== 'function' || !HomeLocalCalendarUtils.prototype) {
78			return false;
79		}
80		if (HomeLocalCalendarUtils.prototype.__hrtkEdmsPatched) {
81			return true;
82		}
83
84		var origCalc = HomeLocalCalendarUtils.prototype._calculateMinstay;
85		HomeLocalCalendarUtils.prototype._calculateMinstay = function (day) {
86			// Try the host's normal path first.
87			var origResult = (typeof origCalc === 'function') ? origCalc.call(this, day) : 1;
88
89			// If this instance has per-date data for `day`, honor it
90			// (property booking widget case — PriceLabs values win).
91			if (typeof this.dateOptions === 'function') {
92				try {
93					var opts = this.dateOptions(day);
94					if (opts) {
95						return origResult;
96					}
97				} catch (e) {
98					return origResult;
99				}
100			}
101
102			// No per-date data on this instance — consult the aggregate.
103			var fromAggregate = aggregateFor(day);
104			if (fromAggregate > 0) {
105				return Math.min(fromAggregate, 100);
106			}
107			return origResult;
108		};
109
110		HomeLocalCalendarUtils.prototype.__hrtkEdmsPatched = true;
111		return true;
112	}
113
114	/**
115	 * Tiny '{key}' template substitution; the localized strings keep
116	 * documented tokens instead of printf placeholders (Core.php).
117	 */
118	function tpl(str, tokens) {
119		return String(str).replace(/\{(\w+)\}/g, function (m, key) {
120			return Object.prototype.hasOwnProperty.call(tokens, key) ? String(tokens[key]) : m;
121		});
122	}
123
124	/** "Aug 21" from '2026-08-21', pinned to UTC so no TZ off-by-one. */
125	function prettyDate(iso) {
126		try {
127			return new Date(iso + 'T00:00:00Z').toLocaleDateString(undefined, {
128				month: 'short',
129				day: 'numeric',
130				timeZone: 'UTC',
131			});
132		} catch (e) {
133			return iso;
134		}
135	}
136
137	function renderMinStayEmpty(target, verdict, i18n, logic) {
138		var wrap = document.createElement('div');
139		wrap.className = 'hrtk-edms-empty';
140
141		var title = document.createElement('h3');
142		title.className = 'hrtk-edms-empty-title';
143		title.textContent = tpl(
144			i18n.title || 'These dates require a minimum stay of {min} nights.',
145			{ min: verdict.required }
146		);
147		wrap.appendChild(title);
148
149		var subtitle = document.createElement('p');
150		subtitle.className = 'hrtk-edms-empty-subtitle';
151		subtitle.textContent = i18n.tryLonger || 'Try the same check-in with a longer stay:';
152		wrap.appendChild(subtitle);
153
154		var chips = document.createElement('div');
155		chips.className = 'hrtk-edms-empty-chips';
156		var chip = document.createElement('button');
157		chip.type = 'button';
158		chip.className = 'hrtk-edms-empty-chip';
159		chip.textContent = tpl(i18n.chipNights || '{from} – {to} · {min} nights', {
160			from: prettyDate(verdict.checkin),
161			to: prettyDate(verdict.suggestedCheckout),
162			min: verdict.required,
163		});
164		chip.addEventListener('click', function () {
165			window.location.href = logic.withCheckout(
166				window.location.href,
167				verdict.suggestedCheckout
168			);
169		});
170		chips.appendChild(chip);
171		wrap.appendChild(chips);
172
173		// textContent wipe clears core's generic message; everything above
174		// was built with createElement/textContent, so nothing is injectable.
175		target.textContent = '';
176		target.appendChild(wrap);
177	}
178
179	/**
180	 * The min-stay verdict for the CURRENT live URL, or null when no
181	 * claim may be made. Computed fresh on every render: the explorer
182	 * pushStates new params on each interactive search (explorer.js
183	 * app.updateUrl), so a verdict captured once at load would go stale
184	 * the moment the guest changes dates or filters, and a stale claim
185	 * is a false claim.
186	 */
187	function currentVerdict(logic) {
188		var search = window.location.search;
189
190		// SBP flexible-dates mode owns the zero-results UX for flex
191		// searches (flexible-search.js replaces the same node); flex
192		// URLs carry checkin/checkout too, so bow out entirely.
193		if (logic.getParam(search, 'flex') === 'flexible') {
194			return null;
195		}
196
197		var params = logic.parseSearch(search);
198		if (!params) {
199			return null;
200		}
201		// Claims read ONLY the certified snapshot pair (claimAggregate +
202		// uncertain, both null until an aggregation run certifies
203		// fleet-complete data); evaluate() fails closed on either. The
204		// picker's cfg.aggregate is deliberately not consulted here — it
205		// may be newer than the snapshot and the pair must stay consistent.
206		var verdict = logic.evaluate(cfg.claimAggregate, params.checkin, params.checkout, cfg.uncertain);
207		if (!verdict || !verdict.suggestedCheckout) {
208			return null;
209		}
210		verdict.checkin = params.checkin;
211		return verdict;
212	}
213
214	function setupEmptyState() {
215		var logic = window.hrtkEdmsEmpty;
216		if (
217			!logic ||
218			typeof logic.getParam !== 'function' ||
219			typeof logic.parseSearch !== 'function' ||
220			typeof logic.evaluate !== 'function' ||
221			typeof logic.withCheckout !== 'function'
222		) {
223			return; // dependency missing or global collided — enhancement off
224		}
225		if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') {
226			return;
227		}
228
229		var i18n = (cfg && cfg.i18n) || {};
230		var containers = document.querySelectorAll('.hfse .hfse-properties-container');
231		Array.prototype.forEach.call(containers, function (container) {
232			var render = function () {
233				var target = container.querySelector('.hfse-properties-no-items');
234				if (!target) {
235					return;
236				}
237				if (target.querySelector('.hrtk-edms-empty') || target.querySelector('.hrtk-fs-empty')) {
238					return;
239				}
240				var verdict = currentVerdict(logic);
241				if (!verdict) {
242					return;
243				}
244				renderMinStayEmpty(target, verdict, i18n, logic);
245			};
246			render(); // server-rendered empty state (initial page load)
247			if (typeof MutationObserver === 'function') {
248				new MutationObserver(render).observe(container, { childList: true });
249			}
250		});
251	}
252
253	// The empty-state layer is an optional enhancement; the calendar
254	// patch below is this module's load-bearing shipped feature and must
255	// survive ANY failure here (a hostile window.hrtkEdmsEmpty collision
256	// included). Deliberately swallowed: the enhancement degrades to
257	// core's generic message, which is the pre-3.195 status quo.
258	try {
259		setupEmptyState();
260	} catch (e) {
261		/* intentional — see above */
262	}
263
264	if (patchCalendarUtils()) {
265		return;
266	}
267
268	// Host script may load fractionally after us; poll briefly.
269	var attempts = 0;
270	var iv = setInterval(function () {
271		attempts++;
272		if (patchCalendarUtils() || attempts > 60) {
273			clearInterval(iv);
274		}
275	}, 150);
276})();

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.