PageSourceSearch

https://www.spoticar.nl/modules/custom/psa_search/dist/js/new_spoticar/search--common.js?tmm1e1

js spoticar.nl collected 2026-10-09 01:38:58 UTC 14,464 bytes, 442 lines download raw bytes

1import {SearchVars} from "./list-search-global-vars.js";
2
3(function ($, Drupal) {
4
5  /**
6   * Parse formatted value using dot thousands separator.
7   * @param {string|number|null|undefined} value
8   * @returns {number|null}
9   */
10  function parseThousandsDot(value) {
11    const digits = String(value ?? '').replace(/\D/g, '');
12    return digits ? Number(digits) : null;
13  }
14
15  /**
16   * Collect noUiSlider slider configs
17   */
18  function collectSliderConfigs() {
19    const filters = {};
20    const sliderInputs = document.querySelectorAll('.slide-filter');
21
22    sliderInputs.forEach(input => {
23      const name = input.getAttribute('name');
24      if (!name) return;
25
26      const min = input.dataset.sliderMin;
27      const max = input.dataset.sliderMax;
28      const step = input.dataset.sliderStep;
29      const defaultValue = input.dataset.sliderDefaultValue;
30
31      if (!min || !max || !step || !defaultValue) return;
32
33      filters[name] = {
34        min: Number(min),
35        max: Number(max),
36        step: Number(step),
37        default: defaultValue.split(',').map(v => Number(v.trim()))
38      };
39    });
40
41    return filters;
42  }
43
44  /**
45   * init noUiSlider slider filter
46   * @param key
47   * @param conf
48   */
49  function initSlider(key, conf) {
50    if (key === SearchVars.selected_slider_name
51      || key === `advanced-${SearchVars.selected_slider_name}`) return;
52
53    const sliderElement = document.getElementById(`slider-${key}`);
54    if (!sliderElement) return;
55
56    if (sliderElement.noUiSlider) {
57      sliderElement.noUiSlider.destroy();
58    }
59
60    const inputMin = document.getElementById(`${key}-min`);
61    const inputMax = document.getElementById(`${key}-max`);
62
63    if (!inputMin || !inputMax) return;
64
65    const startValues =
66      conf.values?.Cash || conf.default || [conf.min, conf.max];
67
68    noUiSlider.create(sliderElement, {
69      start: startValues,
70      connect: true,
71      step: conf.step,
72      range: { min: conf.min, max: conf.max },
73      format: {
74        to: v => Math.round(v),
75        from: v => Number(v),
76      },
77    });
78
79    bindSliderEvents(sliderElement, key, conf, inputMin, inputMax);
80    initToggleButtons(sliderElement);
81  }
82
83  /**
84   * @param sliderElement
85   * @param key
86   * @param conf
87   * @param inputMin
88   * @param inputMax
89   */
90  function bindSliderEvents(sliderElement, key, conf, inputMin, inputMax) {
91    const shouldFormatThousands = sliderElement.closest('[id$="-slide"], .filter-content, .filter-section')
92      ?.querySelector('.filter-data[data-thousand-format="1"]') !== null;
93    sliderElement.noUiSlider.on('update', (values, handle) => {
94      const inputs = [inputMin, inputMax];
95      if (handle === 0 || handle === 1) {
96        const value = values[handle];
97        inputs[handle].value = shouldFormatThousands ? $.fn.format_thousands_dot(value) : value;
98      }
99    });
100
101    const handleBlur = (values, handle) => {
102      const type = handle === 0 ? 'min' : 'max';
103      const filterInput = document.getElementById(`${type}_${key}`);
104
105      if (!filterInput) return;
106
107      filterInput.value = values[handle];
108      filterInput.dispatchEvent(new Event('blur'));
109
110      if (SearchVars?.is_home_page) {
111        syncDuplicatedFilters(sliderElement);
112      }
113    };
114
115    // Setting values from slider to inputs
116    sliderElement.noUiSlider.on('change', handleBlur);
117
118    // Setting values from inputs to slider
119    const bindInput = (input, handle) => {
120      input.addEventListener('input', () => {
121        if (!shouldFormatThousands) return;
122        const parsed = parseThousandsDot(input.value);
123        input.value = parsed === null ? '' : $.fn.format_thousands_dot(parsed);
124      });
125
126      input.addEventListener('change', () => {
127        const parsed = shouldFormatThousands ? parseThousandsDot(input.value) : Number(input.value);
128        if (parsed === null || Number.isNaN(parsed)) return;
129        sliderElement.noUiSlider.set(
130          handle === 0 ? [parsed, null] : [null, parsed]
131        );
132      });
133
134      input.addEventListener('blur', () => {
135        if (shouldFormatThousands) {
136          const parsed = parseThousandsDot(input.value);
137          if (parsed !== null) input.value = $.fn.format_thousands_dot(parsed);
138        }
139        handleBlur(sliderElement.noUiSlider.get(), handle);
140      });
141
142      // input blur on enter key press.
143      input.addEventListener('keydown', (e) => {
144        if (e.key === 'Enter') {
145          input.blur();
146        }
147      });
148    };
149
150    bindInput(inputMin, 0);
151    bindInput(inputMax, 1);
152  }
153
154  /**
155   * @param sliderElement
156   */
157  function initToggleButtons(sliderElement) {
158
159    const toggleButtons = sliderElement
160      .closest('.filter-section')
161      ?.querySelectorAll('.toggle-buttons .toggle');
162
163    if (!toggleButtons?.length) return;
164
165    toggleButtons.forEach(btn => {
166
167      if (btn.dataset.listenerAttached) return;
168      btn.dataset.listenerAttached = 'true';
169
170      const handleToggle = () => {
171        toggleButtons.forEach(b => b.classList.remove('active'));
172        btn.classList.add('active');
173
174        const targetId = btn.dataset.target;
175        const target = document.getElementById(targetId);
176        if (!target) return;
177
178        target.classList.remove('hide');
179
180        const parent = target.parentNode;
181        Array.from(parent.children).forEach(el => {
182          if (el !== target && !el.classList.contains('toggle-buttons')) {
183            el.classList.add('hide');
184          }
185        });
186      };
187
188      btn.addEventListener('click', handleToggle);
189      btn._toggleHandler = handleToggle;
190    });
191  }
192
193  /**
194   * @param sliderElement
195   */
196  function syncDuplicatedFilters(sliderElement) {
197
198    const defaultFilter = sliderElement.dataset.defaultFilter;
199    if (!defaultFilter) return;
200
201    const values = sliderElement.noUiSlider.get().map(Number);
202
203    document
204      .querySelectorAll(`.double-slider[data-default-filter="${defaultFilter}"]`)
205      .forEach(el => {
206        if (el !== sliderElement) {
207          el.noUiSlider.set(values);
208        }
209      });
210  }
211
212  /**
213   * @param context
214   */
215  function runDoubleSlider(context = document) {
216    const filters = collectSliderConfigs();
217
218    if (!Object.keys(filters).length) return;
219
220    Object.entries(filters).forEach(([key, conf]) => {
221      initSlider(key, conf);
222    });
223  }
224
225  Drupal.doubleSliderInit = runDoubleSlider;
226
227  Drupal.behaviors.double_slider = {
228    attach(context) {
229      runDoubleSlider(context);
230    }
231  };
232
233  // --- Toggle filter sections ---
234  const filter_headers = document.querySelectorAll('.filter-header');
235  filter_headers.forEach(header => {
236    header.addEventListener('click', () => {
237      const filter_content = header.nextElementSibling;
238      header.classList.toggle('active');
239      filter_content.classList.toggle('open');
240    });
241  });
242
243  const subFilterWrappers = document.querySelectorAll('.sub-filters-wrapper');
244  subFilterWrappers.forEach(wrapper => {
245    const header = wrapper.querySelector('.sub-filters-header');
246    const content = wrapper.querySelector('.sub-filters-content');
247    const wrapperId = wrapper.id;
248
249    if (!header || !content) return;
250    if (header.classList.contains('active')) {
251      content.classList.add('open');
252      wrapper.style.borderBottom = 'none';
253    }
254
255    header.addEventListener('click', () => {
256      const targetWrapper = document.getElementById(wrapperId);
257      const targetHeader = targetWrapper.querySelector('.sub-filters-header');
258      const targetContent = targetWrapper.querySelector('.sub-filters-content');
259      targetHeader.classList.toggle('active');
260      targetContent.classList.toggle('open');
261      if (targetHeader.classList.contains('active')) {
262        targetWrapper.style.borderBottom = 'none';
263      } else {
264        targetWrapper.style.borderBottom = '1px solid #e0e0e0';
265      }
266    });
267  });
268
269  // --- Toggle filters visibility ---
270  Drupal.behaviors.toggle_filters = {
271    attach: function (context) {
272      const toggle_btn = $('#toggle-filters', context);
273      const wrapper = $('#teaser-page-wrapper', context);
274      const filters_container = document.getElementById("filters-container");
275      const modal_body = document.getElementById("modal-filters-body");
276      const modal = document.getElementById("new-advanced-search");
277      const close_btn = modal?.querySelector(".close");
278      const see_result_btn = modal?.querySelector("#mobile-see-result-btn");
279
280      /**
281       * Moves the filters container into the modal body.
282       * Used when screen width is less than 1199px.
283       */
284      function moveFiltersToModal() {
285        if (!modal_body || !filters_container) return;
286        if (!modal_body.contains(filters_container)) {
287          modal_body.appendChild(filters_container);
288        }
289      }
290
291      /**
292       * Moves the filters container back to its original position in the page.
293       * Used when screen width is 1199px or more.
294       */
295      function moveFiltersBack() {
296        if (!wrapper || !wrapper[0] || !filters_container) return;
297        const teaserPart = wrapper.find(".teaser-part").get(0);
298        if (!teaserPart) return;
299        wrapper[0].insertBefore(filters_container, teaserPart);
300      }
301
302      /**
303       * Opens the modal, moves filters into it, adds backdrop,
304       * and disables page scrolling.
305       */
306      function openModal() {
307        moveFiltersToModal();
308        Drupal.ModalUtils.openModal(modal);
309      }
310      Drupal.t("psa.backoffice.list_search.set_filters");
311      Drupal.t("psa.backoffice.list_search.hide_filter");
312      // Toggle filters visibility or open modal depending on screen size
313      toggle_btn.off('click.toggleFilters').on('click.toggleFilters', function () {
314        if (window.innerWidth > 1199) {
315          wrapper.toggleClass('filters-hidden');
316          const label = wrapper.hasClass('filters-hidden') ?
317           Drupal.t("psa.backoffice.list_search.set_filters") : Drupal.t("psa.backoffice.list_search.hide_filter");
318          toggle_btn.find('.choices-list').text(label);
319        } else {
320          openModal();
321        }
322      });
323
324      // Close modal on close button click
325      function closeSearchModal(event) {
326        event.preventDefault();
327        Drupal.ModalUtils.closeModal(modal);
328      }
329
330      close_btn?.addEventListener("click", function (e) {
331        closeSearchModal(e);
332      });
333
334      see_result_btn?.addEventListener("click", function (e) {
335        closeSearchModal(e);
336      });
337
338      /**
339       * Handles screen resize: moves filters to modal or back to page
340       * depending on current screen width.
341       */
342      function handleResize() {
343        if (window.innerWidth <= 1199) {
344          moveFiltersToModal();
345          Drupal.t("psa.backoffice.list_search.set_filter_mobile");
346          toggle_btn.find('.choices-list').text(Drupal.t("psa.backoffice.list_search.set_filter_mobile"));
347        } else {
348          moveFiltersBack();
349          const label = wrapper.hasClass('filters-hidden') ?
350            Drupal.t("psa.backoffice.list_search.set_filters") : Drupal.t("psa.backoffice.list_search.hide_filter") ;
351          toggle_btn.find('.choices-list').text(label);
352          if (modal?.classList.contains('show')) {
353            Drupal.ModalUtils.closeModal(modal);
354          }
355        }
356      }
357
358      // Listen to window resize and initialize layout
359      window.removeEventListener("resize", handleResize);
360      window.addEventListener("resize", handleResize);
361      handleResize();
362    }
363  };
364
365  // --- Color filter behavior ---
366  Drupal.behaviors.color_filter = {
367    attach: function (context) {
368      $('.filter-section', context).each(function () {
369        const $section = $(this);
370        const $color_options = $section.find('.color-option');
371        const $title = $section.find('.filter-title');
372
373        if (!$color_options.length || !$title.length) return;
374        const initial_selected = $color_options.filter('.selected').length;
375        const base_text = $title.text().replace(/\s*\(\d+\)$/, '');
376        if (initial_selected > 0) {
377          $title.text(`${base_text} (${initial_selected})`);
378        }
379        $color_options.off('click.colorFilter').on('click.colorFilter', function () {
380          const $option = $(this);
381          $option.toggleClass('selected');
382          const selected_count = $section.find('.color-option.selected').length;
383          const base_text = $title.text().replace(/\s*\(\d+\)$/, '');
384          if (selected_count > 0) {
385            $title.text(`${base_text} (${selected_count})`);
386          } else {
387            $title.text(base_text);
388          }
389        });
390      });
391    }
392  };
393
394  // --- Brand filter counter ---
395  Drupal.behaviors.brand_filter = {
396    attach: function (context) {
397      $('.filter-section', context).each(function () {
398        const $section = $(this);
399        const $check_boxes = $section.find('input[type="checkbox"]');
400        const $title = $section.find('.filter-title');
401
402        if (!$check_boxes.length || !$title.length) return;
403        $check_boxes.off('change.brandFilter').on('change.brandFilter', function () {
404          const checked_count = $check_boxes.filter(':checked').length;
405          const base_text = $title.text().replace(/\s*\(\d+\)$/, '');
406          $title.text(checked_count > 0 ? `${base_text} (${checked_count})` : base_text);
407        });
408        $check_boxes.each(function () {
409          $(this).trigger('change.brandFilter');
410        });
411      });
412    }
413  };
414
415  // --- Energy filter counter ---
416  Drupal.behaviors.energy_filter = {
417    attach: function (context) {
418      $('.filter-section.energy-filter', context).each(function () {
419        const $section = $(this);
420        if ($section.data('energyFilterAttached')) {
421          return;
422        }
423        $section.data('energyFilterAttached', true);
424        const $check_boxes = $section.find('input[type="checkbox"]');
425        const $title = $section.find('.filter-title');
426        if (!$check_boxes.length || !$title.length) return;
427
428        $check_boxes.on('change.energyFilter', function () {
429          const checked_count = $check_boxes.filter(':checked').length;
430          const base_text = $title.text().replace(/\s*\(\d+\)$/, '');
431          $title.text(checked_count > 0 ? `${base_text} (${checked_count})` : base_text);
432        });
433        $check_boxes.trigger('change.energyFilter');
434      });
435    }
436  };
437
438  $('#more-filter-home').click(function (e) {
439    $.fn.show_search_modal(e, 'new-advanced-search');
440  });
441
442})(jQuery, Drupal);

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.