PageSourceSearch

https://www.huisartsnu.be/modules/contrib/chosen/js/chosen.js?tlg400

js huisartsnu.be collected 2026-09-29 03:25:28 UTC 13,739 bytes, 431 lines download raw bytes

1/**
2 * @file
3 * Attaches behaviors for the Chosen module.
4 */
5(function(Drupal, drupalSettings, once) {
6  'use strict';
7
8  // Update Chosen elements when state has changed.
9  document.addEventListener('state:disabled', function(e) {
10    if (e.target && e.target.matches('select')) {
11      const event = new Event('chosen:updated', { bubbles: true, cancelable: true });
12      e.target.dispatchEvent(event);
13    }
14  });
15
16  Drupal.behaviors.chosen = {
17
18    settings: {
19
20      /**
21       * Completely ignores elements that match one of these selectors.
22       *
23       * Disabled on:
24       * - Field UI
25       * - WYSIWYG elements
26       * - Tabledrag weights
27       * - Elements that have opted-out of Chosen
28       * - Elements already processed by Chosen.
29       *
30       * @type {string}
31       */
32      ignoreSelector: '#field-ui-field-storage-add-form select, #entity-form-display-edit-form select, #entity-view-display-edit-form select, .wysiwyg, .draggable select[name$="[weight]"], .draggable select[name$="[position]"], .locale-translate-filter-form select, .chosen-disable, .chosen-processed',
33
34      /**
35       * Explicit "opt-in" selector.
36       *
37       * @type {string}
38       */
39      optedInSelector: 'select.chosen-enable',
40
41      /**
42       * The default selector, overridden by drupalSettings.
43       *
44       * @type {string}
45       */
46      selector: 'select:visible',
47
48      /**
49       * Minimum options for single and multiple selects.
50       * These may be defined in drupalSettings.chosen.
51       */
52      minimum_single: 0,
53      minimum_multiple: 0,
54
55      /**
56       * Other default options for Chosen.
57       */
58      options: {},
59      minimum_width: 0,
60      use_relative_width: false,
61    },
62
63    /**
64     * Drupal attach behavior.
65     */
66    attach: function(context, settings) {
67      this.settings = this.getSettings(settings);
68      const elements = this.getElements(context);
69      // Use Drupal's once function to process elements only once
70      once('chosen', elements).forEach(function(element) {
71        // If inside a Drupal dialog, resize the dialog on open & close event.
72        if (element.closest('#drupal-modal')) {
73          element.addEventListener('chosen:showing_dropdown', (e) => {
74            const nextElement = element.nextElementSibling;
75            if (nextElement) {
76              const chosenDrop = nextElement.querySelector('.chosen-drop');
77              if (chosenDrop) {
78                chosenDrop.style.position = 'static';
79                const event = new Event('dialogContentResize', { bubbles: true, cancelable: true });
80                element.dispatchEvent(event);
81              }
82            }
83          });
84          element.addEventListener('chosen:hiding_dropdown', (e) => {
85            const nextElement = element.nextElementSibling;
86            if (nextElement) {
87              const chosenDrop = nextElement.querySelector('.chosen-drop');
88              if (chosenDrop) {
89                chosenDrop.style.position = '';
90                const event = new Event('dialogContentResize', { bubbles: true, cancelable: true });
91                element.dispatchEvent(event);
92              }
93            }
94          });
95        }
96        this.createChosen(element);
97      }.bind(this));
98    },
99
100    /**
101     * Creates a Chosen instance for a specific element.
102     *
103     * @param {HTMLElement} element
104     *   The element.
105     */
106    createChosen: function(element) {
107      const options = this.getElementOptions(element);
108      element.chosen(options);
109      this.observeDisabledState(element);
110
111      if (options.add_helper_buttons || element.hasAttribute('chosen_add_helper_buttons')) {
112        if (element.hasAttribute('multiple')) {
113          const allButton = document.createElement('button');
114          allButton.type = 'button';
115          allButton.className = 'button chosen-helper-btn';
116          allButton.textContent = 'All';
117          allButton.addEventListener('click', function() {
118            Array.from(element.options).forEach(function(option) {
119              option.selected = true;
120            });
121            const event = new Event('chosen:updated', { bubbles: true, cancelable: true });
122            element.dispatchEvent(event);
123          });
124
125          const noneButton = document.createElement('button');
126          noneButton.type = 'button';
127          noneButton.className = 'button chosen-helper-btn';
128          noneButton.textContent = 'None';
129          noneButton.addEventListener('click', function() {
130            Array.from(element.options).forEach(function(option) {
131              option.selected = false;
132            });
133            const event = new Event('chosen:updated', { bubbles: true, cancelable: true });
134            element.dispatchEvent(event);
135          });
136
137          element.parentNode.appendChild(allButton);
138          element.parentNode.appendChild(noneButton);
139        }
140      }
141    },
142
143    /**
144     * Observes disabled attribute changes on a Chosen-managed select.
145     *
146     * @param {HTMLElement} element
147     *   The select element.
148     */
149    observeDisabledState: function(element) {
150      if (element.__chosenDisabledObserver) {
151        return;
152      }
153
154      const observer = new MutationObserver(function(mutations) {
155        mutations.forEach(function(mutation) {
156          if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
157            const event = new Event('chosen:updated', { bubbles: true, cancelable: true });
158            element.dispatchEvent(event);
159          }
160        });
161      });
162
163      observer.observe(element, {
164        attributes: true,
165        attributeFilter: ['disabled'],
166      });
167
168      element.__chosenDisabledObserver = observer;
169    },
170
171    /**
172     * Filter out elements that should not be converted into Chosen.
173     *
174     * @param {HTMLElement} element
175     *   The element.
176     *
177     * @return {boolean}
178     *   TRUE if the element should stay, FALSE otherwise.
179     */
180    filterElements: function(element) {
181      // Remove elements that should be ignored completely.
182      if (element.matches(this.settings.ignoreSelector)) {
183        return false;
184      }
185
186      // Zero value means no minimum.
187      const minOptions = element.hasAttribute('multiple') ? this.settings.minimum_multiple : this.settings.minimum_single;
188      return !minOptions || element.querySelectorAll('option').length >= minOptions;
189    },
190
191    /**
192     * Checks if the element is visible.
193     *
194     * @param {HTMLElement} el
195     *   A DOM element to check visibility for.
196     * @return {boolean}
197     *   True if the element is visible, false otherwise.
198     */
199    isVisible: function(el) {
200      return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
201    },
202
203    /**
204     * Splits a selector string by top-level commas only.
205     *
206     * @param {string} selector
207     *   A CSS selector string.
208     * @return {Array}
209     *   An array of selector parts.
210     */
211    splitSelectors: function(selector) {
212      const result = [];
213      let current = '';
214      let depth = 0;
215
216      for (let i = 0; i < selector.length; i++) {
217        const char = selector[i];
218
219        if (char === '[' || char === '(') {
220          depth++;
221        }
222        else if (char === ']' || char === ')') {
223          depth = Math.max(depth - 1, 0);
224        }
225
226        if (char === ',' && depth === 0) {
227          if (current.trim()) {
228            result.push(current.trim());
229          }
230          current = '';
231        }
232        else {
233          current += char;
234        }
235      }
236
237      if (current.trim()) {
238        result.push(current.trim());
239      }
240
241      return result;
242    },
243
244    /**
245     * Parses the selector to handle :visible manually.
246     *
247     * @param {string} selector
248     *   A CSS selector string that may contain :visible.
249     * @return {Array}
250     *   An array of elements that match the selector and are visible (if :visible is present).
251     */
252    getVisibleElements: function(context, selector) {
253      const selectors = this.splitSelectors(selector);
254      let elements = [];
255
256      selectors.forEach(s => {
257        const hasVisibleFilter = s.includes(':visible');
258        const cleanSelector = s.replace(':visible', '').trim();
259
260        if (!cleanSelector) {
261          return;
262        }
263
264        let selectedElements;
265        try {
266          selectedElements = Array.from(context.querySelectorAll(cleanSelector));
267        } catch (error) {
268          return;
269        }
270
271        if (hasVisibleFilter) {
272          selectedElements = selectedElements.filter(this.isVisible);
273        }
274
275        elements = elements.concat(selectedElements);
276      });
277
278      return elements;
279    },
280
281    /**
282     * Retrieves the elements that should be converted into Chosen instances.
283     *
284     * @param {HTMLElement|Document} context
285     *   A DOM Element or Document to use as context.
286     * @param {string} [selector]
287     *   A selector to use, defaults to the default selector in the settings.
288     */
289    getElements: function(context, selector) {
290      context = context || document;
291      let elements = this.getVisibleElements(context, selector || this.settings.selector);
292
293      // Remove elements that should not be converted into Chosen.
294      elements = elements.filter(function(element) {
295        return this.filterElements(element);
296      }.bind(this));
297
298      // Add elements that have explicitly opted in to Chosen.
299      const optedInElements = Array.from(context.querySelectorAll(this.settings.optedInSelector));
300
301      // Combine elements and opted-in elements, avoiding duplicates.
302      optedInElements.forEach(function(element) {
303        if (!elements.includes(element)) {
304          elements.push(element);
305        }
306      });
307
308      return elements;
309    },
310
311    /**
312     * Retrieves options used to create a Chosen instance based on an element.
313     *
314     * @param {HTMLElement} element
315     *   The element to process.
316     *
317     * @return {Object}
318     *   The options object used to instantiate a Chosen instance with.
319     */
320    getElementOptions: function(element) {
321      const options = Object.assign({}, this.settings.options, this.getElementOverriddenOptions(element));
322      let dimension;
323      let width;
324
325      // The width default option is considered the minimum width, so this
326      // must be evaluated for every option.
327      if (this.settings.minimum_width > 0) {
328        // Given we need to manage settings as both percentage and pixel widths,
329        // we need to handle width calculations separately.
330        if (this.settings.use_relative_width) {
331          dimension = '%';
332          const elementWidth = element.getBoundingClientRect().width;
333          const parentWidth = element.parentNode.getBoundingClientRect().width;
334          width = ((elementWidth / parentWidth) * 100).toPrecision(5);
335        } else {
336          dimension = 'px';
337          width = element.getBoundingClientRect().width;
338        }
339
340        if (width < this.settings.minimum_width) {
341          options.width = this.settings.minimum_width + dimension;
342        } else {
343          options.width = width + dimension;
344        }
345      }
346
347      // Some field widgets have cardinality, so we must respect that.
348      // @see \Drupal\chosen\ChosenFormRender::preRenderSelect()
349      const cardinality = element.dataset.cardinality;
350      if (element.hasAttribute('multiple') && cardinality) {
351        options.max_selected_options = parseInt(cardinality);
352      }
353
354      return options;
355    },
356
357    /**
358     * Retrieves element-specific options used to override global defaults.
359     *
360     * @param {HTMLElement} element
361     *   The element to process.
362     *
363     * @return {Object}
364     *   The options object with per-element overrides.
365     */
366    getElementOverriddenOptions: function(element) {
367      const overriddenOptions = {};
368
369      const noResultsText = element.getAttribute('data-no_results_text');
370      if (noResultsText) {
371        overriddenOptions.no_results_text = noResultsText;
372      }
373
374      const searchContains = element.getAttribute('data-search_contains');
375      if (searchContains === '1') {
376        overriddenOptions.search_contains = true;
377      }
378      else if (searchContains === '2') {
379        overriddenOptions.search_contains = false;
380      }
381
382      return overriddenOptions;
383    },
384
385    /**
386     * Retrieves the settings passed from Drupal.
387     *
388     * @param {Object} [settings]
389     *   Passed Drupal settings object, if any.
390     */
391    getSettings: function(settings) {
392      // Deep merge the settings
393      return this.deepMerge({}, this.settings, (settings && settings.chosen) || drupalSettings.chosen || {});
394    },
395
396    /**
397     * Deep merges multiple objects into the first object.
398     * This function is used instead of Object.assign to handle nested objects.
399     *
400     * @param {Object} target
401     *   The target object to merge properties into.
402     * @param {...Object} sources
403     *   The source objects from which to copy properties.
404     * @return {Object}
405     *   The target object.
406     */
407    deepMerge: function(target, ...sources) {
408      if (!sources.length) return target;
409      const source = sources.shift();
410
411      if (typeof target !== 'object' || typeof source !== 'object') {
412        return target;
413      }
414
415      for (const key in source) {
416        if (source.hasOwnProperty(key)) {
417          if (typeof source[key] === 'object') {
418            if (!target[key]) Object.assign(target, { [key]: {} });
419            this.deepMerge(target[key], source[key]);
420          } else {
421            Object.assign(target, { [key]: source[key] });
422          }
423        }
424      }
425
426      return this.deepMerge(target, ...sources);
427    },
428
429  };
430
431})(Drupal, drupalSettings, once);

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.