PageSourceSearch

https://www.lowyat.net/wp-content/plugins/scry-search/features/window/assets/js/window.js?ver=1.2.1-lmg.6

js lowyat.net collected 2026-10-02 02:54:08 UTC 10,125 bytes, 262 lines download raw bytes

1//create the scry search window object
2window.scrySearch = {
3    version: '1.0.0',   //version of the window object
4    searchForms: [],    //collection of search forms on the page
5    upgrades: {},       //array of upgrades that have been registered for custom functionality
6    //initialize the window object
7    init: function () {
8        this.searchForms = document.querySelectorAll('form');
9
10        //first, get all forms on the page
11        var candidateFormElements = Array.from(document.querySelectorAll('form'));
12
13        //get search forms
14        var searchFormElements = candidateFormElements.filter(function (form) {
15            if (!form) return false;
16
17            var role = form.getAttribute('role');
18            if (role && role.toLowerCase() === 'search') {
19                return true;
20            }
21
22            return !!form.querySelector('input[type="text"][name="s"], input[type="search"][name="s"]');
23        });
24
25        //convert the search form elements to search form objects
26        this.searchForms = searchFormElements.map(function (formElement) {
27            return new ScrySearch_SearchForm(formElement);
28        });
29
30        //emit a custom event to let other scripts know the window is ready
31        document.dispatchEvent(new CustomEvent('scrySearchReady', {
32            detail: {
33                version: this.version,
34                searchForms: this.searchForms,
35                upgrades: this.upgrades,
36            }
37        }));
38    },
39
40    //get all search forms
41    getSearchForms: function () {
42        return this.searchForms;
43    },
44
45    //get all search forms with a particular class set on the <form> tag
46    getSearchFormsByClass: function (className) {
47        return this.searchForms.filter(function (form) {
48            return form?.formElement?.classList.contains(className) || false;
49        });
50    },
51
52    //add an upgrade to the window object
53    registerUpgrade: function (name, version) {
54        if (!this.upgrades[name]) {
55            this.upgrades[name] = new ScrySearch_Upgrade(name, version);
56        } else {
57            console.warn('Upgrade already exists, skipping', name);
58        }
59    },
60
61}
62
63//an object that backs a search form in the dom and provides an anchor point for logic
64class ScrySearch_SearchForm {
65    constructor(formElement) {
66        this.formElement = formElement;
67        this.data = {
68            core: {
69                autoSuggestElement: null,    //holds the autosuggest results container element
70                autosuggestResults: [],    //holds the autosuggest results data
71            },
72        }; //arbitrary data storage for the search form
73
74        this.preSubmitActions = []; //a list of ScrySearch_SubmitAction objects to call before the form is submitted
75        this.postSubmitActions = []; //a list of ScrySearch_SubmitAction objects to call after the form is submitted
76
77        this.preSubmitAjaxActions = []; //a list of ScrySearch_SubmitAction objects to call before the form is submitted via AJAX
78        this.postSubmitAjaxActions = []; //a list of ScrySearch_SubmitAction objects to call after the form is submitted via AJAX
79
80        this.searchInput = formElement.querySelector('input[type="text"][name="s"], input[type="search"][name="s"]');
81
82        this._ajaxRequestId = 0; //monotonically increasing id used to drop stale AJAX responses
83
84        //ensure when the formElement is submitted, we always call the submit function first
85        this.formElement.addEventListener('submit', (e) => {
86            e.preventDefault();
87            this.submit();
88        });
89
90        // Ensure *all* callers hit the debounce (even external code calling form.submitAjax()).
91        this.submitAjax = this.debounceAjaxSubmit(this.submitAjax.bind(this));
92    }
93
94    //submit the search form
95    submit() {
96
97        //sort and call the pre submit handles
98        this.preSubmitActions.sort((a, b) => a.order - b.order).forEach(action => action.call(this));
99
100        //submit the form
101        this.formElement.submit();
102
103        //sort and call the post submit actions
104        //these will likely not be used much due to the page refresh on submit
105        this.postSubmitActions.sort((a, b) => a.order - b.order).forEach(action => action.call(this));
106    }
107
108    //submit the search form via AJAX
109    async submitAjax() {
110        //sort and call the pre submit ajax handles
111        this.preSubmitAjaxActions.sort((a, b) => a.order - b.order).forEach(action => action.call(this));
112
113        //submit the form via AJAX
114        var data = await this._handleAjaxSubmit();
115
116        //sort and call the post submit ajax actions
117        this.postSubmitAjaxActions.sort((a, b) => a.order - b.order).forEach(action => action.call(this, data));
118
119        //return the data
120        return data;
121    }
122
123    //handle ajax submission
124    async _handleAjaxSubmit() {
125        // The REST endpoint only exists for autosuggest; never fire requests when it is disabled.
126        if (!scrySearchWindow.autoSuggestEnabled || !scrySearchWindow.restApiUrl) {
127            return undefined;
128        }
129        // Tag this request so out-of-order responses can be dropped.
130        const requestId = ++this._ajaxRequestId;
131        // Turn FormData into a JSON-able object (supports names like filters[facets][], etc.).
132        const deepSet = (obj, path, value) => {
133            if (Object(obj) !== obj) return obj;
134            if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
135            path.slice(0, -1).reduce((a, c, i) =>
136                Object(a[c]) === a[c]
137                    ? a[c]
138                    : a[c] = (Math.abs(path[i + 1]) >> 0) === +path[i + 1] ? [] : {},
139                obj
140            )[path[path.length - 1]] = value;
141            return obj;
142        };
143
144        //call the deep set function to convert the form data to a json object
145        const formData = new FormData(this.formElement);
146        const formDataObject = {};
147        for (const [path, value] of formData) {
148            deepSet(formDataObject, path, value);
149        }
150        //send a request to the rest api
151        var searchResults = await fetch(scrySearchWindow.restApiUrl, {
152            method: 'POST',
153            headers: {
154                'Content-Type': 'application/json',
155            },
156            credentials: 'same-origin',
157            body: JSON.stringify(formDataObject),
158        });
159        var data = await searchResults.json();
160
161        // A newer request started while this one was in flight: signal the caller to keep the newer results.
162        if (requestId !== this._ajaxRequestId) {
163            return undefined;
164        }
165
166        return data;
167    }
168
169    //debounce the ajax submission
170    debounceAjaxSubmit(fn, waitMs = 250) {
171        // Debounce network submissions so rapid typing can't overwhelm the server.
172        // Returns a promise so callers can still await the eventual AJAX result.
173        let timerId = null;
174        return (...args) => {
175            if (timerId) {
176                clearTimeout(timerId);
177            }
178            return new Promise((resolve, reject) => {
179                timerId = setTimeout(async () => {
180                    try {
181                        resolve(await fn(...args));
182                    } catch (err) {
183                        reject(err);
184                    }
185                }, waitMs);
186            });
187        };
188    }
189
190    //add actions to the pre submit actions
191    addPreSubmitAction(func, order) {
192        //ensure the function of the submit action is not already in the list
193        if (this.preSubmitActions.some(action => action.func === func)) {
194            console.warn('Submit action already exists, skipping', func);
195            return;
196        }
197        this.preSubmitActions.push(new ScrySearch_SubmitAction(func, order));
198    }
199
200    //add actions to the post submit actions
201    addPostSubmitAction(func, order) {
202        //ensure the function of the submit action is not already in the list
203        if (this.postSubmitActions.some(action => action.func === func)) {
204            console.warn('Submit action already exists, skipping', func);
205            return;
206        }
207        this.postSubmitActions.push(new ScrySearch_SubmitAction(func, order));
208    }
209
210    //add actions to the pre submit ajax actions
211    addPreSubmitAjaxAction(func, order) {
212        //ensure the function of the submit action is not already in the list
213        if (this.preSubmitAjaxActions.some(action => action.func === func)) {
214            console.warn('Submit action already exists, skipping', func);
215            return;
216        }
217        this.preSubmitAjaxActions.push(new ScrySearch_SubmitAction(func, order));
218    }
219
220    //add actions to the post submit ajax actions
221    addPostSubmitAjaxAction(func, order) {
222        //ensure the function of the submit action is not already in the list
223        if (this.postSubmitAjaxActions.some(action => action.func === func)) {
224            console.warn('Submit action already exists, skipping', func);
225            return;
226        }
227        this.postSubmitAjaxActions.push(new ScrySearch_SubmitAction(func, order));
228    }
229}
230
231//a function that is supplied externally that can be used to perform actions before or after a search
232class ScrySearch_SubmitAction {
233    constructor(func, order) {
234        this.func = func;
235        this.order = order;
236    }
237
238    //call the function
239    call(searchForm, data) {
240        // Actions may be written as (searchForm) or (searchForm, data).
241        // We key off argument count so "falsy" data (0, "", null) isn't accidentally dropped.
242        if (arguments.length >= 2) {
243            this.func(searchForm, data);
244            return;
245        }
246        this.func(searchForm);
247    }
248}
249
250//an object that represents an external upgrade to the scry search window system
251class ScrySearch_Upgrade {
252    constructor(name, version) {
253        this.name = name;
254        this.version = version;
255        this.data = {}; //arbitrary data storage for the upgrade
256    }
257}
258
259//initialize the scry search window object
260document.addEventListener('DOMContentLoaded', function () {
261    window.scrySearch.init();
262});

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.