Dropdown - JavaScript
Dropdown
Struktur HTML
Section titled “Struktur HTML”SelectDropdown
Section titled “SelectDropdown”
<div class="ina-select-dropdown">
<button class="ina-select-dropdown__trigger">
<span class="ina-select-dropdown__trigger-text">Select...</span>
<svg class="ina-select-dropdown__trigger-icon">...</svg>
</button>
<div class="ina-select-dropdown__panel">
<div class="ina-select-dropdown__search">
<input type="text" class="ina-select-dropdown__search-input" placeholder="Cari data" />
</div>
<div class="ina-select-dropdown__options">
<button class="ina-select-dropdown__option">Option 1</button>
<button class="ina-select-dropdown__option">Option 2</button>
</div>
</div>
</div>BasicDropdown
Section titled “BasicDropdown”
<div class="ina-basic-dropdown">
<div class="ina-basic-dropdown__trigger">
<button type="button" class="ina-button ina-button--primary ina-button--md">
Open Menu
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div class="ina-basic-dropdown__panel ina-basic-dropdown__panel--bottom-end">
<div style="padding: 16px;">
<h3 style="margin: 0 0 8px; font-size: 14px; font-weight: 600;">My Content</h3>
<p style="margin: 0; font-size: 14px; color: #666;">This is a basic dropdown content.</p>
</div>
</div>
</div>Contoh Penggunaan
Section titled “Contoh Penggunaan”Contoh 1 - SelectDropdown: Basic Single Select
Section titled “Contoh 1 - SelectDropdown: Basic Single Select”<!-- Single Select with Search -->
<div class="ina-select-dropdown ina-select-dropdown--size-md" data-searchable="true">
<div class="ina-select-dropdown__trigger">
<input type="text" class="ina-select-dropdown__trigger-input" placeholder="Select Person..." />
<svg
class="ina-select-dropdown__trigger-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<div class="ina-select-dropdown__panel" style="display: none;">
<div class="ina-select-dropdown__options">
<button type="button" class="ina-select-dropdown__option" data-value="John Doe"
>John Doe</button
>
<button type="button" class="ina-select-dropdown__option" data-value="Jane Smith"
>Jane Smith</button
>
<button type="button" class="ina-select-dropdown__option" data-value="Alice Johnson"
>Alice Johnson</button
>
<button type="button" class="ina-select-dropdown__option" data-value="Bob Brown"
>Bob Brown</button
>
</div>
</div>
</div>
<script is:inline>
document.querySelectorAll('.ina-select-dropdown').forEach((dropdown) => {
const trigger = dropdown.querySelector('.ina-select-dropdown__trigger');
const panel = dropdown.querySelector('.ina-select-dropdown__panel');
const input = dropdown.querySelector('.ina-select-dropdown__trigger-input');
const options = dropdown.querySelectorAll('.ina-select-dropdown__option');
if (!trigger || !panel) return;
let isOpen = false;
const toggle = (force) => {
isOpen = force !== undefined ? force : !isOpen;
if (isOpen) {
// Clear display style to let CSS (flex) take over
panel.style.display = '';
if (input) input.focus();
} else {
panel.style.display = 'none';
}
trigger.setAttribute('aria-expanded', isOpen);
};
trigger.addEventListener('click', (e) => {
if (e.target !== input) toggle();
else if (!isOpen) toggle(true);
});
options.forEach((opt) => {
opt.addEventListener('click', (e) => {
e.stopPropagation();
const value = opt.getAttribute('data-value');
const text = opt.textContent.trim();
if (input) input.value = text;
toggle(false);
// Visual selection state
options.forEach((o) => o.classList.remove('ina-select-dropdown__option--selected-single'));
opt.classList.add('ina-select-dropdown__option--selected-single');
});
});
if (input) {
input.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
if (!isOpen) toggle(true);
options.forEach((opt) => {
const text = opt.textContent.trim().toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
});
}
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) toggle(false);
});
});
</script>Contoh 2 - SelectDropdown: Multiple Select
Section titled “Contoh 2 - SelectDropdown: Multiple Select”<div
class="ina-select-dropdown ina-select-dropdown--size-md"
data-multiple="true"
data-searchable="true"
>
<div class="ina-select-dropdown__trigger">
<input type="text" class="ina-select-dropdown__trigger-input" placeholder="Select People..." />
<svg
class="ina-select-dropdown__trigger-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<div class="ina-select-dropdown__panel" style="display: none;">
<div class="ina-select-dropdown__options">
<button type="button" class="ina-select-dropdown__option" data-value="orange">
<span class="ina-select-dropdown__option-label" style="flex: 1;">Orange</span>
<div class="ina-select-dropdown__option-checkbox">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="tabler-icon tabler-icon-check"
style="display: none;"><path d="M5 12l5 5l10 -10"></path></svg
>
</div>
</button>
<button type="button" class="ina-select-dropdown__option" data-value="apple">
<span class="ina-select-dropdown__option-label" style="flex: 1;">Apple</span>
<div class="ina-select-dropdown__option-checkbox">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="tabler-icon tabler-icon-check"
style="display: none;"><path d="M5 12l5 5l10 -10"></path></svg
>
</div>
</button>
<button type="button" class="ina-select-dropdown__option" data-value="banana">
<span class="ina-select-dropdown__option-label" style="flex: 1;">Banana</span>
<div class="ina-select-dropdown__option-checkbox">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="tabler-icon tabler-icon-check"
style="display: none;"><path d="M5 12l5 5l10 -10"></path></svg
>
</div>
</button>
</div>
</div>
</div>
<script is:inline>
document.querySelectorAll('.ina-select-dropdown[data-multiple="true"]').forEach((dropdown) => {
const trigger = dropdown.querySelector('.ina-select-dropdown__trigger');
const panel = dropdown.querySelector('.ina-select-dropdown__panel');
const input = dropdown.querySelector('.ina-select-dropdown__trigger-input');
const options = dropdown.querySelectorAll('.ina-select-dropdown__option');
let selectedValues = [];
const toggle = (force) => {
// Check if display is NONE to determine open state fallback
const currentDisplay = panel.style.display;
const currentState = currentDisplay !== 'none';
const isOpen = force !== undefined ? force : !currentState;
if (isOpen) {
panel.style.display = ''; // Let CSS take over (flex)
if (input) input.focus();
} else {
panel.style.display = 'none';
}
};
const updateTrigger = () => {
if (selectedValues.length === 0) {
input.placeholder = 'Select People...';
input.value = '';
} else if (selectedValues.length > 3) {
input.placeholder = `${selectedValues.length} data terpilih`;
input.value = '';
} else {
// Show labels (capitalized values for demo)
const labels = selectedValues.map((v) => v.charAt(0).toUpperCase() + v.slice(1));
input.placeholder = labels.join(', ');
input.value = '';
}
};
trigger.addEventListener('click', (e) => {
if (e.target !== input) toggle();
else toggle(true);
});
options.forEach((opt) => {
opt.addEventListener('click', (e) => {
e.stopPropagation();
const value = opt.getAttribute('data-value');
const checkbox = opt.querySelector('.ina-select-dropdown__option-checkbox');
const icon = checkbox ? checkbox.querySelector('svg') : null;
if (selectedValues.includes(value)) {
selectedValues = selectedValues.filter((v) => v !== value);
opt.classList.remove('ina-select-dropdown__option--selected-multiple');
if (checkbox) checkbox.classList.remove('ina-select-dropdown__option-checkbox--checked');
if (icon) icon.style.display = 'none';
} else {
selectedValues.push(value);
opt.classList.add('ina-select-dropdown__option--selected-multiple');
if (checkbox) checkbox.classList.add('ina-select-dropdown__option-checkbox--checked');
if (icon) icon.style.display = 'block';
}
updateTrigger();
});
});
if (input) {
input.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
// Force open if typing
if (panel.style.display === 'none') toggle(true);
options.forEach((opt) => {
const text = opt.textContent.trim().toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
});
}
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) toggle(false);
});
});
</script>Contoh 3 - SelectDropdown: Radio Indicator
Section titled “Contoh 3 - SelectDropdown: Radio Indicator” Select One...
<div class="ina-select-dropdown ina-select-dropdown--size-md">
<div class="ina-select-dropdown__trigger">
<span
class="ina-select-dropdown__trigger-text ina-select-dropdown__trigger-text--placeholder"
data-placeholder="Select One...">Select One...</span
>
<svg
class="ina-select-dropdown__trigger-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<div class="ina-select-dropdown__panel" style="display: none;">
<div class="ina-select-dropdown__options">
<button type="button" class="ina-select-dropdown__option" data-value="yes">
<span class="ina-select-dropdown__option-label" style="flex: 1;">Yes</span>
<div class="ina-select-dropdown__option-radio">
<div class="ina-select-dropdown__option-radio-dot" style="display: none;"></div>
</div>
</button>
<button type="button" class="ina-select-dropdown__option" data-value="no">
<span class="ina-select-dropdown__option-label" style="flex: 1;">No</span>
<div class="ina-select-dropdown__option-radio">
<div class="ina-select-dropdown__option-radio-dot" style="display: none;"></div>
</div>
</button>
</div>
</div>
</div>
<script is:inline>
document
.querySelectorAll('.ina-select-dropdown:not([data-multiple="true"])')
.forEach((dropdown) => {
// Skip if it has input (handled by basic example), check for trigger text span logic
const triggerText = dropdown.querySelector('.ina-select-dropdown__trigger-text');
if (!triggerText) return;
const trigger = dropdown.querySelector('.ina-select-dropdown__trigger');
const panel = dropdown.querySelector('.ina-select-dropdown__panel');
const options = dropdown.querySelectorAll('.ina-select-dropdown__option');
let isOpen = false;
const toggle = (force) => {
isOpen = force !== undefined ? force : !isOpen;
if (isOpen) {
panel.style.display = '';
} else {
panel.style.display = 'none';
}
};
trigger.addEventListener('click', (e) => {
e.stopPropagation();
toggle();
});
options.forEach((opt) => {
opt.addEventListener('click', (e) => {
e.stopPropagation();
const value = opt.getAttribute('data-value');
const text = opt.querySelector('.ina-select-dropdown__option-label').textContent;
triggerText.textContent = text;
triggerText.classList.remove('ina-select-dropdown__trigger-text--placeholder');
options.forEach((o) => {
o.classList.remove('ina-select-dropdown__option--selected-single');
// Reset radio state
const radio = o.querySelector('.ina-select-dropdown__option-radio');
if (radio) {
radio.classList.remove('ina-select-dropdown__option-radio--checked');
const dot = radio.querySelector('.ina-select-dropdown__option-radio-dot');
if (dot) dot.style.display = 'none'; // Ensure hidden if JS logic is mixed, but CSS should handle it via checked class usually.
// To match previous request strictly:
// The user showed: class="... option-radio--checked" > ...-dot
}
});
opt.classList.add('ina-select-dropdown__option--selected-single');
const radio = opt.querySelector('.ina-select-dropdown__option-radio');
if (radio) {
radio.classList.add('ina-select-dropdown__option-radio--checked');
// For simple JS example without full CSS dependency assumption, we can force block too if needed,
// but relying on class is cleaner if CSS exists.
// Based on user snippet, simply adding the class should be enough if CSS covers it.
// Let's add display block just in case to be safe as per previous patterns.
const dot = radio.querySelector('.ina-select-dropdown__option-radio-dot');
if (dot) dot.style.display = 'block';
}
toggle(false);
});
});
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) toggle(false);
});
});
</script>Contoh 4 - SelectDropdown: API Integration
Section titled “Contoh 4 - SelectDropdown: API Integration”Debug Info:
<div style="padding: 20px; max-width: 400px">
<div
class="ina-select-dropdown ina-select-dropdown--size-md"
id="select-dropdown-infinite-scroll"
style="width: 100%"
>
<button
type="button"
class="ina-select-dropdown__trigger ina-select-dropdown__trigger--size-md ina-select-dropdown__trigger--status-neutral"
id="select-dropdown-infinite-scroll-trigger"
>
<span
class="ina-select-dropdown__trigger-text ina-select-dropdown__trigger-text--placeholder"
id="select-dropdown-infinite-scroll-text"
>
Pilih produk...
</span>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="ina-select-dropdown__trigger-icon"
id="select-dropdown-infinite-scroll-chevron"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div
class="ina-select-dropdown__panel"
id="select-dropdown-infinite-scroll-panel"
style="display: none; width: 100%"
>
<div class="ina-select-dropdown__search">
<input
type="text"
class="ina-select-dropdown__search-input"
id="select-dropdown-infinite-scroll-search"
placeholder="Cari data"
aria-label="Search"
/>
</div>
<!-- Selected Preview Section -->
<div
class="ina-select-dropdown__preview"
id="select-dropdown-infinite-scroll-preview"
style="display: none"
>
<div class="ina-select-dropdown__preview-content">
<div
class="ina-select-dropdown__preview-item ina-select-dropdown__preview-item--single"
id="select-dropdown-infinite-scroll-preview-item"
>
<span
class="ina-select-dropdown__preview-item-text"
id="select-dropdown-infinite-scroll-preview-text"></span>
<button
type="button"
class="ina-select-dropdown__preview-remove ina-select-dropdown__preview-remove--single"
id="select-dropdown-infinite-scroll-preview-remove"
aria-label="Remove selected"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
</div>
<div
class="ina-select-dropdown__options"
id="select-dropdown-infinite-scroll-options"
style="max-height: 300px; overflow-y: auto"
>
<!-- Options will be populated by JavaScript -->
</div>
<!-- Load More Button -->
<!-- Load More Button Removed -->
<!-- Loading indicator -->
<div
class="ina-select-dropdown__loading"
id="select-dropdown-infinite-scroll-loading"
style="display: none"
>
<div class="ina-select-dropdown__loading-spinner">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="animation: spin 1s linear infinite"
>
<line x1="12" y1="2" x2="12" y2="6"></line>
<line x1="12" y1="18" x2="12" y2="22"></line>
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
<line x1="2" y1="12" x2="6" y2="12"></line>
<line x1="18" y1="12" x2="22" y2="12"></line>
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
</svg>
</div>
<span class="ina-select-dropdown__loading-text">Loading...</span>
</div>
</div>
</div>
<hr style="padding: 16px 0; width: 100%" />
<div
id="select-dropdown-infinite-scroll-debug"
style="
margin-top: 16px;
padding: 12px;
background-color: #f3f4f6;
border-radius: 8px;
font-size: 14px;
"
>
<p style="margin: 0 0 8px 0; font-weight: 600">Debug Info:</p>
<ul id="select-dropdown-infinite-scroll-debug-list" style="margin: 0; padding-left: 20px">
<!-- Debug info will be populated by JavaScript -->
</ul>
</div>
</div>
<script is:inline>
function initSelectDropdownInfinite() {
console.log('Initializing Select Dropdown Infinite...');
const dropdowns = document.querySelectorAll('.ina-select-dropdown');
dropdowns.forEach((container) => {
if (!container.id.includes('infinite')) return;
if (container.hasAttribute('data-initialized')) return;
const trigger = container.querySelector('.ina-select-dropdown__trigger');
const panel = container.querySelector('.ina-select-dropdown__panel');
const optionsContainer = container.querySelector('.ina-select-dropdown__options');
const triggerText = container.querySelector('.ina-select-dropdown__trigger-text');
const searchInput = container.querySelector('.ina-select-dropdown__search-input');
const chevron = container.querySelector('.ina-select-dropdown__trigger-icon');
const previewContainer = container.querySelector('.ina-select-dropdown__preview');
const previewText = container.querySelector('.ina-select-dropdown__preview-item-text');
const previewRemoveBtn = container.querySelector('.ina-select-dropdown__preview-remove');
const loadingIndicator = container.querySelector('.ina-select-dropdown__loading');
// Navigate to debug list. HTML structure: Dropdown -> HR -> Debug
const debugList = container.parentElement.querySelector('ul');
if (
!trigger ||
!panel ||
!optionsContainer ||
!triggerText ||
!previewContainer ||
!previewText ||
!previewRemoveBtn
)
return;
container.setAttribute('data-initialized', 'true');
let options = [];
let selectedValue = null;
let selectedRaw = null;
let isOpen = false;
let currentPage = 1;
let hasMore = false;
let loading = false;
let searchTerm = '';
let searchTimeout = null;
let total = 0;
const updatePreview = () => {
if (!previewContainer || !previewText) return;
if (selectedValue !== null && selectedRaw) {
previewText.textContent = selectedRaw.label;
previewContainer.style.display = 'block';
if (previewRemoveBtn) {
previewRemoveBtn.setAttribute('aria-label', `Remove ${selectedRaw.label}`);
}
} else {
previewContainer.style.display = 'none';
}
};
const updateDebugInfo = () => {
if (!debugList) return;
debugList.innerHTML = `
<li>Current Page: ${currentPage}</li>
<li>Has More: ${hasMore ? 'Yes' : 'No'}</li>
<li>Loading: ${loading ? 'Yes' : 'No'}</li>
<li>Options: ${options.length} / ${total}</li>
<li>Search Term: "${searchTerm}"</li>
<li>Selected: ${
selectedRaw ? `${selectedRaw.label} (ID: ${selectedRaw.value})` : 'None'
}</li>
`;
};
const loadData = async (page, search, append = false) => {
if (loading) return;
loading = true;
if (loadingIndicator && append) {
// Show loading indicator only when appending (infinite scroll)
// ideally for initial load we might want a different state or same,
// here we use the bottom loader for simplicity
loadingIndicator.style.display = 'flex';
} else if (loadingIndicator && !append) {
// Maybe clear options to show loading state if full refresh?
// For now keeping simple
loadingIndicator.style.display = 'flex';
}
try {
const limit = 10;
const skip = (page - 1) * limit;
const trimmedSearch = search.trim();
let apiUrl;
if (trimmedSearch) {
apiUrl = `https://dummyjson.com/products/search?q=${encodeURIComponent(
trimmedSearch
)}&limit=${limit}&skip=${skip}`;
} else {
apiUrl = `https://dummyjson.com/products?limit=${limit}&skip=${skip}`;
}
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const newOptions = data.products.map((product) => ({
label: product.title,
value: product.id,
}));
if (append) {
options = [...options, ...newOptions];
} else {
options = newOptions;
}
const totalLoaded = page * limit;
hasMore = totalLoaded < data.total;
currentPage = page;
searchTerm = search;
total = data.total;
renderOptions();
updateDebugInfo();
} catch (error) {
console.error('Error loading data from API:', error);
if (!append) {
options = [];
}
hasMore = false;
renderOptions();
updateDebugInfo();
} finally {
loading = false;
if (loadingIndicator) {
loadingIndicator.style.display = 'none';
}
}
};
const renderOptions = () => {
const html = options
.map((option) => {
const isSelected = option.value === selectedValue;
return `
<button
type="button"
class="ina-select-dropdown__option ${
isSelected ? 'ina-select-dropdown__option--selected-single' : ''
}"
data-value="${option.value}"
data-label="${option.label}"
>
<div class="ina-select-dropdown__option-content">
<span class="ina-select-dropdown__option-label">${option.label}</span>
</div>
${
isSelected
? `
<div class="ina-select-dropdown__option-check-indicator">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="ina-select-dropdown__option-check-icon"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
`
: ''
}
</button>
`;
})
.join('');
optionsContainer.innerHTML = html;
optionsContainer.querySelectorAll('button[data-value]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const value = parseInt(btn.getAttribute('data-value'));
const label = btn.getAttribute('data-label');
handleSelect(value, label);
});
});
};
const handleSelect = (value, label) => {
if (value === selectedValue) {
selectedValue = null;
selectedRaw = null;
triggerText.textContent = 'Pilih produk...';
triggerText.classList.add('ina-select-dropdown__trigger-text--placeholder');
triggerText.classList.remove('ina-select-dropdown__trigger-text--selected');
} else {
selectedValue = value;
selectedRaw = { label, value };
triggerText.textContent = label;
triggerText.classList.remove('ina-select-dropdown__trigger-text--placeholder');
triggerText.classList.add('ina-select-dropdown__trigger-text--selected');
console.log('Selected Raw:', selectedRaw);
}
updatePreview();
isOpen = false;
panel.style.display = 'none';
if (chevron instanceof SVGElement) {
chevron.style.transform = 'rotate(0deg)';
}
renderOptions();
updateDebugInfo();
};
const handleRemoveSelected = () => {
if (selectedValue === null) return;
selectedValue = null;
selectedRaw = null;
triggerText.textContent = 'Pilih produk...';
triggerText.classList.add('ina-select-dropdown__trigger-text--placeholder');
triggerText.classList.remove('ina-select-dropdown__trigger-text--selected');
updatePreview();
renderOptions();
updateDebugInfo();
};
const handleSearch = (search) => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
currentPage = 1;
// Scroll top when searching
optionsContainer.scrollTop = 0;
loadData(1, search, false);
}, 500);
};
const handleScroll = (e) => {
const target = e.target;
// Check if scrolled near bottom (e.g., 50px buffer)
if (target.scrollHeight - target.scrollTop - target.clientHeight < 50) {
if (hasMore && !loading) {
loadData(currentPage + 1, searchTerm, true);
}
}
};
const toggleDropdown = () => {
isOpen = !isOpen;
if (isOpen) {
panel.style.display = 'flex';
if (chevron instanceof SVGElement) {
chevron.style.transform = 'rotate(180deg)';
}
updatePreview();
if (searchInput) {
setTimeout(() => searchInput.focus(), 50);
}
} else {
panel.style.display = 'none';
if (chevron instanceof SVGElement) {
chevron.style.transform = 'rotate(0deg)';
}
if (searchInput) {
searchInput.value = '';
}
}
};
const handleClickOutside = (e) => {
if (!container.contains(e.target) && isOpen) {
isOpen = false;
panel.style.display = 'none';
if (chevron instanceof SVGElement) {
chevron.style.transform = 'rotate(0deg)';
}
if (searchInput) {
searchInput.value = '';
}
}
};
const handleEscape = (e) => {
if (e.key === 'Escape' && isOpen) {
isOpen = false;
panel.style.display = 'none';
if (chevron instanceof SVGElement) {
chevron.style.transform = 'rotate(0deg)';
}
if (searchInput) {
searchInput.value = '';
}
}
};
// Initialize
updatePreview();
updateDebugInfo();
loadData(1, '', false);
// Event listeners
trigger.addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown();
});
optionsContainer.addEventListener('scroll', handleScroll);
if (previewRemoveBtn) {
previewRemoveBtn.addEventListener('click', (e) => {
e.stopPropagation();
handleRemoveSelected();
});
}
if (searchInput) {
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
searchInput.addEventListener('click', (e) => e.stopPropagation());
}
panel.addEventListener('click', (e) => {
e.stopPropagation();
});
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
});
}
document.addEventListener('DOMContentLoaded', initSelectDropdownInfinite);
document.addEventListener('astro:page-load', initSelectDropdownInfinite);
</script>Contoh 5 - Dropdown: Basic Menu
Section titled “Contoh 5 - Dropdown: Basic Menu”Basic Dropdown Content
This allows any custom content inside.
<div class="ina-basic-dropdown">
<div class="ina-basic-dropdown__trigger">
<button type="button" class="ina-basic-dropdown__trigger-button">
<span class="ina-basic-dropdown__trigger-content">Pilih Opsi</span>
<svg
class="ina-basic-dropdown__trigger-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
<div
class="ina-basic-dropdown__panel ina-basic-dropdown__panel--bottom-start"
style="display: none;"
>
<div style="padding: 1rem;">
<p style="margin: 0; color: #1f1f1f; font-weight: 500;">Basic Dropdown Content</p>
<p style="margin-top: 0.5rem; color: #525252; font-size: 0.875rem;">
This allows any custom content inside.
</p>
</div>
</div>
</div>
<script is:inline>
document.querySelectorAll('.ina-basic-dropdown').forEach((dropdown) => {
const trigger = dropdown.querySelector('.ina-basic-dropdown__trigger');
const panel = dropdown.querySelector('.ina-basic-dropdown__panel');
const btn = trigger.querySelector('.ina-basic-dropdown__trigger-button');
if (!trigger || !panel) return;
let isOpen = false;
const toggle = (force) => {
isOpen = force !== undefined ? force : !isOpen;
if (isOpen) {
// Clear inline display style so CSS takes over (block)
panel.style.display = '';
if (btn) btn.classList.add('ina-basic-dropdown__trigger-button--open');
} else {
panel.style.display = 'none';
if (btn) btn.classList.remove('ina-basic-dropdown__trigger-button--open');
}
};
trigger.addEventListener('click', (e) => {
e.stopPropagation();
toggle();
});
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) {
toggle(false);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
toggle(false);
}
});
});
</script>