PageSourceSearch

https://www.adph.org.uk/wp-content/plugins/facetwp/assets/vendor/fSelect/fSelect.js?ver=4.4.1

js adph.org.uk collected 2026-09-24 09:33:10 UTC 15,160 bytes, 420 lines download raw bytes

1window.fSelect = (() => {
2
3    var build = {};
4
5    class fSelect {
6
7        constructor(selector, options) {
8            let that = this;
9
10            var defaults = {
11                placeholder: 'Select some options',
12                numDisplayed: 3,
13                overflowText: '{n} selected',
14                searchText: 'Search',
15                noResultsText: 'No results found',
16                showSearch: true,
17                optionFormatter: false
18            };
19
20            that.settings = Object.assign({}, defaults, options);
21            build = {output: '', optgroup: 0, idx: 0};
22
23            if ('string' === typeof selector) {
24                var nodes = Array.from(document.querySelectorAll(selector));
25            }
26            else if (selector instanceof Node) {
27                var nodes = [selector];
28            }
29            else if (Array.isArray(selector)) {
30                var nodes = selector;
31            }
32            else {
33                var nodes = [];
34            }
35
36            if ('undefined' === typeof window.fSelectInit) {
37                window.fSelectInit = {
38                    searchCache: '',
39                    lastChoice: null,
40                    lastFocus: null,
41                    activeEl: null
42                };
43                that.bindEvents();
44            }
45
46            nodes.forEach((input) => {
47                if (typeof input.fselect === 'object') {
48                    input.fselect.destroy();
49                }
50
51                that.settings.multiple = input.matches('[multiple]');
52                input.fselect = that;
53                that.input = input;
54                that.create();
55            });
56        }
57
58        create() {
59            var that = this;
60            var options = that.buildOptions();
61            var label = that.getDropdownLabel();
62            var mode = (that.settings.multiple) ? 'multiple' : 'single';
63            var searchClass = (that.settings.showSearch) ? '' : ' fs-hidden';
64            var noResultsClass = (build.idx < 2) ? '' : ' fs-hidden';
65
66            var html = `
67            <div class="fs-wrap ${mode}" tabindex="0">
68                <div class="fs-label-wrap">
69                    <div class="fs-label">${label}</div>
70                    <span class="fs-arrow"></span>
71                </div>
72                <div class="fs-dropdown fs-hidden">
73                    <div class="fs-search${searchClass}">
74                        <input type="text" placeholder="${that.settings.searchText}" />
75                    </div>
76                    <div class="fs-no-results${noResultsClass}">${that.settings.noResultsText}</div>
77                    <div class="fs-options">${options}</div>
78                </div>
79            </div>
80            `;
81
82            var tpl = document.createElement('template');
83            tpl.innerHTML = html;
84            var wrap = tpl.content.querySelector('.fs-wrap');
85            that.input.parentNode.insertBefore(wrap, that.input.nextSibling);
86            that.input.classList.add('fs-hidden');
87
88            // add a relationship link
89            that.input._rel = wrap;
90            wrap._rel = that.input;
91        }
92
93        destroy() {
94            this.input._rel.remove();
95            this.input.classList.remove('fs-hidden');
96            delete this.input._rel;
97        }
98
99        reload() {
100            this.destroy();
101            this.create();
102        }
103
104        open() {
105            var wrap = this.input._rel;
106            wrap.classList.add('fs-open');
107            wrap.querySelector('.fs-dropdown').classList.remove('fs-hidden');
108
109            // don't auto-focus for touch devices
110            if (! window.matchMedia("(pointer: coarse)").matches) {
111                wrap.querySelector('.fs-search input').focus();
112            }
113
114            window.fSelectInit.lastChoice = this.getSelectedOptions('value');
115            window.fSelectInit.activeEl = wrap;
116
117            this.trigger('fs:opened', wrap);
118        }
119
120        close() {
121            this.input._rel.classList.remove('fs-open');
122            this.input._rel.querySelector('.fs-dropdown').classList.add('fs-hidden');
123
124            window.fSelectInit.searchCache = '';
125            window.fSelectInit.lastChoice = null;
126            window.fSelectInit.lastFocus = null;
127            window.fSelectInit.activeEl = null;
128
129            this.trigger('fs:closed', this.input._rel);
130        }
131
132        buildOptions(parent) {
133            var that = this;
134            var parent = parent || that.input;
135
136            Array.from(parent.children).forEach((node) => {
137                if ('optgroup' === node.nodeName.toLowerCase()) {
138                    var opt = `<div class="fs-optgroup-label" data-group="${build.optgroup}">${node.label}</div>`;
139                    build.output += opt;
140                    that.buildOptions(node);
141                    build.optgroup++;
142                }
143                else {
144                    var val = node.value;
145
146                    // skip the first choice in multi-select mode
147                    if (0 === build.idx && '' === val && that.settings.multiple) {
148                        build.idx++;
149                        return;
150                    }
151
152                    var classes = ['fs-option', 'g' + build.optgroup];
153
154                    // append existing classes
155                    node.className.split(' ').forEach((className) => {
156                        if ('' !== className) {
157                            classes.push(className);
158                        }
159                    });
160
161                    if (node.matches('[disabled]')) classes.push('disabled');
162                    if (node.matches('[selected]')) classes.push('selected');
163                    classes = classes.join(' ');
164
165                    if ('function' === typeof that.settings.optionFormatter) {
166                        node.label = that.settings.optionFormatter(node.label, node);
167                    }
168
169                    var opt = `<div class="${classes}" data-value="${val}" data-idx="${build.idx}" tabindex="-1"><span class="fs-checkbox"><i></i></span><div class="fs-option-label">${node.label}</div></div>`;
170
171                    build.output += opt;
172                    build.idx++;
173                }
174            });
175
176            return build.output;
177        }
178
179        getSelectedOptions(field, context) {
180            var context = context || this.input;
181            return Array.from(context.selectedOptions).map((opt) => {
182                return (field) ? opt[field] : opt;
183            });
184        }
185
186        getAdjacentSibling(which) {
187            var that = this;
188            var which = which || 'next';
189            var sibling = window.fSelectInit.lastFocus;
190            var selector = '.fs-option:not(.fs-hidden):not(.disabled)';
191
192            if (sibling) {
193                sibling = sibling[which + 'ElementSibling'];
194
195                while (sibling) {
196                    if (sibling.matches(selector)) break;
197                    sibling = sibling[which + 'ElementSibling'];
198                }
199
200                return sibling;
201            }
202            else if ('next' == which) {
203                sibling = that.input._rel.querySelector(selector);
204            }
205
206            return sibling;
207        }
208
209        getDropdownLabel() {
210            var that = this;
211            var labelText = that.getSelectedOptions('text');
212
213            if (labelText.length < 1) {
214                labelText = that.settings.placeholder;
215            }
216            else if (labelText.length > that.settings.numDisplayed) {
217                labelText = that.settings.overflowText.replace('{n}', labelText.length);
218            }
219            else {
220                labelText = labelText.join(', ');
221            }
222
223            return labelText;
224        }
225
226        debounce(func, wait) {
227            var timeout;
228            return (...args) => {
229                var boundFunc = func.bind(this, ...args);
230                clearTimeout(timeout);
231                timeout = setTimeout(boundFunc, wait);
232            }
233        }
234
235        trigger(eventName, ...args) {
236            document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]}));
237        }
238
239        on(eventName, elementSelector, handler) {
240            document.addEventListener(eventName, function(e) {
241                // loop parent nodes from the target to the delegation node
242                for (var target = e.target; target && target != this; target = target.parentNode) {
243                    if (target.matches(elementSelector)) {
244                        handler.call(target, e);
245                        break;
246                    }
247                }
248            }, false);
249        }
250
251        bindEvents() {
252            var that = this;
253            var optionSelector = '.fs-option:not(.fs-hidden):not(.disabled)';
254            var unaccented = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
255
256            // debounce search for better performance
257            that.on('keyup', '.fs-search input', that.debounce(function(e) {
258                var wrap = e.target.closest('.fs-wrap');
259                var options = wrap._rel.options;
260
261                var matchOperators = /[|\\{}()[\]^$+*?.]/g;
262                var keywords = e.target.value.replace(matchOperators, '\\$&');
263                keywords = unaccented(keywords);
264
265                // if the searchCache already has a prefixed version of this search
266                // then don't un-hide the existing exclusions
267                if (0 !== keywords.indexOf(window.fSelectInit.searchCache)) {
268                    wrap.querySelectorAll('.fs-option, .fs-optgroup-label').forEach((node) => node.classList.remove('fs-hidden'));
269                }
270
271                window.fSelectInit.searchCache = keywords;
272
273                for (var i = 0; i < options.length; i++) {
274                    if ('' === options[i].value) continue;
275
276                    var needle = new RegExp(keywords, 'gi');
277                    var haystack = unaccented(options[i].text);
278
279                    if (null === haystack.match(needle)) {
280                        wrap.querySelector('.fs-option[data-idx="' + i + '"]').classList.add('fs-hidden');
281                    }
282                }
283
284                // hide optgroups if no choices
285                wrap.querySelectorAll('.fs-optgroup-label').forEach((node) => {
286                    var group = node.getAttribute('data-group');
287                    var container = node.closest('.fs-options');
288                    var count = container.querySelectorAll('.fs-option.g' + group + ':not(.fs-hidden)').length;
289
290                    if (count < 1) {
291                        node.classList.add('fs-hidden');
292                    }
293                });
294
295                // toggle the noResultsText div
296                if (wrap.querySelectorAll('.fs-option:not(.fs-hidden').length) {
297                    wrap.querySelector('.fs-no-results').classList.add('fs-hidden');
298                }
299                else {
300                    wrap.querySelector('.fs-no-results').classList.remove('fs-hidden');
301                }
302
303            }, 100));
304
305            that.on('click', optionSelector, function(e) {
306                var wrap = this.closest('.fs-wrap');
307                var value = this.getAttribute('data-value');
308                var input = wrap._rel;
309                var isMultiple = wrap.classList.contains('multiple');
310
311                if (!isMultiple) {
312                    input.value = value;
313                    wrap.querySelectorAll('.fs-option.selected').forEach((node) => node.classList.remove('selected'));
314                }
315                else {
316                    var idx = parseInt(this.getAttribute('data-idx'));
317                    input.options[idx].selected = !this.classList.contains('selected');
318                }
319
320                this.classList.toggle('selected');
321
322                var label = input.fselect.getDropdownLabel();
323                wrap.querySelector('.fs-label').innerHTML = label;
324
325                // fire a change event
326                input.dispatchEvent(new Event('change', { bubbles: true }));
327                input.fselect.trigger('fs:changed', wrap);
328
329                if (!isMultiple) {
330                    input.fselect.close();
331                }
332
333                e.stopImmediatePropagation();
334            });
335
336            that.on('keydown', '*', function(e) {
337                var wrap = this.closest('.fs-wrap');
338
339                if (!wrap) return;
340
341                if (-1 < [38, 40, 27].indexOf(e.which)) {
342                    e.preventDefault();
343                }
344
345                if (32 == e.which || 13 == e.which) { // space, enter
346                    if (e.target.closest('.fs-search')) {
347                        // preserve spaces for search
348                    }
349                    else if (e.target.matches(optionSelector)) {
350                        e.target.click();
351                        e.preventDefault();
352                    }
353                    else {
354                        wrap.querySelector('.fs-label').click();
355                        e.preventDefault();
356                    }
357                }
358                else if (38 == e.which) { // up
359                    var sibling = wrap._rel.fselect.getAdjacentSibling('previous');
360                    window.fSelectInit.lastFocus = sibling; // stop at the search box
361
362                    if (sibling) {
363                        sibling.focus();
364                    }
365                    else {
366                        wrap.querySelector('.fs-search input').focus();
367                    }
368                }
369                else if (40 == e.which) { // down
370                    var sibling = wrap._rel.fselect.getAdjacentSibling('next');
371
372                    if (sibling) {
373                        sibling.focus();
374                        window.fSelectInit.lastFocus = sibling; // stop at the bottom
375                    }
376                }
377                else if (9 == e.which || 27 == e.which) { // tab, esc
378                    wrap._rel.fselect.close();
379                }
380            });
381
382            that.on('click', '*', function(e) {
383                var wrap = this.closest('.fs-wrap');
384                var lastActive = window.fSelectInit.activeEl;
385
386                if (wrap) {
387                    var labelWrap = this.closest('.fs-label-wrap');
388
389                    if (labelWrap) {
390                        if (lastActive) {
391                            lastActive._rel.fselect.close();
392                        }
393                        if (wrap !== lastActive) {
394                            wrap._rel.fselect.open();
395                        }
396                    }
397                }
398                else {
399                    if (lastActive) {
400                        lastActive._rel.fselect.close();
401                    }
402                }
403            });
404        }
405    }
406
407    var $ = (selector, options) => new fSelect(selector, options);
408
409    return $;
410
411})();
412
413if ('undefined' !== typeof fUtil) {
414    fUtil.fn.fSelect = function(opts) {
415        this.each(function() { // no arrow function to preserve "this"
416            fSelect(this, opts);
417        });
418        return this;
419    };
420}

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.