PageSourceSearch

https://pwcf.org/wp-content/plugins/wpfilters/assets/js/frontend/render.js?ver=1.1.1

js pwcf.org collected 2026-10-02 06:28:21 UTC 12,514 bytes, 404 lines download raw bytes

1/* global _WPFILTERS */
2
3( function($) {
4
5	'use strict';
6
7	// Fetches filtered markup and replaces result/pagination regions in place.
8	const app = {
9
10		/**
11		 * Init.
12		 *
13		 * @since 0.0.1
14		 */
15		init: () => {
16
17			$( app.ready );
18		},
19
20		/**
21		 * Document ready
22		 *
23		 * @since 0.0.1
24		 */
25		ready: () => {
26
27			// Shared request payload consumed by frontend elements.js.
28			window.WPFILTERS_HTTP = {
29				"url_vars": {}
30			};
31
32			let loop = jQuery( '.wpfilters-results' );
33
34			// Inside a SearchWP template, the .wpfilters-results wrapper is
35			// only present when the "Enable WPFilters" setting is on.
36			// When absent, skip all fallback detection (loop + pagination).
37			const isSearchWPTemplate = !! document.querySelector( '.swp-rp-main' );
38			const wpfiltersDisabled  = isSearchWPTemplate && ! loop.length;
39
40			if ( ! loop.length && ! wpfiltersDisabled ) {
41				const has_loop = app.detectLoop(document.body);
42
43				if (has_loop) {
44					app.addClass('wpfilters-results', app.constructor(has_loop));
45				}
46			}
47
48			if ( ! wpfiltersDisabled ) {
49				const has_pagination = app.detectPagination(document.body);
50
51				if (has_pagination) {
52					app.addClass('wpfilters-pagination', app.constructor(has_pagination));
53				}
54			}
55
56		},
57
58		sendRequest: () => {
59
60			// Visual loading state for common loop containers.
61			const $resultsTarget = jQuery('.wpfilters-results');
62
63			$resultsTarget.css( 'opacity', 0.5 );
64
65			// Request server-rendered HTML based on current normalized filter params.
66			$.post( document.URL, JSON.stringify({
67				data: {
68					'http_params': WPFILTERS_HTTP,
69				},
70				action: _WPFILTERS.prefix + 'request'
71			}), function (response){
72
73				try {
74					const json = JSON.parse(response);
75					if (!json || typeof json.template !== 'string') {
76						return;
77					}
78
79					// Rebuild response as a DOM fragment and extract known targets.
80					const obj = app.constructor(json.template);
81					let $tpl = app.find('.wpfilters-results', obj);
82
83					// Preserve legacy marker support for non-SearchWP contexts.
84					if ($tpl.nodes && $tpl.nodes.length < 1 && obj.nodes && obj.nodes.length > 0) {
85						const loop = app.detectLoop(obj.nodes[0]);
86
87						if (loop) {
88							$tpl = app.addClass('wpfilters-results', app.constructor(loop));
89						}
90					}
91
92					if ($tpl.nodes && $tpl.nodes.length > 0) {
93						const inject = $tpl.nodes[0].innerHTML;
94
95						if (inject) {
96							jQuery('.wpfilters-results').html(inject);
97
98						}
99					}
100
101					const $currentPagination = jQuery('.wpfilters-pagination');
102					let $pagination = app.find('.wpfilters-pagination', obj);
103
104					// Preserve legacy marker support for non-SearchWP contexts.
105					if ($pagination.nodes && $pagination.nodes.length < 1 && obj.nodes && obj.nodes.length > 0) {
106						const pagination = app.detectPagination(obj.nodes[0]);
107
108						if (pagination) {
109							$pagination = app.addClass('wpfilters-pagination', app.constructor(pagination));
110						}
111					}
112
113					let paginationInject = null;
114					if ($pagination.nodes && $pagination.nodes.length > 0) {
115						paginationInject = $pagination.nodes[0].innerHTML;
116					}
117
118					// Missing pagination in response is a valid state.
119					if (!paginationInject && $currentPagination.length) {
120						$currentPagination.empty();
121					} else if (paginationInject && $currentPagination.length) {
122						$currentPagination.html(paginationInject);
123					} else if (paginationInject && !$currentPagination.length) {
124						// Pagination exists in response but not in DOM yet.
125						// Only inject a separate wrapper if the pagination is outside
126						// the results container in the response. When it's inside,
127						// it was already injected as part of the results HTML above.
128						if (!app.isPaginationInsideResults(obj)) {
129							const $resultsEl = jQuery('.wpfilters-results');
130							if ($resultsEl.length) {
131								const paginationWrapper = document.createElement('div');
132								paginationWrapper.className = 'wpfilters-pagination';
133								paginationWrapper.innerHTML = paginationInject;
134
135								if (app.isPaginationBeforeResults(obj)) {
136									$resultsEl.before(paginationWrapper);
137								} else {
138									$resultsEl.after(paginationWrapper);
139								}
140							}
141						}
142					}
143
144					const $resultCount = app.find('.woocommerce-result-count', obj);
145					if ($resultCount.nodes && $resultCount.nodes.length > 0 && jQuery('.woocommerce-result-count').length > 0) {
146						jQuery('.woocommerce-result-count').replaceWith($resultCount.nodes[0].cloneNode(true));
147					}
148
149					// Replace the SearchWP Template page header so the H1
150					// ("Search results for: …") and the embedded SearchWP
151					// search-form input reflect the new keyword on every
152					// WPFilters refresh. The header is rendered OUTSIDE
153					// `.wpfilters-results` (see
154					// `searchwp/includes/Templates/Frontend.php` `render_page_header()`),
155					// so it isn't picked up by the inner-content replacement
156					// above. We use `outerHTML` + `replaceWith` so any
157					// attributes on the wrapper (`class`, `role`, etc.)
158					// from the server-rendered response also update.
159					const $newPageHeader = app.find('.swp-rp-page-header', obj);
160					const $currentPageHeader = jQuery('.swp-rp-page-header');
161					if (
162						$newPageHeader.nodes &&
163						$newPageHeader.nodes.length > 0 &&
164						$currentPageHeader.length > 0
165					) {
166						$currentPageHeader.replaceWith($newPageHeader.nodes[0].outerHTML);
167					}
168
169					// Refresh on-page filter elements with the new filter
170					// HTML (option counts, available options) shipped by
171					// the server in `json.filter_data`. Filter elements
172					// typically live OUTSIDE `.wpfilters-results` (in
173					// sidebars or as standalone widgets), so the
174					// inner-content replace above doesn't reach them.
175					// Without this loop, filter option counts stay at
176					// their page-load values forever — most visibly
177					// after clicking Reset, where users expect counts to
178					// revert to unfiltered values.
179					if (json.filter_data && typeof json.filter_data === 'object') {
180						Object.keys(json.filter_data).forEach(function(elementId) {
181							const id = parseInt(elementId, 10);
182							const html = json.filter_data[elementId];
183							if (!html) {
184								return;
185							}
186
187							// Match both already-injected elements (with
188							// data-element-id preserved by `injectFilterHtml`)
189							// AND any fresh placeholders that may have just
190							// landed inside `.wpfilters-results` via the
191							// inner-content replace above.
192							const $current = jQuery(
193								'.wpfilters-element[data-element-id="' + id + '"], ' +
194								'.wpfilters-element-placeholder[data-element-id="' + id + '"]'
195							);
196							if ($current.length === 0) {
197								return;
198							}
199
200							// Don't disrupt the element the user is
201							// actively TYPING in — the Search element's
202							// text input or any similar text-bearing
203							// control. Replacing would steal focus and
204							// lose the caret.
205							//
206							// Narrowly scoped to text inputs because the
207							// previous broader check ("any focused
208							// element inside this filter") also matched
209							// just-clicked checkboxes/radios. After
210							// un-checking a filter value, the checkbox
211							// still holds focus when the AJAX response
212							// arrives — skipping replacement then left
213							// stale option counts on the page (the
214							// freshly-computed counts under the new
215							// un-narrowed scope never landed). For
216							// checkbox/radio/select, focus loss after
217							// replacement is fine because the user
218							// isn't typing.
219							const active = document.activeElement;
220							if (
221								active &&
222								(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') &&
223								(active.type === 'search' || active.type === 'text' || active.type === 'email' || active.type === 'url' || active.tagName === 'TEXTAREA') &&
224								$current.get(0) === active.closest('.wpfilters-element, .wpfilters-element-placeholder')
225							) {
226								return;
227							}
228
229							const $new = jQuery(html);
230							if ($new.length === 0) {
231								return;
232							}
233							$new.first().attr('data-element-id', String(id));
234							$current.replaceWith($new);
235						});
236
237						// Keep the in-memory cache fresh for any code
238						// path that reads `_WPFILTERS.filterData` later
239						// (e.g. a second refresh, or a future feature).
240						if (typeof window._WPFILTERS !== 'undefined') {
241							window._WPFILTERS.filterData = Object.assign(
242								{},
243								window._WPFILTERS.filterData || {},
244								json.filter_data
245							);
246						}
247
248						// Tell elements.js to re-run per-element
249						// enhancements (multi-select chips, show-more
250						// toggle) against the fresh DOM.
251						jQuery(document).trigger('wpfilters:elementsRefreshed');
252					}
253				}
254				catch(e) {
255					console.error(e);
256				}
257				finally {
258					$resultsTarget.css( 'opacity', 1 );
259				}
260			}, 'text');
261		},
262
263		detectLoop: (node) => {
264			return app.detectComment(node, 'wpfilters-loop');
265		},
266
267		detectPagination: (node) => {
268			return app.detectComment(node, 'wpfilters-pagination');
269		},
270
271		detectComment: (node, nodeValue) => {
272			// Finds sections wrapped by marker comments in rendered HTML.
273			const parents = [];
274			let curNode = null;
275			const iterator = document.createNodeIterator(node, NodeFilter.SHOW_COMMENT, () => {
276				return NodeFilter.FILTER_ACCEPT; /* IE expects a function */
277			}, false);
278
279			while (curNode = iterator.nextNode()) {
280				if (curNode.nodeType === 8 && curNode.nodeValue === nodeValue) {
281					parents.push(curNode.parentNode);
282				}
283			}
284
285			return parents.length > 0 ? parents : false;
286		},
287
288		constructor: (selector) => {
289			// Lightweight wrapper that normalizes selectors, nodes, arrays, and HTML strings.
290
291			const obj = {
292				nodes: [],
293			}
294
295			if (typeof selector === 'string' || selector instanceof String) { // string
296				var selector = selector.replace(':selected', ':checked');
297
298				if ('' === selector) {
299					obj.nodes = [];
300				}
301				else if (app.isValidSelector(selector)) {
302					obj.nodes = Array.from(document.querySelectorAll(selector));
303				}
304				else {
305					var tpl = document.createElement('template');
306					tpl.innerHTML = selector;
307					obj.nodes = [tpl.content];
308				}
309			}
310			else if (Array.isArray(selector)) { // array of nodes
311				obj.nodes = selector;
312			}
313			else if (typeof selector === 'object' && selector.nodeName) { // node
314				obj.nodes = [selector];
315			}
316			else if (selector === window) { // window
317				obj.nodes = [window];
318			}
319			else { // document
320				obj.nodes = [document];
321			}
322
323			return obj;
324		},
325
326		isValidSelector: (string) => {
327			try {
328				document.createDocumentFragment().querySelector(string);
329			}
330			catch(err) {
331				return false;
332			}
333			return true;
334		},
335
336		find: (selector, obj) => {
337			// Query utility scoped to a wrapped object graph.
338			var selector = selector.replace(':selected', ':checked');
339			let nodes = [];
340			let clone = app.clone(obj.nodes);
341
342			clone.nodes.forEach(node => {
343				nodes = nodes.concat(Array.from(node.querySelectorAll(selector)));
344			});
345
346			clone.nodes = nodes;
347			return clone;
348		},
349
350		clone: (nodes) => {
351			return app.constructor(nodes);
352		},
353
354		addClass: (className, obj) => {
355			obj.nodes.forEach(node => node.classList.add(className));
356			return obj;
357		},
358
359		isPaginationInsideResults: (obj) => {
360			// Check if the pagination element is a descendant of the results element
361			// in the response DOM.
362			if (!obj.nodes || !obj.nodes.length) {
363				return false;
364			}
365
366			const root = obj.nodes[0];
367			const results = root.querySelector('.wpfilters-results');
368			const pagination = root.querySelector('.wpfilters-pagination');
369
370			if (!results || !pagination) {
371				return false;
372			}
373
374			return results.contains(pagination);
375		},
376
377		isPaginationBeforeResults: (obj) => {
378			// Walk the response DOM to determine if pagination appears before results.
379			if (!obj.nodes || !obj.nodes.length) {
380				return false;
381			}
382
383			const root = obj.nodes[0];
384			const results = root.querySelector('.wpfilters-results');
385			const pagination = root.querySelector('.wpfilters-pagination');
386
387			if (!results || !pagination) {
388				return false;
389			}
390
391			// compareDocumentPosition bit 4 (DOCUMENT_POSITION_FOLLOWING) means
392			// pagination comes before results in document order.
393			return !!(pagination.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING);
394		},
395
396	};
397
398	app.init();
399
400	window.wpfilters = window.wpfilters || {};
401
402	window.wpfilters.Render = app;
403
404}( 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.