PageSourceSearch

https://www.humanizeai.io/assets/js/Dropdown.js

js humanizeai.io collected 2026-09-25 18:38:33 UTC 8,798 bytes, 245 lines download raw bytes

1class Dropdown {
2    constructor(container, options = {}) {
3        this.container = typeof container === 'string' ? document.querySelector(container) : container;
4        this.options = {
5            name: 'dropdown',
6            defaultValue: null,
7            placeholder: 'Select an option',
8            items: [],
9            onChange: null,
10            ...options
11        };
12
13        this.init();
14    }
15
16    init() {
17        this.render();
18        this.bindEvents();
19        this.setInitialValue();
20    }
21
22    render() {
23        const { name, placeholder, items } = this.options;
24
25        this.container.innerHTML = `
26            <div class="minitools-custom-dropdown">
27                <div class="minitools-custom-dropdown-toggle" tabindex="0">
28                    <span class="selected-text">${placeholder}</span>
29                    <div class="minitools-custom-dropdown-arrow"></div>
30                </div>
31                <div class="minitools-custom-dropdown-menu">
32                    ${items.map(item => `
33                        <button class="minitools-custom-dropdown-item" data-value="${item.value}">
34                            ${item.text}
35                            <span class="check-icon">
36<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
37<path d="M6.48272 12C6.30863 12.0007 6.14119 11.9272 6.01619 11.795L2.86781 8.46198C2.80505 8.39514 2.75498 8.31547 2.72045 8.22752C2.68592 8.13957 2.6676 8.04506 2.66655 7.94939C2.66443 7.75618 2.73274 7.56995 2.85648 7.43169C2.98021 7.29343 3.14921 7.21445 3.32632 7.21213C3.50343 7.20981 3.67412 7.28434 3.80086 7.41933L6.48539 10.2601L12.1983 4.20704C12.3253 4.07206 12.4961 3.9976 12.6733 4.00006C12.8506 4.00251 13.0197 4.08168 13.1434 4.22013C13.2671 4.35859 13.3354 4.545 13.3331 4.73835C13.3309 4.9317 13.2583 5.11616 13.1314 5.25115L6.94925 11.795C6.82425 11.9272 6.65681 12.0007 6.48272 12Z" fill="#8D8D8D"/>
38</svg>
39</span>
40                        </button>
41                    `).join('')}
42                </div>
43                <select name="${name}" class="form-select w-auto" style="display: none;">
44                    ${items.map(item => `
45                        <option value="${item.value}">${item.text}</option>
46                    `).join('')}
47                </select>
48            </div>
49        `;
50
51        // Cache DOM elements
52        this.dropdown = this.container.querySelector('.minitools-custom-dropdown');
53        this.toggle = this.dropdown.querySelector('.minitools-custom-dropdown-toggle');
54        this.menu = this.dropdown.querySelector('.minitools-custom-dropdown-menu');
55        this.selectedText = this.dropdown.querySelector('.selected-text');
56        this.hiddenSelect = this.dropdown.querySelector('select');
57        this.items = this.dropdown.querySelectorAll('.minitools-custom-dropdown-item');
58    }
59
60    bindEvents() {
61        // Toggle dropdown
62        this.toggle.addEventListener('click', () => this.toggleDropdown());
63
64        // Keyboard support for toggle
65        this.toggle.addEventListener('keydown', (e) => {
66            if (e.key === 'Enter' || e.key === ' ') {
67                e.preventDefault();
68                this.toggleDropdown();
69            } else if (e.key === 'ArrowDown') {
70                e.preventDefault();
71                if (!this.dropdown.classList.contains('open')) {
72                    this.toggleDropdown();
73                }
74                const firstItem = this.items[0];
75                if (firstItem) firstItem.focus();
76            }
77        });
78
79        // Item selection
80        this.items.forEach((item, index) => {
81            item.addEventListener('click', (e) => {
82                e.preventDefault();
83                this.selectItem(item);
84            });
85
86            // Keyboard navigation for items
87            item.addEventListener('keydown', (e) => {
88                switch (e.key) {
89                    case 'ArrowDown':
90                        e.preventDefault();
91                        const nextItem = this.items[index + 1];
92                        if (nextItem) nextItem.focus();
93                        break;
94                    case 'ArrowUp':
95                        e.preventDefault();
96                        const prevItem = this.items[index - 1];
97                        if (prevItem) {
98                            prevItem.focus();
99                        } else {
100                            this.toggle.focus();
101                        }
102                        break;
103                    case 'Enter':
104                    case ' ':
105                        e.preventDefault();
106                        this.selectItem(item);
107                        this.toggle.focus();
108                        break;
109                    case 'Escape':
110                        this.closeDropdown();
111                        this.toggle.focus();
112                        break;
113                }
114            });
115        });
116
117        // Close on outside click
118        document.addEventListener('click', (e) => {
119            if (!this.dropdown.contains(e.target)) {
120                this.closeDropdown();
121            }
122        });
123
124        // Recalculate direction on window resize/scroll
125        window.addEventListener('resize', () => {
126            if (this.dropdown.classList.contains('open')) {
127                this.checkDropdownDirection();
128            }
129        });
130        
131        window.addEventListener('scroll', () => {
132            if (this.dropdown.classList.contains('open')) {
133                this.checkDropdownDirection();
134            }
135        });
136    }
137
138    toggleDropdown() {
139        const isOpening = !this.dropdown.classList.contains('open');
140        
141        if (isOpening) {
142            this.dropdown.classList.add('open');
143            // Check direction after opening to ensure proper positioning
144            this.checkDropdownDirection();
145        } else {
146            this.closeDropdown();
147        }
148    }
149
150    closeDropdown() {
151        this.dropdown.classList.remove('open');
152        this.dropdown.classList.remove('open-upward');
153    }
154
155    selectItem(item) {
156        const value = item.dataset.value;
157        const text = item.textContent.trim();
158
159        // Update visual selection
160        this.items.forEach(i => i.classList.remove('selected'));
161        item.classList.add('selected');
162
163        // Update displayed text
164        this.selectedText.textContent = text;
165
166        // Update hidden select value
167        this.hiddenSelect.value = value;
168
169        // Trigger change event
170        const changeEvent = new Event('change', { bubbles: true });
171        this.hiddenSelect.dispatchEvent(changeEvent);
172
173        // Call custom onChange if provided
174        if (this.options.onChange) {
175            this.options.onChange(value, text);
176        }
177
178        this.closeDropdown();
179    }
180
181    setInitialValue() {
182        const { defaultValue } = this.options;
183        if (defaultValue) {
184            const item = Array.from(this.items).find(item => item.dataset.value === defaultValue);
185            if (item) {
186                this.selectItem(item);
187            }
188        }
189    }
190
191    checkDropdownDirection() {
192        const rect = this.toggle.getBoundingClientRect();
193        const viewportHeight = window.innerHeight;
194
195        // Calculate available space below and above
196        const spaceBelow = viewportHeight - rect.bottom;
197        const spaceAbove = rect.top;
198
199        // Get menu height - we need to temporarily show it to measure
200        const wasVisible = this.menu.style.display !== 'none';
201        if (!wasVisible) {
202            // Temporarily show menu to measure its height
203            this.menu.style.visibility = 'hidden';
204            this.menu.style.display = 'block';
205        }
206        
207        const menuHeight = this.menu.getBoundingClientRect().height;
208        
209        if (!wasVisible) {
210            // Hide it again
211            this.menu.style.display = 'none';
212            this.menu.style.visibility = 'visible';
213        }
214
215        // Add some padding for safety
216        const padding = 20;
217
218        // Determine if dropdown should open upward
219        const shouldOpenUpward = (spaceBelow < menuHeight + padding) && (spaceAbove > menuHeight + padding);
220
221        // Apply or remove the upward class
222        if (shouldOpenUpward) {
223            this.dropdown.classList.add('open-upward');
224        } else {
225            this.dropdown.classList.remove('open-upward');
226        }
227    }
228
229    // Public methods
230    getValue() {
231        return this.hiddenSelect.value;
232    }
233
234    setValue(value) {
235        const item = Array.from(this.items).find(item => item.dataset.value === value);
236        if (item) {
237            this.selectItem(item);
238        }
239    }
240
241    destroy() {
242        // Remove event listeners and clean up
243        this.container.innerHTML = '';
244    }
245}

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.