PageSourceSearch

https://www.francebatterie.com/js/search-cascade.js?v=utf8-20260612

js francebatterie.com collected 2026-10-08 08:09:11 UTC 27,682 bytes, 654 lines download raw bytes

1/**
2 * Système de select personnalisés avec recherche intégrée
3 * Remplace les selects natifs par des composants custom avec filtrage
4 * Version avec icônes et dropdown 2 colonnes
5 */
6
7// Icônes SVG pour les dispositifs
8const dispositifIcons = {
9    'ordinateur-portable': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M2 20h20"/><path d="M7 16v4M17 16v4"/></svg>`,
10    'aspirateur': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="18" r="3"/><path d="M11 18h6a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-4"/><path d="M8 6V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v11"/></svg>`,
11    'telephone': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="3"/><path d="M12 18h.01"/></svg>`,
12    'smartphone': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="3"/><path d="M12 18h.01"/></svg>`,
13    'gps': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.4 8 12 8 12s8-6.6 8-12a8 8 0 0 0-8-8z"/></svg>`,
14    'collier_de_chien': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><ellipse cx="12" cy="12" rx="9" ry="4"/><path d="M12 16v3"/><circle cx="12" cy="20" r="1"/></svg>`,
15    'talkie_walkie': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="6" y="4" width="12" height="18" rx="2"/><path d="M10 2v2M14 2v2"/><rect x="8" y="7" width="8" height="4" rx="1"/><circle cx="12" cy="16" r="2"/></svg>`,
16    'medical': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 3h6v6h6v6h-6v6H9v-6H3V9h6z"/></svg>`,
17    'enceinte': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="3"/><circle cx="12" cy="14" r="4"/><circle cx="12" cy="6" r="2"/></svg>`,
18    'alarme': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a7 7 0 0 0-7 7c0 7 7 13 7 13s7-6 7-13a7 7 0 0 0-7-7z"/><circle cx="12" cy="9" r="2.5"/><path d="M2 2l2 2M22 2l-2 2"/></svg>`,
19    'tablette': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M12 18h.01"/></svg>`,
20    'lecteur_code_barre': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 5v14"/><path d="M6 5v14"/><path d="M10 5v14"/><path d="M13 5v14"/><path d="M17 5v14"/><path d="M21 5v14"/><path d="M8 5v14" stroke-width="2.5"/><path d="M15 5v14" stroke-width="2.5"/></svg>`,
21    'montre': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="6"/><path d="M12 6V2M12 22v-4"/><path d="M9 2h6M9 22h6"/><path d="M12 9v3l2 2"/></svg>`,
22    'camescope': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-4v12l-6-4z"/></svg>`,
23    'telecommande_grue': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="2"/><circle cx="9" cy="8" r="1.5"/><circle cx="15" cy="8" r="1.5"/><rect x="8" y="13" width="8" height="6" rx="1"/></svg>`,
24    'telephone_sans_fil': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="7" y="4" width="10" height="18" rx="2"/><path d="M10 7h4"/><rect x="9" y="10" width="6" height="8" rx="1"/><path d="M12 2v2"/><path d="M8 2c-1 1-1.5 2-1.5 3M16 2c1 1 1.5 2 1.5 3"/></svg>`,
25    'drone': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="8" y="10" width="8" height="4" rx="1"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/><path d="M6 7l2 3M18 7l-2 3M6 17l2-3M18 17l-2-3"/></svg>`,
26    'babyphone': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="6" y="8" width="12" height="14" rx="3"/><circle cx="12" cy="15" r="3"/><path d="M9 4a3 3 0 0 1 6 0"/><path d="M7 2a5 5 0 0 1 10 0"/></svg>`,
27    'tpe_terminal_paiement': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="2"/><rect x="7" y="4" width="10" height="5" rx="1"/><path d="M8 12h2M11 12h2M14 12h2"/><path d="M8 15h2M11 15h2M14 15h2"/><path d="M8 18h8"/></svg>`,
28    'casque_sans_fil': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 14v-2a9 9 0 0 1 18 0v2"/><rect x="1" y="14" width="4" height="6" rx="1"/><rect x="19" y="14" width="4" height="6" rx="1"/></svg>`,
29    'cuisine': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2v4M12 18v4"/><rect x="4" y="6" width="16" height="12" rx="2"/><circle cx="8" cy="12" r="2"/><path d="M14 10h4M14 14h4"/></svg>`,
30    'default': `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg>`
31};
32
33// Données des dispositifs par type de produit
34const dispositifsByTypeProduit = {
35    'batterie': [
36        { value: 'ordinateur-portable', label: 'Ordinateur portable' },
37        { value: 'aspirateur', label: 'Aspirateur' },
38        { value: 'telephone', label: 'Téléphone' },
39        { value: 'gps', label: 'GPS' },
40        { value: 'collier_de_chien', label: 'Collier de chien' },
41        { value: 'talkie_walkie', label: 'Talkie-walkie' },
42        { value: 'medical', label: 'Matériel médical' },
43        { value: 'enceinte', label: 'Enceinte' },
44        { value: 'alarme', label: 'Alarme' },
45        { value: 'tablette', label: 'Tablette' },
46        { value: 'lecteur_code_barre', label: 'Lecteur code barre' },
47        { value: 'montre', label: 'Montre' },
48        { value: 'camescope', label: 'Caméscope / Appareil photo' },
49        { value: 'telecommande_grue', label: 'Télécommande grue' },
50        { value: 'telephone_sans_fil', label: 'Téléphone sans fil' },
51        { value: 'drone', label: 'Drone' },
52        { value: 'babyphone', label: 'Babyphone' },
53        { value: 'tpe_terminal_paiement', label: 'TPE / Terminal de paiement' },
54        { value: 'casque_sans_fil', label: 'Casque sans fil' },
55        { value: 'cuisine', label: 'Cuisine' }
56    ],
57    'chargeur': [
58        { value: 'ordinateur-portable', label: 'Ordinateur portable' },
59        { value: 'telephone', label: 'Téléphone' },
60        { value: 'smartphone', label: 'Smartphone' },
61        { value: 'tablette', label: 'Tablette' },
62        { value: 'camescope', label: 'Caméscope / Appareil photo' }
63    ]
64};
65
66/**
67 * Les marques sont calculées manuellement pour chaque "dispositifsByTypeProduit" 
68 * dans le script /ajax/generer_marques.php
69 */
70
71// Vérification des marques
72if (typeof marquesBatterieByDispositif === 'undefined' || typeof marquesChargeurByDispositif === 'undefined') {
73    console.error('Les données de marques ne sont pas chargées.');
74}
75
76// État global de l'application
77const appState = {
78    typeProduit: '',
79    dispositif: '',
80    marque: '',
81    modele: '',
82    modelesList: []
83};
84
85// Classe pour gérer un select personnalisé
86class CustomSelect {
87    constructor(id, options = {}) {
88        this.id = id;
89        this.container = document.getElementById(id);
90        this.name = options.name || id;
91        this.placeholder = options.placeholder || 'Sélectionner...';
92        this.searchable = options.searchable || false;
93        this.disabled = options.disabled || false;
94        this.onChange = options.onChange || null;
95        this.showOtherButton = options.showOtherButton || false;
96        this.otherButtonLabel = options.otherButtonLabel || 'Autre';
97        this.onOtherClick = options.onOtherClick || null;
98        this.showIcons = options.showIcons || false; // Nouvelle option pour les icônes
99        this.multiColumn = options.multiColumn || false; // Nouvelle option pour 2 colonnes
100        this.items = [];
101        this.selectedValue = '';
102        this.selectedLabel = '';
103        this.isOpen = false;
104        this.filteredItems = [];
105        
106        this.init();
107    }
108
109    init() {
110        this.render();
111        this.attachEvents();
112    }
113
114    render() {
115        const searchHTML = this.searchable ? `
116            <div class="custom-select__search">
117                <input 
118                    type="text" 
119                    class="custom-select__search-input" 
120                    placeholder="Rechercher..."
121                    autocomplete="off"
122                />
123                <svg class="custom-select__search-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
124                    <path d="M7 12C9.76142 12 12 9.76142 12 7C12 4.23858 9.76142 2 7 2C4.23858 2 2 4.23858 2 7C2 9.76142 4.23858 12 7 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
125                    <path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
126                </svg>
127            </div>
128        ` : '';
129
130        // Ajouter la classe multi-column si nécessaire
131        const multiColumnClass = this.multiColumn ? 'custom-select--multi-column' : '';
132        const iconsClass = this.showIcons ? 'custom-select--with-icons' : '';
133
134        this.container.innerHTML = `
135            <div class="custom-select ${this.disabled ? 'custom-select--disabled' : ''} ${multiColumnClass} ${iconsClass}" data-name="${this.name}">
136                <input type="hidden" name="${this.name}" value="${this.selectedValue}" />
137                <button type="button" class="custom-select__trigger" ${this.disabled ? 'disabled' : ''}>
138                    <span class="custom-select__value">${this.selectedLabel || this.placeholder}</span>
139                    <svg class="custom-select__arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">
140                        <path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
141                    </svg>
142                </button>
143                <div class="custom-select__dropdown">
144                    ${searchHTML}
145                    <div class="custom-select__options">
146                        <div class="custom-select__option custom-select__option--placeholder">
147                            ${this.placeholder}
148                        </div>
149                    </div>
150                </div>
151            </div>
152        `;
153
154        this.selectElement = this.container.querySelector('.custom-select');
155        this.trigger = this.container.querySelector('.custom-select__trigger');
156        this.valueDisplay = this.container.querySelector('.custom-select__value');
157        this.dropdown = this.container.querySelector('.custom-select__dropdown');
158        this.optionsContainer = this.container.querySelector('.custom-select__options');
159        this.hiddenInput = this.container.querySelector('input[type="hidden"]');
160        
161        if (this.searchable) {
162            this.searchInput = this.container.querySelector('.custom-select__search-input');
163        }
164    }
165
166    attachEvents() {
167        this.trigger.addEventListener('click', (e) => {
168            e.stopPropagation();
169            this.toggle();
170        });
171
172        if (this.searchable && this.searchInput) {
173            this.searchInput.addEventListener('input', (e) => {
174                this.filterOptions(e.target.value);
175            });
176
177            this.searchInput.addEventListener('click', (e) => {
178                e.stopPropagation();
179            });
180        }
181
182        document.addEventListener('click', (e) => {
183            if (!this.container.contains(e.target)) {
184                this.close();
185            }
186        });
187
188        this.trigger.addEventListener('keydown', (e) => {
189            if (e.key === 'Enter' || e.key === ' ') {
190                e.preventDefault();
191                this.toggle();
192            } else if (e.key === 'Escape') {
193                this.close();
194            }
195        });
196    }
197
198    toggle() {
199        if (this.disabled) return;
200        this.isOpen ? this.close() : this.open();
201    }
202
203    open() {
204        if (this.disabled) return;
205        
206        document.querySelectorAll('.custom-select').forEach(select => {
207            if (select !== this.selectElement) {
208                select.classList.remove('custom-select--open');
209            }
210        });
211
212        this.selectElement.classList.add('custom-select--open');
213        this.isOpen = true;
214
215        // Ajuster la position du dropdown pour éviter le débordement
216        this.adjustDropdownPosition();
217
218        if (this.searchable && this.searchInput) {
219            setTimeout(() => {
220                this.searchInput.focus();
221            }, 100);
222        }
223    }
224
225    /**
226     * Ajuste la position du dropdown pour qu'il ne déborde pas de l'écran
227     */
228    adjustDropdownPosition() {
229        if (!this.multiColumn) return;
230
231        // Reset des classes de position
232        this.selectElement.classList.remove('custom-select--align-right', 'custom-select--align-left');
233        this.dropdown.style.left = '';
234        this.dropdown.style.right = '';
235
236        // Attendre que le dropdown soit rendu
237        requestAnimationFrame(() => {
238            const triggerRect = this.trigger.getBoundingClientRect();
239            const dropdownRect = this.dropdown.getBoundingClientRect();
240            const viewportWidth = window.innerWidth;
241            const margin = 20; // Marge de sécurité
242
243            // Vérifier si le dropdown déborde à droite
244            if (triggerRect.left + dropdownRect.width > viewportWidth - margin) {
245                // Aligner à droite du trigger
246                this.dropdown.style.left = 'auto';
247                this.dropdown.style.right = '0';
248                
249                // Si ça déborde encore, décaler vers la gauche
250                const newRight = viewportWidth - triggerRect.right;
251                if (newRight + dropdownRect.width > viewportWidth - margin) {
252                    const offset = (triggerRect.left + dropdownRect.width) - (viewportWidth - margin);
253                    this.dropdown.style.right = 'auto';
254                    this.dropdown.style.left = `-${offset}px`;
255                }
256            } else {
257                // Aligner à gauche normalement
258                this.dropdown.style.left = '0';
259                this.dropdown.style.right = 'auto';
260            }
261        });
262    }
263
264    close() {
265        this.selectElement.classList.remove('custom-select--open');
266        this.isOpen = false;
267        
268        if (this.searchable && this.searchInput) {
269            this.searchInput.value = '';
270            this.filterOptions('');
271        }
272    }
273
274    setItems(items) {
275        this.items = items;
276        this.filteredItems = [...items];
277        this.renderOptions();
278    }
279
280    filterOptions(searchTerm) {
281        const term = searchTerm.toLowerCase().trim();
282        
283        if (!term) {
284            this.filteredItems = [...this.items];
285        } else {
286            this.filteredItems = this.items.filter(item => 
287                item.label.toLowerCase().includes(term)
288            );
289        }
290        
291        this.renderOptions();
292    }
293
294    getIcon(value) {
295        return dispositifIcons[value] || dispositifIcons['default'];
296    }
297
298    /**
299     * Formate le texte avec une majuscule initiale et le reste en minuscule
300     * @param {string} text - Le texte à formater
301     * @returns {string} - Le texte formaté
302     */
303    formatLabel(text) {
304        if (!text) return text;
305        return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
306    }
307
308    renderOptions() {
309        let optionsHTML = '';
310        
311        if (this.filteredItems.length === 0) {
312            optionsHTML = `
313                <div class="custom-select__option custom-select__option--empty">
314                    Le résultat n'apparait pas ici ? Cliquez sur "Autre" pour voir la liste complète.
315                </div>
316            `;
317        } else {
318            optionsHTML = this.filteredItems.map(item => {
319                const iconHTML = this.showIcons 
320                    ? `<span class="custom-select__option-icon">${this.getIcon(item.value)}</span>` 
321                    : '';
322                
323                const displayLabel = this.formatLabel(item.label);
324                
325                return `
326                    <div class="custom-select__option ${item.value === this.selectedValue ? 'custom-select__option--selected' : ''}" 
327                         data-value="${item.value}">
328                        ${iconHTML}
329                        <span class="custom-select__option-label">${displayLabel}</span>
330                    </div>
331                `;
332            }).join('');
333        }
334
335        // Toujours afficher le bouton "Autre" s'il est activé
336        if (this.showOtherButton) {
337            const otherIcon = this.showIcons 
338                ? `<span class="custom-select__option-icon">
339                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
340                        <circle cx="12" cy="12" r="10"/>
341                        <path d="M12 8v8M8 12h8"/>
342                    </svg>
343                   </span>` 
344                : '';
345            
346            optionsHTML += `
347                <div class="custom-select__option custom-select__option--other" data-action="other">
348                    ${otherIcon}
349                    <span class="custom-select__option-label">${this.otherButtonLabel}</span>
350                </div>
351            `;
352        }
353
354        this.optionsContainer.innerHTML = optionsHTML;
355
356        this.optionsContainer.querySelectorAll('.custom-select__option').forEach(option => {
357            option.addEventListener('click', (e) => {
358                const target = e.target.closest('.custom-select__option');
359                const value = target.dataset.value;
360                const action = target.dataset.action;
361                
362                if (action === 'other') {
363                    if (this.onOtherClick) {
364                        this.onOtherClick();
365                    }
366                    this.close();
367                } else if (value) {
368                    this.selectOption(value);
369                }
370            });
371        });
372    }
373
374    selectOption(value) {
375        const item = this.items.find(i => i.value === value);
376        if (!item) return;
377
378        this.selectedValue = value;
379        this.selectedLabel = item.label;
380        
381        const displayLabel = this.formatLabel(item.label);
382        
383        // Afficher l'icône dans le trigger si showIcons est activé
384        if (this.showIcons) {
385            this.valueDisplay.innerHTML = `
386                <span class="custom-select__value-icon">${this.getIcon(value)}</span>
387                <span>${displayLabel}</span>
388            `;
389        } else {
390            this.valueDisplay.textContent = displayLabel;
391        }
392        
393        this.hiddenInput.value = value;
394
395        this.optionsContainer.querySelectorAll('.custom-select__option').forEach(opt => {
396            opt.classList.toggle('custom-select__option--selected', opt.dataset.value === value);
397        });
398
399        this.close();
400
401        if (this.onChange) {
402            this.onChange(value, item);
403        }
404    }
405
406    reset(placeholder) {
407        this.selectedValue = '';
408        this.selectedLabel = '';
409        this.valueDisplay.textContent = placeholder || this.placeholder;
410        this.hiddenInput.value = '';
411        this.items = [];
412        this.filteredItems = [];
413        this.renderOptions();
414    }
415
416    setDisabled(disabled) {
417        this.disabled = disabled;
418        this.trigger.disabled = disabled;
419        this.selectElement.classList.toggle('custom-select--disabled', disabled);
420        if (disabled) {
421            this.close();
422        }
423    }
424
425    getValue() {
426        return this.selectedValue;
427    }
428}
429
430// Instances des selects personnalisés
431let selectTypeProduit, selectDispositif, selectMarque, selectModele;
432
433// Initialisation
434document.addEventListener('DOMContentLoaded', function() {
435    // Type de produit - simple, 1 colonne
436    selectTypeProduit = new CustomSelect('select_type_produit', {
437        name: 'type_produit',
438        placeholder: '-- Sélectionnez un type --',
439        searchable: false,
440        onChange: handleTypeProduitChange
441    });
442
443    // Dispositif - avec icônes et 2 colonnes
444    selectDispositif = new CustomSelect('select_dispositif', {
445        name: 'dispositif',
446        placeholder: '-- Choisissez d\'abord un type --',
447        searchable: false,
448        disabled: true,
449        showOtherButton: true,
450        otherButtonLabel: 'Autre',
451        onOtherClick: handleDispositifOther,
452        onChange: handleDispositifChange,
453        showIcons: true,      // Active les icônes
454        multiColumn: true     // Active le mode 2 colonnes
455    });
456
457    // Marque - 2 colonnes, sans icônes
458    selectMarque = new CustomSelect('select_marque', {
459        name: 'marque',
460        placeholder: '-- Choisissez d\'abord un dispositif --',
461        searchable: true,
462        disabled: true,
463        showOtherButton: true,
464        otherButtonLabel: 'Autre',
465        onOtherClick: handleMarqueOther,
466        onChange: handleMarqueChange,
467        multiColumn: true     // Active le mode 2 colonnes
468    });
469
470    // Modèle - 2 colonnes, sans icônes
471    selectModele = new CustomSelect('select_modele', {
472        name: 'id_modele',
473        placeholder: '-- Choisissez d\'abord une marque --',
474        searchable: true,
475        disabled: true,
476        showOtherButton: true,
477        otherButtonLabel: 'Autre',
478        onOtherClick: handleModeleOther,
479        onChange: handleModeleChange,
480        multiColumn: true     // Active le mode 2 colonnes
481    });
482
483    // Charger les types de produits
484    selectTypeProduit.setItems([
485        { value: 'batterie', label: 'Batterie' },
486        { value: 'chargeur', label: 'Chargeur' }
487    ]);
488});
489
490/**
491 * Gère le clic sur "Autre" pour le dispositif
492 */
493function handleDispositifOther() {
494    const typeProduit = appState.typeProduit;
495    
496    if (typeProduit === 'batterie') {
497        window.location.href = '/batterie.html';
498    } else if (typeProduit === 'chargeur') {
499        window.location.href = '/chargeur.html';
500    }
501}
502
503/**
504 * Gère le clic sur "Autre" pour la marque
505 */
506function handleMarqueOther() {
507    const typeProduit = appState.typeProduit;
508    const dispositif = appState.dispositif;
509    const marque = appState.marque;
510    
511    if (typeProduit && dispositif && marque) {
512        const dispositifSlug = dispositif.replace(/_/g, '-');
513        const marqueParts = marque.split('_');
514        const marqueSlug = marqueParts[marqueParts.length - 1].toLowerCase();
515        window.location.href = `/${typeProduit}/${dispositifSlug}/${marqueSlug}.html`;
516    } else if (typeProduit && dispositif) {
517        const dispositifSlug = dispositif.replace(/_/g, '-');
518        window.location.href = `/${typeProduit}/${dispositifSlug}.html`;
519    }
520}
521
522/**
523 * Gère le clic sur "Autre" pour le modèle
524 */
525function handleModeleOther() {
526    const typeProduit = appState.typeProduit;
527    const dispositif = appState.dispositif;
528    const marque = appState.marque;
529    
530    if (typeProduit && dispositif && marque) {
531        const dispositifSlug = dispositif.replace(/_/g, '-');
532        const marqueParts = marque.split('_');
533        const marqueSlug = marqueParts[marqueParts.length - 1].toLowerCase();
534        window.location.href = `/${typeProduit}/${dispositifSlug}/${marqueSlug}.html`;
535    }
536}
537
538/**
539 * Gère le changement de type de produit
540 */
541function handleTypeProduitChange(value) {
542    appState.typeProduit = value;
543
544    selectDispositif.reset('-- Sélectionnez un dispositif --');
545    selectMarque.reset('-- Choisissez d\'abord un dispositif --');
546    selectModele.reset('-- Choisissez d\'abord une marque --');
547    
548    selectMarque.setDisabled(true);
549    selectModele.setDisabled(true);
550
551    if (!value) {
552        selectDispositif.setDisabled(true);
553        return;
554    }
555
556    const dispositifs = dispositifsByTypeProduit[value] || [];
557    
558    if (dispositifs.length > 0) {
559        selectDispositif.setItems(dispositifs);
560        selectDispositif.setDisabled(false);
561    } else {
562        selectDispositif.setDisabled(true);
563    }
564}
565
566/**
567 * Gère le changement de dispositif
568 */
569function handleDispositifChange(value) {
570    appState.dispositif = value;
571
572    selectMarque.reset('-- Sélectionnez une marque --');
573    selectModele.reset('-- Choisissez d\'abord une marque --');
574    selectModele.setDisabled(true);
575
576    if (!value) {
577        selectMarque.setDisabled(true);
578        return;
579    }
580
581    const marquesDict = appState.typeProduit === 'chargeur' 
582        ? marquesChargeurByDispositif 
583        : marquesBatterieByDispositif;
584
585    const marques = marquesDict[value] || [];
586
587    if (marques.length > 0) {
588        selectMarque.setItems(marques);
589        selectMarque.setDisabled(false);
590    } else {
591        selectMarque.setDisabled(true);
592        console.warn(`Aucune marque trouvée pour ${appState.typeProduit} / ${value}`);
593    }
594}
595
596/**
597 * Gère le changement de marque
598 */
599function handleMarqueChange(value) {
600    appState.marque = value;
601
602    selectModele.reset('-- Chargement des modèles... --');
603    selectModele.setDisabled(true);
604
605    if (!value) {
606        selectModele.reset('-- Choisissez d\'abord une marque --');
607        return;
608    }
609
610    fetchModeles(value);
611}
612
613/**
614 * Gère le changement de modèle
615 */
616function handleModeleChange(value) {
617    appState.modele = value;
618    
619    if (value) {
620        window.location.href = value;
621    }
622}
623
624/**
625 * Récupère les modèles depuis le serveur
626 */
627function fetchModeles(marque) {
628    const lang = 'fr';
629    const url = `/ajax/get_modeles.php?type_article=${encodeURIComponent(marque)}&lang=${lang}`;
630
631    fetch(url)
632        .then(response => {
633            if (!response.ok) {
634                throw new Error(`HTTP error! status: ${response.status}`);
635            }
636            return response.json();
637        })
638        .then(data => {
639            if (data.success && data.modeles && data.modeles.length > 0) {
640                appState.modelesList = data.modeles;
641                selectModele.setItems(data.modeles);
642                selectModele.setDisabled(false);
643            } else {
644                selectModele.reset('-- Aucun modèle disponible --');
645                selectModele.setDisabled(true);
646                console.warn(`Aucun modèle trouvé pour ${marque}`);
647            }
648        })
649        .catch(error => {
650            console.error('Erreur lors de la récupération des modèles:', error);
651            selectModele.reset('-- Erreur de chargement --');
652            selectModele.setDisabled(true);
653        });
654}

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.