PageSourceSearch

https://www.housingwire.com/_static/??-eJyVj8sOAiEMRX9IrMZXXBj/wq2…/a3tltWPf7+ab7Wy7WizXs+4B9s+YGQ==

js housingwire.com collected 2026-10-09 22:41:22 UTC 45,569 bytes, 1,388 lines download raw bytes

1document.addEventListener("DOMContentLoaded", () => {
2	/**
3	 * Add referral url as a parameter whenever a link to the subscribe page is clicked
4	 */
5	document.addEventListener("click", function (event) {
6		const link = event.target.closest("a[href]");
7		if (!link) return;
8
9		const clickedUrl = new URL(link.href, window.location.origin);
10		const targetUrl = new URL(howMixpanel.subscribe_page_url);
11
12		// Normalize hostnames by removing "www." prefix if present
13		const normalizeHostname = (hostname) =>
14			hostname.startsWith("www.") ? hostname.slice(4) : hostname;
15
16		// Normalize pathname by removing trailing slash if present (except root "/")
17		const normalizePathname = (pathname) =>
18			pathname !== "/" && pathname.endsWith("/")
19				? pathname.slice(0, -1)
20				: pathname;
21
22		const clickedHost = normalizeHostname(clickedUrl.hostname);
23		const targetHost = normalizeHostname(targetUrl.hostname);
24
25		const clickedPath = normalizePathname(clickedUrl.pathname);
26		const targetPath = normalizePathname(targetUrl.pathname);
27
28		// Check if hostnames and paths match (ignoring protocol and www, trailing slash)
29		if (clickedHost === targetHost && clickedPath === targetPath) {
30			event.preventDefault();
31
32			clickedUrl.searchParams.set("ref", window.location.href);
33
34			window.location.href = clickedUrl.toString();
35		}
36	});
37
38	/**
39	 * If no referral url is set on the subscribe page, add direct, a search engine name, or undefined
40	 */
41	(function () {
42		const currentUrl = window.location.href;
43		const targetUrl = howMixpanel.subscribe_page_url;
44
45		if (currentUrl === targetUrl) {
46			const url = new URL(currentUrl);
47			if (!url.searchParams.has("ref")) {
48				let ref = "direct";
49
50				if (document.referrer) {
51					try {
52						const referrerUrl = new URL(document.referrer);
53						if (referrerUrl.hostname !== window.location.hostname) {
54							ref = referrerUrl.hostname;
55						}
56					} catch {
57						ref = "undefined";
58					}
59				}
60
61				url.searchParams.set("ref", ref);
62
63				window.history.replaceState({}, "", url.toString());
64			}
65		}
66	})();
67
68	function waitForMixpanel(callback) {
69		if (
70			typeof mixpanel !== "undefined" &&
71			mixpanel.track &&
72			window.orchestrator &&
73			window.orchestrator.dataFetched
74		) {
75			callback();
76		} else {
77			setTimeout(() => waitForMixpanel(callback), 100);
78		}
79	}
80
81	async function trackEventsSegmented() {
82		const baseProps = {
83			page: window.location.pathname,
84			user_type: window.orchestrator.userType,
85			user_id: window.orchestrator.userId,
86			user_name: window.orchestrator.userName,
87			user_email: window.orchestrator.userEmail,
88			user_role: window.orchestrator.userRole,
89		};
90
91		//console.log("trackEvents called with baseProps:", baseProps);
92
93		mixpanel.track("page_view_segmented", baseProps);
94		//console.log("Sent page_view_segmented event");
95
96		if (typeof howMixpanel !== "undefined" && howMixpanel.article_title) {
97			//console.log("Sending Article Read event for:", howMixpanel.article_title);
98			mixpanel.track("Article Read", {
99				...baseProps,
100				"Article Name": howMixpanel.article_title,
101			});
102		} else {
103			//console.log("No article title found, skipping Article Read event");
104		}
105	}
106
107	waitForMixpanel(() => {
108		trackEventsSegmented();
109		document.addEventListener("click", function (e) {
110			const trackableElement = e.target.closest('[data-mixpanel="true"]');
111
112			if (trackableElement) {
113				try {
114					const location =
115						trackableElement.getAttribute("data-mixpanel-location") ||
116						"Unknown";
117					const sublocation =
118						trackableElement.getAttribute("data-mixpanel-sublocation") ||
119						"Unknown";
120					const label =
121						trackableElement.getAttribute("data-mixpanel-label") || "Unknown";
122					const page = window.location.pathname;
123
124					mixpanel.track(`${location} - ${label} Clicked`, {
125						page: page,
126						button_location: sublocation,
127						user_type: window.orchestrator.userType,
128					});
129				} catch (error) {
130					console.error("Mixpanel tracking error:", error);
131				}
132			}
133		});
134	});
135});
136;
137// MR Infinite Table Script (vanilla JS)
138// Assumptions (per your "proceed with defaults"):
139// - API pagination params: limit & offset
140// - Server-side sorting via `category` query param
141// - Other filters/search are applied client-side to fetched results
142// - Table container IDs/classes provided by you are used as-is
143// - Page size = 20
144//
145// Wire this script into your page (e.g., put in a <script> tag after your HTML).
146(() => {
147	// Config
148	const MR_SETTINGS = window.MR_Settings || {};
149	const API_BASE = MR_SETTINGS.api_base || "/wp-json/rt-lo/v1/ranked";
150	const YEAR = MR_SETTINGS.year || "2025";
151	const AD_IMAGES = how_mortgageRankings.ad_images || null; // expected shape: { small: '...', medium: '...', large: '...' }
152	const AD_URL = how_mortgageRankings.ad_url || null;
153	const category =
154		window.how_mortgageRankings &&
155		"undefined" !== typeof how_mortgageRankings.category
156			? how_mortgageRankings.category
157			: "";
158	const type = how_mortgageRankings.type;
159	const COLSPAN =
160		"undefined" !== typeof category && "yoy_volume" === category ? 8 : 7;
161	let PAGE_SIZE = MR_SETTINGS.page_size || 20;
162
163	// category will be read into `category` (see below). Scroll threshold can also be overridden.
164	const SCROLL_THRESHOLD_PX = MR_SETTINGS.scroll_threshold || 300;
165
166	// DOM selectors
167	const tableBody = document.querySelector("#MR_TableBody");
168	const sortTabsContainer = null;
169
170	// Defer these so we can resolve them at init() (after DOM ready)
171	let searchInput = null;
172	let stateFilter = null;
173	let brokerFilter = null;
174	let minVolumeInput = null;
175	let minUnitsInput = null;
176	let employerSearchInput = null;
177
178	const loaders = document.querySelectorAll(".mr_loader");
179	const mrTitle = document.querySelector("#MR_title");
180
181	const TABLE_ROW_TEMPLATE = {
182		className: "mr-no-results",
183
184		// change this if the Year over Year Growth category needs a different c
184olspan
185
186		htmlNoResults() {
187			return `<td colspan="${COLSPAN}" style="text-align:center; padding: 6rem 1rem!important; color:#666;">No results</td>`;
188		},
189		htmlLoading() {
190			return `<td colspan="${COLSPAN}" style="text-align:center; padding: 6rem 1rem!important; color:#666;"><span class="mr-intable-loading">Loading...</span></td>`;
191		},
192	};
193
194	// Try server-side search. Set to false to use client-side name filtering.
195	const SERVER_SIDE_SEARCH = true;
196
197	// Parameter name to send to the API for name search. Change to 'q' or 'search' if your API uses that.
198	const SERVER_SEARCH_PARAM = "name";
199	let lastServerSearch = "";
200
201	const SERVER_EMPLOYER_SEARCH_PARAM = "employer";
202	let lastServerEmployerSearch = "";
203
204	if (!tableBody) {
205		console.error(
206			"Required DOM elements (#MR_TableBody or #MR_sortTabs) not found.",
207		);
208		return;
209	}
210
211	// State
212	let offset = 0;
213	let isLoading = false;
214	let exhausted = false; // no more pages
215	let fetchedItems = []; // all items fetched so far (kept in server order)
216	let renderRequestId = 0; // for debounce invalidation
217	let infiniteObserver = null;
218
219	// Utility formatters
220	const currencyFmt = new Intl.NumberFormat("en-US", {
221		style: "currency",
222		currency: "USD",
223		maximumFractionDigits: 0,
224	});
225	const numberFmt = new Intl.NumberFormat("en-US");
226
227	// Helpers to show/hide loader elements
228	function showInitialLoader() {
229		// clear table and show the in-table loading row
230		tableBody.innerHTML = "";
231		const tr = document.createElement("tr");
232		tr.className = TABLE_ROW_TEMPLATE.className;
233		tr.innerHTML = TABLE_ROW_TEMPLATE.htmlLoading();
234		tableBody.appendChild(tr);
235	}
236	function hideInitialLoader() {
237		const existing = tableBody.querySelector(
238			"tr." + TABLE_ROW_TEMPLATE.className,
239		);
240		if (existing) {
241			existing.remove();
242		}
243	}
244
245	// Replace showAppendLoader / hideAppendLoader with this minimal implementation
246	// (ensures the loader is inserted after the <table> element, never inside the <tbody>)
247
248	// REPLACE your current showAppendLoader() and hideAppendLoader() with this more robust version.
249	// This ensures the append loader is inserted after the nearest wrapper (.mr-table-wrap) or the table element,
250	// and removes any in-table loader first so it doesn't look like it's inside the table.
251
252	function showAppendLoader() {
253		// don't add another if it already exists
254		if (document.getElementById("MR_append_loader")) {
255			return;
256		}
257
258		// ensure any in-table loader is removed so there's no visual overlap
259		if ("function" === typeof hideInitialLoader) {
260			hideInitialLoader();
261		}
262
263		// Prefer the wrapper if present
264		const tableEl = tableBody.closest("table");
265		let insertAfterEl = null;
266
267		if (tableEl) {
268			// look for a wrapper you might be using, fallback to the table element
269			const wrap = tableEl.closest(".mr-table-wrap") || tableEl.parentElement;
270			insertAfterEl = wrap || tableEl;
271		} else {
272			// fallback to tableBody parent
273			insertAfterEl = tableBody.parentElement || document.body;
274		}
275
276		const wrapper = document.createElement("div");
277		wrapper.id = "MR_append_loader";
278		wrapper.className = "mr-append-loader";
279		wrapper.setAttribute("role", "status");
280		wrapper.setAttribute("aria-live", "polite");
281		wrapper.style.textAlign = "center";
282		wrapper.style.padding = "12px 0";
283		wrapper.innerHTML =
284			'<div class="mr-intable-loading" aria-hidden="true"><span class="mr-intable-loading-label" style="color: #666">Loading...</span></div>';
285
286		// Insert after the chosen element
287		if (insertAfterEl && insertAfterEl.parentNode) {
288			if (insertAfterEl.nextSibling) {
289				insertAfterEl.parentNode.insertBefore(
290					wrapper,
291					insertAfterEl.nextSibling,
292				);
293			} else {
294				insertAfterEl.parentNode.appendChild(wrapper);
295			}
296		} else {
297			document.body.appendChild(wrapper);
298		}
299	}
300
301	function hideAppendLoader() {
302		const el = document.getElementById("MR_append_loader");
303		if (el) {
304			el.remove();
305		}
306	}
307
308	// Build fetch URL with params
309	// Replace your existing buildUrl function with this version that uses the `name` query param for server-side search.
310	function buildUrl(params = {}) {
311		// Create a URL object that works if API_BASE is absolute or relative
312		let url;
313		try {
314			url = new URL(API_BASE); // works if API_BASE is absolute (https://...)
315		} catch (err) {
316			// fallback: treat API_BASE as relative to the current origin
317			url = new URL(API_BASE, window.location.origin);
318		}
319
320		// Ensure year & category are set (these override any existing query values)
321		url.searchParams.set("year", YEAR);
322		url.searchParams.set("category", category);
323
324		// Add provided params (limit, offset, etc.)
325		Object.keys(params).forEach((k) => {
326			url.searchParams.set(k, params[k]);
327		});
328
329		// If server-side search is enabled and there's a search term, include it as `name`
330		if (SERVER_SIDE_SEARCH) {
331			if (searchInput) {
332				const q = (searchInput.value || "").trim();
333				if (q) {
334					url.searchParams.set(SERVER_SEARCH_PARAM || "name", q);
335				} else {
336					url.searchParams.delete(SERVER_SEARCH_PARAM || "name");
337				}
338			}
339
340			if (employerSearchInput) {
341				const employerQ = (employerSearchInput.value || "").trim();
342				if (employerQ) {
343					url.searchParams.set(
344						SERVER_EMPLOYER_SEARCH_PARAM || "employer",
345						employerQ,
346					);
347				} else {
348					url.searchParams.delete(SERVER_EMPLOYER_SEARCH_PARAM || "employer");
349				}
350			}
351		}
352
353		// Add min_units (read current input value at call time). fallback if no ID.
354		(function addMinUnits() {
355			const group =
356				document.querySelector(".flex.flex-wrap.items-end") || document;
357			const fallbackUnits = group
358				? Array.from(group.querySelectorAll('input[type="number"]'))[1] || null
359				: null;
360			const unitsEl =
361				document.getElementById("MR_minUnits") ||
362				minUnitsInput ||
363				fallbackUnits;
364			if (unitsEl) {
365				const u = (unitsEl.value || "").toString().trim();
366				if ("" !== u && !Number.isNaN(Number(u))) {
367					url.searchParams.set("min_units", u);
368				} else {
369					url.searchParams.delete("min_units");
370				}
371			} else {
372				url.searchParams.delete("min_units");
373			}
374		})();
375
376		// Add state filter when present (read live)
377		if (stateFilter) {
378			const sv = (stateFilter.value || "").toString().trim();
379			if (sv && "All" !== sv) {
380				url.searchParams.set("state", sv);
381			} else {
382				url.searchParams.delete("state");
383			}
384		}
385
386		return url.toString();
387	}
388
389	// Replace your existing fetchPage function with this version.
390	// This updates #MR_number on every successful fetch when `json.count` is present.
391	// Helper: are we allowed to fetch beyond the first page?
392	// Helper: are we allowed to fetch beyond the first page?
393	function allowedToFetchMore() {
394		// Restriction only applies when type is 'segmented'.
395		// If not segmented, allow fetching more for all users.
396		if ("segmented" !== type) {
397			return true;
398		}
399
400		// When segmented, only 'subscriber' or 'registered_user' may fetch past the first page
401		return (
402			"subscriber" === window.orchestrator.userType ||
403			"registered_user" === window.orchestrator.userType
404		);
405	}
406
407	async function fetchPage() {
408		// Prevent concurrent fetches or when we've already exhausted results
409		if (isLoading || exhausted) {
410			return;
411		}
412
413		// If user is not allowed to fetch more and we've already fetched the first page, stop here
414		if (!allowedToFetchMore() && offset >= PAGE_SIZE) {
415			exhausted = true;
416
417			// ensure any append loader is removed
418			hideAppendLoader();
419			return;
420		}
421
422		isLoading = true;
423		if (0 === offset) {
424			showInitialLoader();
425		} else {
426			showAppendLoader();
427		}
428
429		try {
430			if (
431				"segmented" == type &&
432				!(
433					"subscriber" === window.orchestrator.userType ||
434					"registered_user" === window.orchestrator.userType
435				)
436			) {
437				PAGE_SIZE = 10;
438			}
439			const url = buildUrl({ limit: PAGE_SIZE, offset });
440			const resp = await fetch(url);
441			if (!resp.ok) {
442				console.error("API fetch error", resp.status, resp.statusText);
443				exhausted = true;
444				return;
445			}
446			const json = await resp.json();
447
448			// Update the MR_number element whenever the response includes a count
449			if (json && "undefined" !== typeof json.count) {
450				const countEl = document.getElementById("MR_number");
451				if (countEl) {
452					const totalCount = Number(json.count || 0);
453					countEl.textContent = numberFmt.format(totalCount);
454				}
455			}
456
457			const page = Array.isArray(json.data) ? json.data : [];
458
459			// Push into fetchedItems
460			fetchedItems = fetchedItems.concat(page);
461
462			// If the returned page is smaller than PAGE_SIZE, or we've fetched all according to server count, we've reached the end
463			if (
464				page.length < PAGE_SIZE ||
465				fetchedItems.length >= (json.count || Infinity)
466			) {
467				exhausted = true;
468			}
469
470			// Advance offset for the next fetch (if permitted)
471			offset += PAGE_SIZE;
472
473			// Repopulate filter dropdowns (based on seen data)
474			populateStateFilterOptions();
475
476			// Re-render visible rows using current filters/search
477			renderFilteredRowsDebounced();
478		} catch (err) {
479			console.error("Fetch exception", err);
480			exhausted = true;
481		} finally {
482			isLoading = false;
483			hideInitialLoader();
484			hideAppendLoader();
485
486			// If we've reached the end, stop observing / remove sentinel
487			if (exhausted) {
488				try {
489					teardownInfiniteScroll();
490				} catch (e) {
491					// non-fatal
492				}
493			}
494		}
495	}
496
497	// Rendering a single row (returns tr element)
498	// Replace your existing createRow function with this version.
499	// Handles the normal columns and a special layout when category === 'yoy_volume'.
500	// Assumptions:
501	// - previous total is available as submission.prev_total_volume (or submission.previous_total_volume).
502	// - percent change is available as submission.volume_change_pct or submission.yoy_volume (the code tries both).
503	// - If the percent value is a decimal (e.g. 0.125) it will be multiplied by 100 for display.
504	// - Adjust the field names if your API uses different keys.
505	function createRow(item) {
506		const sub = item.submission || {};
507		const off = item.originator || item.officer || {};
508		const rank = null != item.rank ? item.rank : "";
509		const name = off.loan_originator_name || "";
510		const employer = off.lo_employer;
511		const state = off.branch_state || "";
512		const units = Number(sub.total_count || 0);
513
514		// Determine totalVolume based on explicit category matches
515		let totalVolume = 0;
516		if ("undefined" !== typeof category && "fha_volume" === category) {
517			totalVolume = Number(sub.fha_volume || 0);
518		} else if ("undefined" !== typeof category && "refi_volume" === category) {
519			totalVolume = Number(sub.refi_volume || 0);
520		} else if (
521			"undefined" !== typeof category &&
522			"purchase_volume" === category
523		) {
524			totalVolume = Number(sub.purchase_volume || 0);
525		} else if ("undefined" !== typeof category && "va_volume" === category) {
526			totalVolume = Number(sub.va_volume || 0);
527		} else if ("undefined" !== typeof category && "usda_volume" === category) {
528			totalVolume = Number(sub.usda_volume || 0);
529		} else if ("undefined" !== typeof category && "nonqm_volume" === category) {
530			totalVolume = Number(sub.nonqm_volume || 0);
531		} else if (
532			"undefined" !== typeof category &&
533			"equity_volume" === category
534		) {
535			totalVolume = Number(sub.equity_volume || 0);
536		} else if (
537			"undefined" !== typeof category &&
538			"brokered_volume" === category
539		) {
540			totalVolume = Number(sub.brokered_volume || 0);
541		} else {
542			// default fallback
543			totalVolume = Number(sub.total_volume || 0);
544		}
545
546		const avgLoan = 0 < units ? Math.round(totalVolume / units) : 0;
547		const tr = document.createElement("tr");
548
549		// If this category shows year-over-year columns, render the alternate columns
550		if ("undefined" !== typeof category && "yoy_volume" === category) {
551			// Try a couple of plausible previous-total field names
552			const prevTotalRaw =
553				"undefined" !== typeof sub.prev_total_volume
554					? sub.prev_total_volume
555					: "undefined" !== typeof sub.previous_total_volume
556						? sub.previous_total_volume
557						: 0;
558			const prevTotal = Number(prevTotalRaw || 0);
559
560			const delta = totalVolume - prevTotal;
561
562			// Try plausible percent fields
563			let pctRaw =
564				"undefined" !== typeof sub.volume_change_pct
565					? sub.volume_change_pct
566					: "undefined" !== typeof sub.yoy_volume
567						? sub.yoy_volume
568						: 0;
569			pctRaw = Number(pctRaw || 0);
570
571			// If API returns decimal (0.125) convert to percent; if it returns >1 assume it's already percent.
572			/*
573			const pctForDisplay = ! isNaN( pctRaw ) ?
574				( 1 >= Math.abs( pctRaw ) ? pctRaw * 100 : pctRaw ) :
575				0; */
576
577			const pctForDisplay = pctRaw * 100;
578
579			// Format percent with one decimal
580			const pctDisplay = `${new Intl.NumberFormat("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(pctForDisplay)}%`;
581
582			tr.innerHTML = `
583				<td class="mr-col-number">${escapeHtml(rank)}</td>
584				<td class="mr-col-name">${escapeHtml(name)}</td>
585				<td class="mr-col-employer">${escapeHtml(employer)}</td>
586				<td class="mr-col-state">${escapeHtml(state)}</td>
587				<td class="mr-col-prev-total">${escapeHtml(currencyFmt.format(prevTotal))}</td>
588				<td class="mr-col-total-volume">${escapeHtml(currencyFmt.format(totalVolume))}</td>
589				<td class="mr-col-delta-volume">${escapeHtml(currencyFmt.format(delta))}</td>
590				<td class="mr-col-volume-pct">${escapeHtml(pctDisplay)}</td>
591			`.trim();
592
593			return tr;
594		}
595
596		// Default row layout
597		tr.innerHTML = `
598			<td class="mr-col-number">${escapeHtml(rank)}</td>
599			<td class="mr-col-name">${escapeHtml(name)}</td>
600			<td class="mr-col-employer">${escapeHtml(employer)}</td>
601			<td class="mr-col-state">${escapeHtml(state)}</td>
602			<td class="mr-col-total-volume">${escapeHtml(currencyFmt.format(totalVolume))}</td>
603			<td class="mr-col-units">${escapeHtml(numberFmt.format(units))}</td>
604			<td class="mr-col-avg-loan">${escapeHtml(currencyFmt.format(avgLoan))}</td>
605		`.trim();
606
607		return tr;
608	}
609
610	function createAdRow() {
611		const tr = document.createElement("tr");
612		tr.className = "mr-ad-row relative";
613
614		const td = document.createElement("td");
615		td.setAttribute("colspan", COLSPAN);
616		td.style.backgroundColor = "#EEECEA";
617
618		// Use your tailwind padding class inside the cell
619		const inner = document.createElement("div");
620		inner.className =
621			"mr-ad-row-inner p-4 text-center flex items-center justify-center";
622		inner.style.backgroundColor = "#EEECEA";
623
624		// Build responsive image markup if AD_IMAGES provided, otherwise fallback placeholder
625		if (AD_IMAGES && (AD_IMAGES.small || AD_IMAGES.medium || AD_IMAGES.large)) {
626			// Replace the inner.innerHTML assignment block with this (uses MR_SETTINGS.ad_url as AD_URL)
627			// Replace the inner.innerHTML assignment with this to output one <img> per size inside the <a>.
628			// Default Tailwind visibility classes are provided but you can replace them as you prefer.
629			const adHref = AD_URL || "#";
630			const imgParts = [];
631
632			// small: visible on smallest breakpoints (example classes: visible on sm and below)
633			if (AD_IMAGES.small) {
634				imgParts.push(
635					`<img src="${AD_IMAGES.small}" alt="Ad small" class="ad-img-small block important:md:hidden w-full h-auto" style="max-width: 300px" loading="lazy" />`,
636				);
637			}
638
639			// medium: visible on medium breakpoints (example classes)
640			if (AD_IMAGES.medium) {
641				imgParts.push(
642					`<img src="${AD_IMAGES.medium}" alt="Ad medium" class="ad-img-medium important:hidden important:md:block w-full h-auto" style="max-width: 720px" loading="lazy" />`,
643				);
644			}
645
646			// large: visible on large breakpoints (example classes)
647			if (AD_IMAGES.large) {
648				//imgParts.push( `<img src="${AD_IMAGES.large}" alt="Ad large" class="ad-img-large hidden lg:block w-full h-auto" loading="lazy" style="max-width:720px;" />` );
649			}
650
651			inner.innerHTML = `<a href="${adHref}" class="inline-block" target="_blank" rel="noopener noreferrer">${imgParts.join("")}</a>`;
652		} else {
653			// fallback -- empty box the editor can style or replace later
654			inner.innerHTML = '<div class="w-full h-24 bg-gray-100"></div>';
655		}
656
657		td.appendChild(inner);
658		tr.appendChild(td);
659		return tr;
660	}
661
662	// Simple HTML escape for injected content
663	function escapeHtml(str) {
664		if (null === str || str === undefined) {
665			return "";
666		}
667		return String(str)
668			.replace(/&/g, "&amp;")
669			.replace(/</g, "&lt;")
670			.replace(/>/g, "&gt;");
671	}
672
673	// Filtering logic (client-side)
674	function applyFilters(items) {
675		const q = ((searchInput && searchInput.value) || "").trim().toLowerCase();
676		const stateVal = (stateFilter && stateFilter.value) || "All";
677		const brokerVal = (brokerFilter && brokerFilter.value) || "All";
678		const minVol = Number((minVolumeInput && minVolumeInput.value) || 0);
679		const minUnits = Number((minUnitsInput && minUnitsInput.value) || 0);
680		const employerQ = ((employerSearchInput && employerSearchInput.value) || "")
681			.trim()
682			.toLowerCase();
683
684		return items.filter((item) => {
685			// Prefer originator but fall back to officer for compatibility
686			const off = item.originator || item.officer || {};
687			const sub = item.submission || {};
688
689			// Search by LO name (loan_originator_name) — only if server-side search is disabled
690			if (!SERVER_SIDE_SEARCH) {
691				if (q) {
692					const name = (off.loan_originator_name || "").toLowerCase();
693					if (!name.includes(q)) {
694						return false;
695					}
696				}
697
698				if (employerQ) {
699					const employer = (off.lo_employer || "").toLowerCase();
700					if (!employer.includes(employerQ)) {
701						return false;
702					}
703				}
704			}
705
706			// State filter
707			if (stateVal && "All" !== stateVal) {
708				if ((off.branch_state || "") !== stateVal) {
709					return false;
710				}
711			}
712
713			// Broker/banker filter (map company_institution_type to Broker/Banker/Other)
714			if (brokerVal && "All" !== brokerVal) {
715				const cit = (off.company_institution_type || "").toLowerCase();
716				const inferred = cit.includes("broker")
717					? "Broker"
718					: cit.includes("bank") || cit.includes("mortgage")
719						? "Banker"
720						: "Other";
721				if (inferred !== brokerVal) {
722					return false;
723				}
724			}
725
726			// Min volume (use submission.total_volume)
727			const totalVolume = Number(sub.total_volume || sub.total || 0);
728			if (minVol && totalVolume < minVol) {
729				return false;
730			}
731
732			// Min units (use submission.total_count)
733			const units = Number(sub.total_count || sub.units || 0);
734			if (minUnits && units < minUnits) {
735				return false;
736			}
737
738			return true;
739		});
740	}
741
742	// Render filtered rows (clear table and append)
743	// Replace the existing renderFilteredRows() with this version
744	function renderFilteredRows() {
745		renderRequestId += 1;
746		const thisRenderId = renderRequestId;
747
748		const filtered = applyFilters(fetchedItems);
749
750		// Clear table
751		tableBody.innerHTML = "";
752
753		const fragment = document.createDocumentFragment();
754
755		if (!filtered || 0 === filtered.length) {
756			// If there are no filtered rows, show a single "No results" row.
757			const tr = document.createElement("tr");
758			tr.className = TABLE_ROW_TEMPLATE.className;
759			tr.innerHTML = TABLE_ROW_TEMPLATE.htmlNoResults();
760			fragment.appendChild(tr);
761		} else {
762			// Loop with index so we can insert the ad after the 10th row (after index 9)
763			for (let i = 0; i < filtered.length; i++) {
764				const item = filtered[i];
765				const tr = createRow(item);
766				fragment.appendChild(tr);
767
768				// After the 10th row (i === 9) and only when there are at least 10 rows total,
769				// insert the ad row as the 11th row. Only insert once.
770				if (9 === i && 10 <= filtered.length) {
771					const adTr = createAdRow();
772					fragment.appendChild(adTr);
773				}
774			}
775		}
776
777		tableBody.appendChild(fragment);
778
779		// If another render was requested meanwhile, nothing else to do (next render will replace)
780		if (thisRenderId !== renderRequestId) {
781			return;
782		}
783	}
784
785	// Debounced render to avoid rapid DOM thrash
786	let renderDebounceTimer = null;
787	function renderFilteredRowsDebounced(delay = 200) {
788		if (renderDebounceTimer) {
789			clearTimeout(renderDebounceTimer);
790		}
791		renderDebounceTimer = setTimeout(() => {
792			renderFilteredRows();
793			renderDebounceTimer = null;
794		}, delay);
795	}
796
797	// Populate state filter options based on fetchedItems seen so far
798	function populateStateFilterOptions() {
799		if (!stateFilter) {
800			return;
801		}
802
803		// Gather unique states from fetchedItems, checking originator then officer
804		const states = new Set();
805		fetchedItems.forEach((it) => {
806			const s =
807				(it.originator && it.originator.branch_state) ||
808				(it.officer && it.officer.branch_state) ||
809				"";
810			if (s) {
811				states.add(s);
812			}
813		});
814
815		// Convert to sorted array
816		const stateList = Array.from(states).sort();
817
818		// Save current selection (may be 'All' or a two-letter code)
819		const current = stateFilter.value || "All";
820
821		// Preserve a current non-All selection even if it didn't appear in the newly gathered list
822		if (current && "All" !== current && -1 === stateList.indexOf(current)) {
823			stateList.unshift(current);
824		}
825
826		// Rebuild options: 'All' first
827		stateFilter.innerHTML = "";
828		const allOpt = document.createElement("option");
829		allOpt.value = "All";
830		allOpt.textContent = "All";
831		stateFilter.appendChild(allOpt);
832
833		stateList.forEach((s) => {
834			const opt = document.createElement("option");
835			opt.value = s;
836			opt.textContent = s;
837			stateFilter.appendChild(opt);
838		});
839
840		// restore selection if still present, otherwise default to 'All'
841		if ([...stateFilter.options].some((o) => o.value === current)) {
842			stateFilter.value = current;
843		} else {
844			stateFilter.value = "All";
845		}
846	}
847
848	// Populate broker filter with All / Broker / Banker / Other (static)
849	function populateBrokerFilter() {
850		if (!brokerFilter) {
851			return;
852		}
853		const opts = ["All", "Broker", "Banker", "Other"];
854		brokerFilter.innerHTML = "";
855		opts.forEach((val) => {
856			const o = document.createElement("option");
857			o.value = val;
858			o.textContent = val;
859			brokerFilter.appendChild(o);
860		});
861	}
862
863	// Debounce helper
864	function debounce(fn, wait) {
865		let t = null;
866		return function (...args) {
867			if (t) {
868				clearTimeout(t);
869			}
870			t = setTimeout(() => fn.apply(this, args), wait);
871		};
872	}
873
874	function setupFilters() {
875		searchInput = document.querySelector("#MR_Search") || null;
876		stateFilter = document.querySelector("#MR_stateFilter") || null;
877		brokerFilter = document.querySelector("#MR_brokerFilter") || null;
878		employerSearchInput = document.querySelector("#MR_Search_Employer") || null;
879
880		// Resolve numeric inputs robustly (prefer IDs, fall back to number inputs inside the filters area)
881		const filterGroup =
882			document.querySelector(".flex.flex-wrap.items-end") || document;
883		const numberInputs = Array.from(
884			filterGroup.querySelectorAll('input[type="number"]'),
885		);
886		minVolumeInput =
887			document.getElementById("MR_minVolume") || numberInputs[0] || null;
888		minUnitsInput =
889			document.getElementById("MR_minUnits") ||
890			numberInputs[1] ||
891			numberInputs[0] ||
892			null;
893
894		// replace existing searchInput handler with this
895		if (searchInput) {
896			// keep last value to avoid unnecessary fetches
897			let lastSearchVal = (lastServerSearch || "").toString();
898
899			const debouncedSearch = debounce(() => {
900				const q = (searchInput.value || "").trim();
901				if (q !== lastSearchVal) {
902					lastSearchVal = q;
903					lastServerSearch = q;
904					offset = 0;
905					exhausted = false;
906					fetchedItems = [];
907					tableBody.innerHTML = "";
908					fetchPage();
909				}
910			}, 1000); // 1 second debounce
911
912			// only input/change — no focus/click handlers
913			searchInput.addEventListener("input", debouncedSearch);
914			searchInput.addEventListener("change", () => {
915				const q = (searchInput.value || "").trim();
916				if (q !== lastSearchVal) {
917					lastSearchVal = q;
918					lastServerSearch = q;
919					offset = 0;
920					exhausted = false;
921					fetchedItems = [];
922					tableBody.innerHTML = "";
923					fetchPage();
924				}
925			});
926		}
927
928		if (employerSearchInput) {
929			let lastEmployerSearchVal = (lastServerEmployerSearch || "").toString();
930
931			const debouncedEmployerSearch = debounce(() => {
932				const q = (employerSearchInput.value || "").trim();
933				if (q !== lastEmployerSearchVal) {
934					lastEmployerSearchVal = q;
935					lastServerEmployerSearch = q;
936					offset = 0;
937					exhausted = false;
938					fetchedItems = [];
939					tableBody.innerHTML = "";
940					fetchPage();
941				}
942			}, 1000);
943
944			employerSearchInput.addEventListener("input", debouncedEmployerSearch);
945			employerSearchInput.addEventListener("change", () => {
946				const q = (employerSearchInput.value || "").trim();
947				if (q !== lastEmployerSearchVal) {
948					lastEmployerSearchVal = q;
949					lastServerEmployerSearch = q;
950					offset = 0;
951					exhausted = false;
952					fetchedItems = [];
953					tableBody.innerHTML = "";
954					fetchPage();
955				}
956			});
957		}
958
959		if (stateFilter) {
960			stateFilter.addEventListener("change", () => {
961				// reset paging and fetched cache, then request first page from server
962				offset = 0;
963				exhausted = false;
964				if (Array.isArray(fetchedItems)) {
965					fetchedItems.length = 0;
966				}
967				if (tableBody) {
968					tableBody.innerHTML = "";
969				}
970				fetchPage();
971			});
972		}
973		if (brokerFilter) {
974			brokerFilter.addEventListener("change", () => {
975				renderFilteredRowsDebounced();
976			});
977		}
978		if (minVolumeInput) {
979			minVolumeInput.addEventListener(
980				"input",
981				debounce(() => {
982					renderFilteredRowsDebounced();
983				}, 300),
984			);
985		}
986		if (minUnitsInput) {
987			let lastMinUnitsVal = (minUnitsInput.value || "").toString();
988
989			const debouncedMinUnits = debounce(() => {
990				const v = (minUnitsInput.value || "").trim();
991				if (v !== lastMinUnitsVal) {
992					lastMinUnitsVal = v;
993					offset = 0;
994					exhausted = false;
995					fetchedItems = [];
996					tableBody.innerHTML = "";
997					fetchPage();
998				}
999			}, 1000); // 1 second debounce
1000
1001			minUnitsInput.addEventListener("input", debouncedMinUnits);
1002			minUnitsInput.addEventListener("change", () => {
1003				const v = (minUnitsInput.value || "").trim();
1004				if (v !== lastMinUnitsVal) {
1005					lastMinUnitsVal = v;
1006					offset = 0;
1007					exhausted = false;
1008					fetchedItems = [];
1009					tableBody.innerHTML = "";
1010					fetchPage();
1011				}
1012			});
1013		}
1014	}
1015
1016	// --- START: integrate mr-state dropdown (insert at end of setupFilters(), before the closing brace) ---
1017	(function initMrStateDropdown() {
1018		const HIDDEN_CLASS = "tw-hidden";
1019		const trigger = document.getElementById("mr-state-trigger");
1020		const list = document.getElementById("mr-state-list");
1021		if (!trigger || !list) {
1022			return;
1023		}
1024
1025		// Find or create stateFilter so buildUrl()/fetchPage() will see the value
1026		if (!stateFilter) {
1027			const container =
1028				document.querySelector(".flex.flex-wrap.items-end") || document.body;
1029			const hiddenSelect = document.createElement("select");
1030			hiddenSelect.id = "MR_stateFilter";
1031			hiddenSelect.style.display = "none";
1032			hiddenSelect.value = "All";
1033			container.appendChild(hiddenSelect);
1034			stateFilter = hiddenSelect;
1035		}
1036
1037		const valueSpan = trigger.querySelector(".mr-state-value") || null;
1038
1039		// Toggle dropdown visibility on trigger click
1040		trigger.addEventListener("click", (ev) => {
1041			ev.stopPropagation();
1042			const showing = !list.classList.contains(HIDDEN_CLASS);
1043			if (showing) {
1044				list.classList.add(HIDDEN_CLASS);
1045				trigger.setAttribute("aria-expanded", "false");
1046			} else {
1047				list.classList.remove(HIDDEN_CLASS);
1048				trigger.setAttribute("aria-expanded", "true");
1049
1050				// focus first interactive item
1051				const firstBtn = list.querySelector("button[data-value]");
1052				if (firstBtn) {
1053					firstBtn.focus();
1054				}
1055			}
1056		});
1057
1058		// Close when clicking outside
1059		document.addEventListener("click", (ev) => {
1060			if (!list.classList.contains(HIDDEN_CLASS)) {
1061				if (!trigger.contains(ev.target) && !list.contains(ev.target)) {
1062					list.classList.add(HIDDEN_CLASS);
1063					trigger.setAttribute("aria-expanded", "false");
1064				}
1065			}
1066		});
1067
1068		// Keyboard support: ArrowDown opens, Escape closes
1069		trigger.addEventListener("keydown", (ev) => {
1070			if ("ArrowDown" === ev.key) {
1071				ev.preventDefault();
1072				list.classList.remove(HIDDEN_CLASS);
1073				trigger.setAttribute("aria-expanded", "true");
1074				const firstBtn = list.querySelector("button[data-value]");
1075				if (firstBtn) {
1076					firstBtn.focus();
1077				}
1078			}
1079		});
1080		document.addEventListener("keydown", (ev) => {
1081			if ("Escape" === ev.key && !list.classList.contains(HIDDEN_CLASS)) {
1082				list.classList.add(HIDDEN_CLASS);
1083				trigger.setAttribute("aria-expanded", "false");
1084				trigger.focus();
1085			}
1086		});
1087
1088		// Handle selection clicks
1089		list.addEventListener("click", (ev) => {
1090			const btn = ev.target.closest("button[data-value]");
1091			if (!btn) {
1092				return;
1093			}
1094			ev.stopPropagation();
1095
1096			const value = btn.getAttribute("data-value");
1097			const text = (btn.textContent || "").trim();
1098
1099			// Update visible label
1100			if ("all" === value || !value) {
1101				if (valueSpan) {
1102					valueSpan.textContent = "All States";
1103				}
1104			} else {
1105				if (valueSpan) {
1106					valueSpan.textContent = text;
1107				}
1108			}
1109
1110			// Update stateFilter so buildUrl() picks it up
1111			try {
1112				if (stateFilter) {
1113					if ("all" === value || !value) {
1114						stateFilter.value = "All";
1115					} else {
1116						// ensure option exists (if using a hidden select with no options)
1117						if (![...stateFilter.options].some((o) => o.value === value)) {
1118							const opt = document.createElement("option");
1119							opt.value = value;
1120							opt.textContent = text;
1121							stateFilter.appendChild(opt);
1122						}
1123						stateFilter.value = value;
1124					}
1125
1126					// trigger change event in case anything listens to it
1127					stateFilter.dispatchEvent(new Event("change", { bubbles: true }));
1128				}
1129			} catch (e) {
1130				// non-fatal
1131				console.warn("mr-state: could not update stateFilter", e);
1132			}
1133
1134			// hide dropdown
1135			list.classList.add(HIDDEN_CLASS);
1136			trigger.setAttribute("aria-expanded", "false");
1137
1138			// reset paging and fetch (server-driven count) — use existing fetchPage()
1139			try {
1140				offset = 0;
1141				exhausted = false;
1142				if (Array.isArray(fetchedItems)) {
1143					fetchedItems.length = 0;
1144				}
1145				if (tableBody) {
1146					tableBody.innerHTML = "";
1147				}
1148				if ("function" === typeof fetchPage) {
1149					fetchPage();
1150				} else {
1151					// fallback: attempt to call renderFilteredRowsDebounced if fetchPage is not present
1152					if ("function" === typeof renderFilteredRowsDebounced) {
1153						renderFilteredRowsDebounced();
1154					}
1155				}
1156			} catch (err) {
1157				console.error("mr-state: fetch trigger failed", err);
1158			}
1159		});
1160	})();
1161
1162	// --- END: integrate mr-state dropdown ---
1163
1164	// Infinite scroll handler
1165	function onScroll() {
1166		if (isLoading || exhausted) {
1167			return;
1168		}
1169		const scrolledToBottom =
1170			window.innerHeight + window.scrollY >=
1171			document.documentElement.scrollHeight - SCROLL_THRESHOLD_PX;
1172		if (!scrolledToBottom) {
1173			return;
1174		}
1175
1176		fetchPage();
1177	}
1178
1179	function setupInfiniteScroll() {
1180		// create or locate a small sentinel element placed after the table/wrapper
1181		const tableEl = tableBody.closest("table") || tableBody;
1182		const wrap = tableEl
1183			? tableEl.closest(".mr-table-wrap") || tableEl.parentElement
1184			: document.body;
1185
1186		let sentinel = document.getElementById("MR_scroll-sentinel");
1187		if (!sentinel) {
1188			sentinel = document.createElement("div");
1189			sentinel.id = "MR_scroll-sentinel";
1190			sentinel.style.width = "1px";
1191			sentinel.style.height = "1px";
1192			sentinel.style.visibility = "hidden";
1193			if (wrap && wrap.parentNode) {
1194				if (wrap.nextSibling) {
1195					wrap.parentNode.insertBefore(sentinel, wrap.nextSibling);
1196				} else {
1197					wrap.parentNode.appendChild(sentinel);
1198				}
1199			} else {
1200				document.body.appendChild(sentinel);
1201			}
1202		}
1203
1204		// Use IntersectionObserver when available (more reliable on mobile)
1205		if ("IntersectionObserver" in window) {
1206			// rootMargin preloads earlier; adjust value if you want earlier/later loads
1207			infiniteObserver = new IntersectionObserver(
1208				(entries) => {
1209					entries.forEach((entry) => {
1210						if (entry.isIntersecting && !isLoading && !exhausted) {
1211							// slight debounce to avoid double-invocation on some browsers
1212							setTimeout(() => {
1213								if (!isLoading && !exhausted) {
1214									fetchPage();
1215								}
1216							}, 100);
1217						}
1218					});
1219				},
1220				{ root: null, rootMargin: "200px 0px", threshold: 0 },
1221			);
1222
1223			infiniteObserver.observe(sentinel);
1224		} else {
1225			// Fallback: attach scroll listener (keep existing onScroll)
1226			window.addEventListener("scroll", onScroll, { passive: true });
1227		}
1228	}
1229
1230	function teardownInfiniteScroll() {
1231		const sentinel = document.getElementById("MR_scroll-sentinel");
1232		if (infiniteObserver) {
1233			infiniteObserver.disconnect();
1234			infiniteObserver = null;
1235		} else {
1236			window.removeEventListener("scroll", onScroll, { passive: true });
1237		}
1238		if (sentinel && sentinel.parentNode) {
1239			sentinel.parentNode.removeChild(sentinel);
1240		}
1241	}
1242
1243	// Initialization
1244	function init() {
1245		// Populate static broker options
1246		populateBrokerFilter();
1247
1248		// Setup filters
1249		setupFilters();
1250
1251		// If there's no category provided, do not call the API — show "No results" in-table
1252		if (!category) {
1253			fetchedItems = [];
1254
1255			// renderFilteredRows will produce the single "No results" row when fetchedItems is empty
1256			renderFilteredRows();
1257			return;
1258		}
1259
1260		// Otherwise fetch the first page and enable infinite scroll
1261		fetchPage();
1262		setupInfiniteScroll();
1263	}
1264
1265	// Start when DOM is ready
1266	if ("loading" === document.readyState) {
1267		window.addEventListener("DOMContentLoaded", init);
1268	} else {
1269		init();
1270	}
1271})();
1272
1273document.addEventListener("DOMContentLoaded", () => {
1274	const toggle = document.getElementById("MR_toggle_sidebar");
1275	const sidebar = document.getElementById("MR_sidebar_inner");
1276	if (!toggle || !sidebar) {
1277		return;
1278	}
1279
1280	const svg = toggle.querySelector("svg");
1281
1282	toggle.addEventListener("click", (e) => {
1283		e.preventDefault();
1284		sidebar.classList.toggle("tw-hidden");
1285		if (svg) {
1286			svg.classList.toggle("rotate-180");
1287		}
1288	});
1289});
1290;
1291!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}
1291),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}({11:function(e,t,n){"use strict";n.r(t);n(12);"undefined"!=typeof tp&&(tp=window.tp||[],tp.push(["init",function(){if(!0===tp.pianoId.isUserValid()){var e=tp.pianoId.getUser();!function(e,t){if(null!==document.getElementById("comments")){var n=document.getElementById("author"),r=document.getElementById("email");n.value=e,r.value=t}}((e.firstName?e.firstName:"")+" "+(e.lastName?e.lastName:""),e.email)}}]))},12:function(e,t,n){}});;
1292!function(e){var t={};function i(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,i),o.l=!0,o.exports}i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)i.d(n,o,function(t){return e[t]}.bind(null,o));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=26)}({26:function(e,t,i){"use strict";i.r(t),{init:function(){this.window=$(window),this.videoWrap=$(".responsive-embed.widescreen"),this.video=$(".container__vid"),this.player=$(".jwplayer"),this.videoHeight=this.video.innerHeight(),this.bindEvents()},bindEvents:function(){var e=this;this.window.on("scroll",function(t){return e.initFloatingVideo(t)})},initFloatingVideo:function(){this.window.scrollTop()>this.videoHeight+this.videoWrap.offset().top?this.video.addClass("is-stuck"):this.video.removeClass("is-stuck")}}.init()}});;
1293(function ($) {
1294    $(document).on('fusion-builder-settings-render', function () {
1295        $('select[name="id"]').on('change', function () {
1296            let selectedTableID = $(this).val();
1297
1298            if (selectedTableID) {
1299                $.ajax({
1300                    url: wdt_ajax_object.ajaxurl,
1301                    type: 'POST',
1302                    data: {
1303                        action: 'get_table_type_by_id',
1304                        table_id: selectedTableID
1305                    },
1306                    success: function (response) {
1307                        var tableType = JSON.parse(response).tableType;
1308
1309                        if (tableType === 'woo_commerce') {
1310                            $('select[name="table_view"]').closest('.fusion-builder-option').hide();
1311                        } else {
1312                            $('select[name="table_view"]').closest('.fusion-builder-option').show();
1313                        }
1314                    }
1315                });
1316            }
1317        });
1318    });
1319
1320    $(document).on('change', '.wpdatatable select[name="id"]', function () {
1321        let selectedTableID = $(this).val();
1322
1323        if (selectedTableID) {
1324            $.ajax({
1325                url: wdt_ajax_object.ajaxurl,
1326                type: 'POST',
1327                data: {
1328                    action: 'get_table_type_by_id',
1329                    table_id: selectedTableID
1330                },
1331                success: function (response) {
1332                    var tableType = JSON.parse(response).tableType;
1333
1334                    if (tableType === 'woo_commerce') {
1335                        $('li[data-option-id="table_view"]').hide();
1336                    } else {
1337                        $('li[data-option-id="table_view"]').hide();
1338                    }
1339                }
1340            });
1341        }
1342    });
1343    $(document).on('change', 'ul[data-element="wpdatatable"] input#id', function () {
1344        let selectedTableID = $(this).val();
1345
1346        if (selectedTableID) {
1347            $.ajax({
1348                url: wdt_ajax_object.ajaxurl,
1349                type: 'POST',
1350                data: {
1351                    action: 'get_table_type_by_id',
1352                    table_id: selectedTableID
1353                },
1354                success: function (response) {
1355                    var tableType = JSON.parse(response).tableType;
1356
1357                    if (tableType === 'woo_commerce') {
1358                        $('li[data-option-id="table_view"]').hide();
1359                    } else {
1360                        $('li[data-option-id="table_view"]').hide();
1361                    }
1362                }
1363            });
1364        }
1365    });
1366})(jQuery);;
1367$( document ).ready( function () {
1368	var $temp = $( '<input>' );
1369	var $url = $( location ).attr( 'href' );
1370
1371	$( '.sd-content' ).append( '<div class="link-icon-text">Link Copied</div>' );
1372
1373	$( document ).on( 'click', '.share-link_copy a', function( event ) {
1374		event.preventDefault();
1375
1376		$( 'body' ).append( $temp );
1377		$temp.val( $url ).select();
1378		document.execCommand( 'copy' );
1379		$temp.remove();
1380
1381		$( '.link-icon-text' ).show();
1382
1383		setTimeout( function() {
1384			$( '.link-icon-text' ).hide();
1385		}, 3000 );
1386	} );
1387} );
1388;

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.