PageSourceSearch

https://thetriumphofdavid.com/themes/bootstrap5/js/search.js?_=1777897953

js thetriumphofdavid.com collected 2026-10-08 19:45:13 UTC 13,056 bytes, 389 lines download raw bytes

1/*global VuFind */
2
3VuFind.register('search', function search() {
4  let jsRecordListSelector = '.js-result-list';
5  let paginationLinksSelector = '.js-pagination a,.js-pagination-simple a';
6  let scrollElementSelector = '.search-stats';
7  let searchStatsSelector = '.js-search-stats';
8  let searchFormSelector = 'form.searchForm';
9  let resultsControlFormSelector = '.search-controls form';
10  let sortFormSelector = resultsControlFormSelector + '.search-sort';
11  let limitFormSelector = resultsControlFormSelector + '.search-result-limit';
12  let viewTypeSelector = '.view-buttons a';
13  let selectAllSelector = '.checkbox-select-all';
14
15  // Forward declaration
16  let loadResults = function loadResultsForward() {};
17
18  /**
19   * Get the URL without any parameters
20   * @param {string} url URL to get base from
21   * @returns {string} Base URL without query parameters
22   */
23  function getBaseUrl(url) {
24    const parts = url.split('?');
25    return parts[0];
26  }
27
28  /**
29   * Initialize pagination.
30   */
31  function initPagination() {
32    document.querySelectorAll(paginationLinksSelector).forEach((element) => {
33      if (!element.dataset.ajaxPagination) {
34        element.dataset.ajaxPagination = true;
35        element.addEventListener('click', function handleClick(event) {
36          event.preventDefault();
37          const href = this.getAttribute('href');
38          loadResults(href, true);
39        });
40      }
41    });
42  }
43
44  /**
45   * Initialize result controls that are not refreshed via AJAX.
46   *
47   * Note that view type links are updated in updateResultControls, but using them
48   * will cause a reload since page contents may change.
49   */
50  function initResultControls() {
51    document.querySelectorAll(resultsControlFormSelector).forEach((form) => {
52      if (!form.dataset.ajaxPagination) {
53        form.dataset.ajaxPagination = true;
54        form.querySelectorAll('.jumpMenu').forEach(jumpMenu => {
55          // Disable original jump menu function:
56          jumpMenu.classList.remove('jumpMenu');
57          jumpMenu.addEventListener('change', function handleSubmit(event) {
58            event.preventDefault();
59            // Build a URL from form action and fields and load results:
60            let urlParts = form.getAttribute('action').split('?', 2);
61            const query = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : '');
62
63            /**
64             * Add data from an element to query
65             * @param {HTMLElement|RadioNodeList} el Element or RadioNodeList to add values from
66             */
67            function _addToQuery(el) {
68              if ('radio' === el.type && !el.checked) {
69                return;
70              }
71              if (el.name.endsWith('[]')) {
72                query.append(el.name, el.value);
73              } else {
74                query.set(el.name, el.value);
75              }
76            }
77
78            Object.entries(form.elements).forEach(([, element]) => {
79              // Chrome represents multiple hidden 'filter[]' fields as a RadioNodeList:
80              if (element instanceof RadioNodeList) {
81                Object.entries(element).forEach(([, setElement]) => {
82                  _addToQuery(setElement);
83                });
84              } else {
85                _addToQuery(element);
86              }
87            });
88            // Remove page so that any change resets it:
89            query.delete('page');
90            const url = urlParts[0] + '?' + query.toString();
91            loadResults(url, true);
92          });
93        });
94      }
95    });
96  }
97
98  /**
99   * Prepend a hidden field to a form.
100   * @param {?Element} form Form to prepend element into or null
101   * @param {string} name Name for the input element
102   * @param {string} value Value for the input element
103   */
104  function prependHiddenField(form, name, value) {
105    if (!form) {
106      return;
107    }
108    const input = document.createElement('input');
109    input.type = "hidden";
110    input.name = name;
111    input.value = value;
112    form.prepend(input);
113  }
114
115  /**
116   * Handle a hidden field.
117   *
118   * Adds, updates or removes the field as necessary.
119   * @param {string} formSelector Selector for searching the form
120   * @param {string} fieldName Name of the field to search from the form
121   * @param {?string} value Value to set or null to remove the field
122   */
123  function handleHiddenField(formSelector, fieldName, value) {
124    let form = document.querySelector(formSelector);
125    if (!form) {
126      return;
127    }
128    let field = form.querySelector("input[name=" + fieldName + "]");
129    if (field) {
130      if (value) {
131        field.value = value;
132      } else {
133        field.remove();
134      }
135    } else if (value) {
136      prependHiddenField(form, fieldName, value);
137    }
138  }
139
140  /**
141   * Update value of a select field
142   * @param {?Element} select Select element to update
143   * @param {?string} value Value to set
144   */
145  function updateSelectValue(select, value) {
146    if (!select) {
147      return;
148    }
149    if (select.value !== value) {
150      if (value) {
151        select.value = value;
152      } else {
153        const defaultValue = select.querySelector('option[data-default]');
154        if (defaultValue) {
155          select.value = defaultValue.value;
156        }
157      }
158    }
159  }
160
161  /**
162   * Update URLs of result controls (sort, limit, view type)
163   *
164   * We will deliberately avoid replacing the controls for accessibility, so we need
165   * to ensure that they contain current URL parameters.
166   * @param {string} pageUrl Current page URL to use for updating sort and limit controls
167   */
168  function updateResultControls(pageUrl) {
169    const parts = pageUrl.split('?', 2);
170    const params = new URLSearchParams(parts.length > 1 ? parts[1] : '');
171    const sort = params.get('sort');
172    const limit = params.get('limit');
173
174    // Update hidden fields of the search form:
175    handleHiddenField(searchFormSelector, 'limit', limit);
176    handleHiddenField(searchFormSelector, 'sort', sort);
177
178    // Update hidden fields of search control forms:
179    handleHiddenField(sortFormSelector, 'limit', limit);
180    handleHiddenField(limitFormSelector, 'sort', sort);
181
182    // Update currently selected values (required for history traversal to show correct values):
183    updateSelectValue(document.querySelector(sortFormSelector + ' select'), sort);
184    updateSelectValue(document.querySelector(limitFormSelector + ' select'), limit);
185
186    // Update view type links:
187    document.querySelectorAll(viewTypeSelector).forEach((element) => {
188      const url = element.getAttribute('href');
189      const urlParts = url.split('?', 2);
190      const urlParams = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : '');
191      if (sort) {
192        urlParams.set('sort', sort);
193      } else {
194        urlParams.delete('sort');
195      }
196      if (limit) {
197        urlParams.set('limit', limit);
198      } else {
199        urlParams.delete('limit');
200      }
201      element.setAttribute('href', urlParts[0] + '?' + urlParams.toString());
202    });
203
204    // Reset "select all" checkbox:
205    document.querySelectorAll(selectAllSelector).forEach((el) => el.checked = false);
206  }
207
208  /**
209   * Update URLs of links pointing to the same results page
210   *
211   * Updates links pointing to this page to ensure that they contain current URL
212   * parameters (e.g. sort and limit). Any link that only contains query parameters
213   * is considered.
214   * @param {string} pageUrl Current page URL to use for updating links
215   */
216  function updateResultLinks(pageUrl) {
217    let urlParts = pageUrl.split('?', 2);
218    const params = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : '');
219    const sort = params.get('sort');
220    const limit = params.get('limit');
221
222    document.querySelectorAll('a').forEach((a) => {
223      // Use original href attribute since a.href returns an absolute URL:
224      const href = a.getAttribute('href');
225      if (!href || !href.startsWith('?')) {
226        return true;
227      }
228      const hrefParams = new URLSearchParams(href.substr(1));
229      if (null === sort) {
230        hrefParams.delete('sort');
231      } else {
232        hrefParams.set('sort', sort);
233      }
234      if (null === limit) {
235        hrefParams.delete('limit');
236      } else {
237        hrefParams.set('limit', limit);
238      }
239      a.href = '?' + hrefParams.toString();
240    });
241  }
242
243  /**
244   * Scroll view port to results
245   * @param {string} _style Scroll behavior ('smooth' (default), 'instant' or 'auto')
246   */
247  function scrollToResults(_style) {
248    const scrollEl = document.querySelector(scrollElementSelector);
249    if (scrollEl && window.scrollY > scrollEl.offsetTop) {
250      const style = typeof _style !== 'undefined' ? _style : 'smooth';
251      scrollEl.scrollIntoView({behavior: style});
252    }
253  }
254
255  /**
256   * Show an error message
257   * @param {string} error Error message to show
258   */
259  function showError(error) {
260    let errorMsg = document.createElement('div');
261    errorMsg.classList = 'alert alert-danger';
262    errorMsg.textContent = error;
263    const recordList = document.querySelector(jsRecordListSelector);
264    recordList.replaceChildren(errorMsg);
265  }
266
267  /**
268   * Load results and update associated elements.
269   * @param {string} pageUrl Current page URL to load results
270   * @param {boolean} addToHistory Add current search into history
271   */
272  loadResults = function loadResultsReal(pageUrl, addToHistory) {
273    const recordList = document.querySelector(jsRecordListSelector);
274    const backend = recordList.dataset.backend;
275    if (typeof backend === 'undefined') {
276      showError('ERROR: data-backend not set for record list');
277      return;
278    }
279    if (recordList.classList.contains('loading')) {
280      return;
281    }
282    recordList.classList.add('loading');
283    const history = recordList.dataset.history;
284
285    recordList.prepend(VuFind.loadingOverlay());
286    scrollToResults();
287    const searchStats = document.querySelector(searchStatsSelector);
288    const statsKey = searchStats.dataset.key;
289
290    const queryParams = new URLSearchParams('method=getSearchResults');
291    queryParams.set('source', backend);
292    if (typeof history !== 'undefined') {
293      queryParams.set('history', history);
294    }
295    queryParams.set('statsKey', statsKey);
296    let pageUrlParts = pageUrl.split('?');
297    if (typeof pageUrlParts[1] !== 'undefined') {
298      queryParams.set('querystring', pageUrlParts[1]);
299      if (addToHistory) {
300        window.history.pushState(
301          {
302            url: getBaseUrl(window.location.href) + '?' + pageUrlParts[1]
303          },
304          '',
305          '?' + pageUrlParts[1]
306        );
307      }
308    }
309    updateResultControls(pageUrl);
310    updateResultLinks(pageUrl);
311
312    VuFind.emit('results-load', {
313      url: pageUrl,
314      addToHistory: addToHistory
315    });
316
317    fetch(VuFind.path + '/AJAX/JSON?' + queryParams.toString())
318      .then((response) => {
319        response.json()
320          .then((result) => {
321            if (result.error) {
322              throw result.error;
323            }
324            if (!response.ok) {
325              throw result.data;
326            }
327            // We expect to get the results list in elements, but reset it to hide spinner just in case:
328            recordList.textContent = '';
329            Object.entries(result.data.elements).forEach(([elementSelector, contents]) => {
330              document.querySelectorAll(elementSelector).forEach((element) => {
331                VuFind.setElementContents(
332                  element,
333                  contents.content,
334                  contents.attrs,
335                  contents.target ? (contents.target + 'HTML') : ''
336                );
337              });
338            });
339            VuFind.initResultScripts(jsRecordListSelector);
340            initPagination();
341
342            VuFind.emit('results-loaded', {
343              url: pageUrl,
344              addToHistory: addToHistory,
345              data: result
346            });
347
348            recordList.classList.remove('loading');
349          })
350          .catch((error) => {
351            // This error message is from the server, so no need to prefix it
352            showError(error);
353            recordList.classList.remove('loading');
354          });
355      })
356      .catch((error) => {
357        showError(VuFind.translate('error_occurred') + ' - ' + error);
358        recordList.classList.remove('loading');
359      });
360  };
361
362  /**
363   * Handle history state change event and load results accordingly.
364   * @param {Event} event Event to use for loading results
365   */
366  function historyStateListener(event) {
367    if (event.state && event.state.url && getBaseUrl(window.location.href) === getBaseUrl(event.state.url)) {
368      event.preventDefault();
369      loadResults(event.state.url, false);
370    }
371  }
372
373  /**
374   * Initialize JS pagination if enabled
375   */
376  function init() {
377    if (document.querySelector(jsRecordListSelector)) {
378      initPagination();
379      initResultControls();
380      window.history.replaceState({url: window.location.href}, '', window.location.href);
381      window.addEventListener('popstate', historyStateListener);
382    }
383  }
384
385  return {
386    init: init,
387    scrollToResults
388  };
389});

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.