1172 lines
40 KiB
HTML
1172 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dropdown Enhanced</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
min-height: 100vh;
|
|
padding: 2rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
main {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
color: #2c3e50;
|
|
margin-bottom: 3rem;
|
|
font-size: 2.5rem;
|
|
font-weight: 300;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.component-showcase {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.dropdown-container {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.dropdown-label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
color: #4a5568;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.025em;
|
|
}
|
|
|
|
.dropdown {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.dropdown-trigger {
|
|
width: 100%;
|
|
min-height: 48px;
|
|
padding: 0.75rem 3rem 0.75rem 1rem;
|
|
background: white;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
font-size: 1rem;
|
|
color: #2d3748;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dropdown-trigger:hover {
|
|
border-color: #cbd5e0;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.dropdown-trigger:focus {
|
|
outline: none;
|
|
border-color: #4299e1;
|
|
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15);
|
|
}
|
|
|
|
.dropdown.open .dropdown-trigger {
|
|
border-color: #4299e1;
|
|
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15);
|
|
}
|
|
|
|
.dropdown-arrow {
|
|
position: absolute;
|
|
right: 1rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
pointer-events: none;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.dropdown.open .dropdown-arrow {
|
|
transform: translateY(-50%) rotate(180deg);
|
|
}
|
|
|
|
.dropdown-arrow svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
color: #718096;
|
|
}
|
|
|
|
.dropdown-placeholder {
|
|
color: #a0aec0;
|
|
}
|
|
|
|
.dropdown-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.25rem 0.5rem;
|
|
background: #edf2f7;
|
|
border-radius: 6px;
|
|
font-size: 0.875rem;
|
|
color: #4a5568;
|
|
animation: tagFadeIn 0.2s ease;
|
|
}
|
|
|
|
@keyframes tagFadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.8);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
.dropdown-tag-remove {
|
|
cursor: pointer;
|
|
padding: 0.125rem;
|
|
margin-left: 0.25rem;
|
|
border-radius: 3px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.dropdown-tag-remove:hover {
|
|
background: #cbd5e0;
|
|
}
|
|
|
|
.dropdown-tag-remove svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
position: absolute;
|
|
top: calc(100% + 0.5rem);
|
|
left: 0;
|
|
right: 0;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12);
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
pointer-events: none;
|
|
transition: all 0.2s ease;
|
|
max-height: 320px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.dropdown.open .dropdown-menu {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.dropdown-menu.position-top {
|
|
top: auto;
|
|
bottom: calc(100% + 0.5rem);
|
|
}
|
|
|
|
.dropdown-search {
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
position: sticky;
|
|
top: 0;
|
|
background: white;
|
|
z-index: 10;
|
|
}
|
|
|
|
.dropdown-search-input {
|
|
width: 100%;
|
|
padding: 0.625rem 2.5rem 0.625rem 1rem;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
font-size: 0.875rem;
|
|
transition: all 0.2s ease;
|
|
background: #f7fafc;
|
|
}
|
|
|
|
.dropdown-search-input:focus {
|
|
outline: none;
|
|
border-color: #4299e1;
|
|
background: white;
|
|
}
|
|
|
|
.dropdown-search-icon {
|
|
position: absolute;
|
|
right: 1.75rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
pointer-events: none;
|
|
color: #a0aec0;
|
|
}
|
|
|
|
.dropdown-search-icon svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.dropdown-options {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #cbd5e0 transparent;
|
|
}
|
|
|
|
.dropdown-options::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.dropdown-options::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.dropdown-options::-webkit-scrollbar-thumb {
|
|
background-color: #cbd5e0;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.dropdown-group {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.dropdown-group:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.dropdown-group-header {
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #718096;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
user-select: none;
|
|
}
|
|
|
|
.dropdown-option {
|
|
padding: 0.75rem 1rem;
|
|
margin: 0.125rem 0;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dropdown-option:hover {
|
|
background: #f7fafc;
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.dropdown-option.focused {
|
|
background: #edf2f7;
|
|
box-shadow: inset 0 0 0 2px #4299e1;
|
|
}
|
|
|
|
.dropdown-option.selected {
|
|
background: #e6f4ff;
|
|
color: #2b6cb0;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dropdown-option.disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.dropdown-option-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dropdown-option-icon svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.dropdown-option-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.dropdown-option-text {
|
|
font-size: 0.9375rem;
|
|
color: #2d3748;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.dropdown-option-description {
|
|
font-size: 0.8125rem;
|
|
color: #718096;
|
|
margin-top: 0.125rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.dropdown-option-checkbox {
|
|
position: absolute;
|
|
right: 1rem;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid #cbd5e0;
|
|
border-radius: 4px;
|
|
background: white;
|
|
transition: all 0.15s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dropdown-option.selected .dropdown-option-checkbox {
|
|
background: #4299e1;
|
|
border-color: #4299e1;
|
|
}
|
|
|
|
.dropdown-option-checkbox svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: white;
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.dropdown-option.selected .dropdown-option-checkbox svg {
|
|
opacity: 1;
|
|
}
|
|
|
|
.dropdown-loading {
|
|
padding: 2rem;
|
|
text-align: center;
|
|
color: #718096;
|
|
}
|
|
|
|
.dropdown-loading-spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
margin: 0 auto 1rem;
|
|
border: 3px solid #e2e8f0;
|
|
border-top-color: #4299e1;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.dropdown-no-results {
|
|
padding: 2rem;
|
|
text-align: center;
|
|
color: #718096;
|
|
}
|
|
|
|
.dropdown-footer {
|
|
padding: 0.75rem 1rem;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 0.8125rem;
|
|
color: #718096;
|
|
background: #f7fafc;
|
|
text-align: center;
|
|
}
|
|
|
|
.keyboard-hint {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.125rem 0.375rem;
|
|
background: #e2e8f0;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
color: #4a5568;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.dropdown-menu {
|
|
position: fixed;
|
|
left: 1rem;
|
|
right: 1rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
max-height: 80vh;
|
|
}
|
|
|
|
.dropdown.open .dropdown-menu {
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.dropdown-menu.position-top {
|
|
top: 50%;
|
|
bottom: auto;
|
|
}
|
|
}
|
|
|
|
/* Highlight animation for search matches */
|
|
.highlight {
|
|
background: linear-gradient(to right, transparent 0%, #fef3c7 20%, #fef3c7 80%, transparent 100%);
|
|
padding: 0 2px;
|
|
border-radius: 2px;
|
|
animation: highlightPulse 1s ease infinite;
|
|
}
|
|
|
|
@keyframes highlightPulse {
|
|
0%, 100% { opacity: 0.7; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
/* Demo specific styles */
|
|
.demo-section {
|
|
margin-top: 3rem;
|
|
padding: 2rem;
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.demo-title {
|
|
font-size: 1.25rem;
|
|
color: #2d3748;
|
|
margin-bottom: 1.5rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.selected-values {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: #f7fafc;
|
|
border-radius: 8px;
|
|
font-size: 0.875rem;
|
|
color: #4a5568;
|
|
font-family: monospace;
|
|
word-break: break-all;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Dropdown - Enhanced</h1>
|
|
|
|
<div class="component-showcase">
|
|
<!-- Single Select Dropdown -->
|
|
<div class="dropdown-container">
|
|
<label class="dropdown-label">Choose your favorite framework</label>
|
|
<div class="dropdown" id="single-select">
|
|
<button class="dropdown-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
<span class="dropdown-placeholder">Select a framework...</span>
|
|
<span class="dropdown-arrow">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Multi Select Dropdown -->
|
|
<div class="dropdown-container">
|
|
<label class="dropdown-label">Select team members</label>
|
|
<div class="dropdown" id="multi-select">
|
|
<button class="dropdown-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
<span class="dropdown-placeholder">Select team members...</span>
|
|
<span class="dropdown-arrow">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Async Loading Dropdown -->
|
|
<div class="dropdown-container">
|
|
<label class="dropdown-label">Search for a country</label>
|
|
<div class="dropdown" id="async-select">
|
|
<button class="dropdown-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
<span class="dropdown-placeholder">Type to search countries...</span>
|
|
<span class="dropdown-arrow">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="demo-section">
|
|
<h2 class="demo-title">Selected Values</h2>
|
|
<div class="selected-values" id="selected-values">No selections yet</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Enhanced Dropdown Class
|
|
class EnhancedDropdown {
|
|
constructor(element, options = {}) {
|
|
this.element = element;
|
|
this.trigger = element.querySelector('.dropdown-trigger');
|
|
this.isOpen = false;
|
|
this.isMultiple = options.multiple || false;
|
|
this.searchable = options.searchable !== false;
|
|
this.loadData = options.loadData || null;
|
|
this.options = options.options || [];
|
|
this.selectedValues = new Set();
|
|
this.focusedIndex = -1;
|
|
this.searchQuery = '';
|
|
this.loading = false;
|
|
this.loadingTimeout = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createMenu();
|
|
this.bindEvents();
|
|
this.render();
|
|
}
|
|
|
|
createMenu() {
|
|
this.menu = document.createElement('div');
|
|
this.menu.className = 'dropdown-menu';
|
|
this.menu.setAttribute('role', 'listbox');
|
|
|
|
if (this.searchable) {
|
|
const searchContainer = document.createElement('div');
|
|
searchContainer.className = 'dropdown-search';
|
|
searchContainer.innerHTML = `
|
|
<input type="text" class="dropdown-search-input" placeholder="Search..." aria-label="Search options">
|
|
<span class="dropdown-search-icon">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
</span>
|
|
`;
|
|
this.menu.appendChild(searchContainer);
|
|
this.searchInput = searchContainer.querySelector('.dropdown-search-input');
|
|
}
|
|
|
|
this.optionsContainer = document.createElement('div');
|
|
this.optionsContainer.className = 'dropdown-options';
|
|
this.menu.appendChild(this.optionsContainer);
|
|
|
|
const footer = document.createElement('div');
|
|
footer.className = 'dropdown-footer';
|
|
footer.innerHTML = `
|
|
Use <span class="keyboard-hint">↑↓</span> to navigate,
|
|
<span class="keyboard-hint">Enter</span> to select,
|
|
<span class="keyboard-hint">Esc</span> to close
|
|
`;
|
|
this.menu.appendChild(footer);
|
|
|
|
this.element.appendChild(this.menu);
|
|
}
|
|
|
|
bindEvents() {
|
|
// Trigger events
|
|
this.trigger.addEventListener('click', () => this.toggle());
|
|
this.trigger.addEventListener('keydown', (e) => this.handleTriggerKeydown(e));
|
|
|
|
// Search events
|
|
if (this.searchInput) {
|
|
this.searchInput.addEventListener('input', (e) => this.handleSearch(e.target.value));
|
|
this.searchInput.addEventListener('keydown', (e) => this.handleSearchKeydown(e));
|
|
}
|
|
|
|
// Click outside to close
|
|
document.addEventListener('click', (e) => {
|
|
if (!this.element.contains(e.target)) {
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
// Window resize for positioning
|
|
window.addEventListener('resize', () => {
|
|
if (this.isOpen) this.updatePosition();
|
|
});
|
|
}
|
|
|
|
toggle() {
|
|
this.isOpen ? this.close() : this.open();
|
|
}
|
|
|
|
async open() {
|
|
this.isOpen = true;
|
|
this.element.classList.add('open');
|
|
this.trigger.setAttribute('aria-expanded', 'true');
|
|
this.updatePosition();
|
|
|
|
if (this.searchInput) {
|
|
setTimeout(() => this.searchInput.focus(), 100);
|
|
}
|
|
|
|
if (this.loadData && this.options.length === 0) {
|
|
await this.loadOptions();
|
|
}
|
|
|
|
this.focusedIndex = this.findFirstSelectedIndex();
|
|
this.updateFocus();
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.element.classList.remove('open');
|
|
this.trigger.setAttribute('aria-expanded', 'false');
|
|
this.searchQuery = '';
|
|
if (this.searchInput) {
|
|
this.searchInput.value = '';
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
updatePosition() {
|
|
const rect = this.trigger.getBoundingClientRect();
|
|
const menuHeight = this.menu.offsetHeight;
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
const spaceAbove = rect.top;
|
|
|
|
if (spaceBelow < menuHeight && spaceAbove > spaceBelow) {
|
|
this.menu.classList.add('position-top');
|
|
} else {
|
|
this.menu.classList.remove('position-top');
|
|
}
|
|
}
|
|
|
|
async loadOptions() {
|
|
this.loading = true;
|
|
this.render();
|
|
|
|
try {
|
|
clearTimeout(this.loadingTimeout);
|
|
this.loadingTimeout = setTimeout(async () => {
|
|
const data = await this.loadData(this.searchQuery);
|
|
this.options = data;
|
|
this.loading = false;
|
|
this.render();
|
|
}, 300);
|
|
} catch (error) {
|
|
console.error('Error loading options:', error);
|
|
this.loading = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
handleSearch(query) {
|
|
this.searchQuery = query.toLowerCase();
|
|
this.focusedIndex = -1;
|
|
|
|
if (this.loadData) {
|
|
this.loadOptions();
|
|
} else {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
handleTriggerKeydown(e) {
|
|
switch (e.key) {
|
|
case 'Enter':
|
|
case ' ':
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
this.open();
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
this.open();
|
|
this.focusedIndex = this.getFilteredOptions().length - 1;
|
|
this.updateFocus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
handleSearchKeydown(e) {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
this.moveFocus(1);
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
this.moveFocus(-1);
|
|
break;
|
|
case 'Enter':
|
|
e.preventDefault();
|
|
this.selectFocusedOption();
|
|
break;
|
|
case 'Escape':
|
|
e.preventDefault();
|
|
this.close();
|
|
this.trigger.focus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
moveFocus(direction) {
|
|
const filtered = this.getFilteredOptions();
|
|
const maxIndex = filtered.length - 1;
|
|
|
|
this.focusedIndex += direction;
|
|
|
|
if (this.focusedIndex < 0) {
|
|
this.focusedIndex = maxIndex;
|
|
} else if (this.focusedIndex > maxIndex) {
|
|
this.focusedIndex = 0;
|
|
}
|
|
|
|
this.updateFocus();
|
|
}
|
|
|
|
updateFocus() {
|
|
const options = this.optionsContainer.querySelectorAll('.dropdown-option:not(.disabled)');
|
|
options.forEach((option, index) => {
|
|
if (index === this.focusedIndex) {
|
|
option.classList.add('focused');
|
|
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
} else {
|
|
option.classList.remove('focused');
|
|
}
|
|
});
|
|
}
|
|
|
|
selectFocusedOption() {
|
|
const filtered = this.getFilteredOptions();
|
|
if (this.focusedIndex >= 0 && this.focusedIndex < filtered.length) {
|
|
const option = filtered[this.focusedIndex];
|
|
this.selectOption(option);
|
|
}
|
|
}
|
|
|
|
selectOption(option) {
|
|
if (option.disabled) return;
|
|
|
|
if (this.isMultiple) {
|
|
if (this.selectedValues.has(option.value)) {
|
|
this.selectedValues.delete(option.value);
|
|
} else {
|
|
this.selectedValues.add(option.value);
|
|
}
|
|
this.render();
|
|
this.updateTrigger();
|
|
this.updateDemoValues();
|
|
} else {
|
|
this.selectedValues.clear();
|
|
this.selectedValues.add(option.value);
|
|
this.updateTrigger();
|
|
this.updateDemoValues();
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
removeTag(value, event) {
|
|
event.stopPropagation();
|
|
this.selectedValues.delete(value);
|
|
this.updateTrigger();
|
|
this.updateDemoValues();
|
|
}
|
|
|
|
updateTrigger() {
|
|
const selected = this.options.filter(opt => this.selectedValues.has(opt.value));
|
|
|
|
if (selected.length === 0) {
|
|
this.trigger.innerHTML = `
|
|
<span class="dropdown-placeholder">${this.getPlaceholder()}</span>
|
|
${this.getArrowHtml()}
|
|
`;
|
|
} else if (this.isMultiple) {
|
|
const tags = selected.map(opt => `
|
|
<span class="dropdown-tag">
|
|
${opt.label}
|
|
<span class="dropdown-tag-remove" data-value="${opt.value}">
|
|
<svg fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</span>
|
|
</span>
|
|
`).join('');
|
|
|
|
this.trigger.innerHTML = tags + this.getArrowHtml();
|
|
|
|
// Bind remove events
|
|
this.trigger.querySelectorAll('.dropdown-tag-remove').forEach(btn => {
|
|
btn.addEventListener('click', (e) => this.removeTag(btn.dataset.value, e));
|
|
});
|
|
} else {
|
|
this.trigger.innerHTML = `
|
|
<span>${selected[0].label}</span>
|
|
${this.getArrowHtml()}
|
|
`;
|
|
}
|
|
}
|
|
|
|
getPlaceholder() {
|
|
return this.trigger.querySelector('.dropdown-placeholder')?.textContent || 'Select...';
|
|
}
|
|
|
|
getArrowHtml() {
|
|
return `
|
|
<span class="dropdown-arrow">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
getFilteredOptions() {
|
|
if (!this.searchQuery) return this.options;
|
|
|
|
return this.options.filter(option => {
|
|
const searchTarget = `${option.label} ${option.description || ''}`.toLowerCase();
|
|
return this.fuzzyMatch(searchTarget, this.searchQuery);
|
|
});
|
|
}
|
|
|
|
fuzzyMatch(str, pattern) {
|
|
let patternIdx = 0;
|
|
let strIdx = 0;
|
|
|
|
while (strIdx < str.length && patternIdx < pattern.length) {
|
|
if (str[strIdx] === pattern[patternIdx]) {
|
|
patternIdx++;
|
|
}
|
|
strIdx++;
|
|
}
|
|
|
|
return patternIdx === pattern.length;
|
|
}
|
|
|
|
highlightMatch(text, query) {
|
|
if (!query) return text;
|
|
|
|
const regex = new RegExp(`(${query.split('').join('.*?')})`, 'gi');
|
|
return text.replace(regex, '<span class="highlight">$1</span>');
|
|
}
|
|
|
|
findFirstSelectedIndex() {
|
|
const filtered = this.getFilteredOptions();
|
|
return filtered.findIndex(opt => this.selectedValues.has(opt.value));
|
|
}
|
|
|
|
render() {
|
|
if (this.loading) {
|
|
this.optionsContainer.innerHTML = `
|
|
<div class="dropdown-loading">
|
|
<div class="dropdown-loading-spinner"></div>
|
|
<div>Loading options...</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const filtered = this.getFilteredOptions();
|
|
|
|
if (filtered.length === 0) {
|
|
this.optionsContainer.innerHTML = `
|
|
<div class="dropdown-no-results">
|
|
<div>No results found</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const groups = this.groupOptions(filtered);
|
|
let html = '';
|
|
let optionIndex = 0;
|
|
|
|
for (const [group, options] of Object.entries(groups)) {
|
|
if (group !== 'undefined') {
|
|
html += `<div class="dropdown-group-header">${group}</div>`;
|
|
}
|
|
|
|
html += `<div class="dropdown-group">`;
|
|
|
|
for (const option of options) {
|
|
const isSelected = this.selectedValues.has(option.value);
|
|
const isFocused = optionIndex === this.focusedIndex;
|
|
|
|
html += `
|
|
<div class="dropdown-option ${isSelected ? 'selected' : ''} ${isFocused ? 'focused' : ''} ${option.disabled ? 'disabled' : ''}"
|
|
role="option"
|
|
aria-selected="${isSelected}"
|
|
data-value="${option.value}"
|
|
data-index="${optionIndex}">
|
|
${option.icon ? `
|
|
<span class="dropdown-option-icon">${option.icon}</span>
|
|
` : ''}
|
|
<div class="dropdown-option-content">
|
|
<div class="dropdown-option-text">${this.highlightMatch(option.label, this.searchQuery)}</div>
|
|
${option.description ? `
|
|
<div class="dropdown-option-description">${this.highlightMatch(option.description, this.searchQuery)}</div>
|
|
` : ''}
|
|
</div>
|
|
${this.isMultiple ? `
|
|
<span class="dropdown-option-checkbox">
|
|
<svg fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
optionIndex++;
|
|
}
|
|
|
|
html += `</div>`;
|
|
}
|
|
|
|
this.optionsContainer.innerHTML = html;
|
|
|
|
// Bind option click events
|
|
this.optionsContainer.querySelectorAll('.dropdown-option:not(.disabled)').forEach(option => {
|
|
option.addEventListener('click', () => {
|
|
const value = option.dataset.value;
|
|
const optionData = this.options.find(opt => opt.value === value);
|
|
this.selectOption(optionData);
|
|
});
|
|
|
|
option.addEventListener('mouseenter', () => {
|
|
this.focusedIndex = parseInt(option.dataset.index);
|
|
this.updateFocus();
|
|
});
|
|
});
|
|
}
|
|
|
|
groupOptions(options) {
|
|
const groups = {};
|
|
|
|
for (const option of options) {
|
|
const group = option.group || 'undefined';
|
|
if (!groups[group]) {
|
|
groups[group] = [];
|
|
}
|
|
groups[group].push(option);
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
updateDemoValues() {
|
|
const event = new CustomEvent('selectionChange', {
|
|
detail: {
|
|
dropdown: this.element.id,
|
|
values: Array.from(this.selectedValues)
|
|
}
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
// Framework options with icons
|
|
const frameworkOptions = [
|
|
{
|
|
value: 'react',
|
|
label: 'React',
|
|
description: 'A JavaScript library for building user interfaces',
|
|
icon: '⚛️',
|
|
group: 'Frontend Frameworks'
|
|
},
|
|
{
|
|
value: 'vue',
|
|
label: 'Vue.js',
|
|
description: 'The progressive JavaScript framework',
|
|
icon: '🟢',
|
|
group: 'Frontend Frameworks'
|
|
},
|
|
{
|
|
value: 'angular',
|
|
label: 'Angular',
|
|
description: 'Platform for building mobile and desktop apps',
|
|
icon: '🔺',
|
|
group: 'Frontend Frameworks'
|
|
},
|
|
{
|
|
value: 'svelte',
|
|
label: 'Svelte',
|
|
description: 'Cybernetically enhanced web apps',
|
|
icon: '🧡',
|
|
group: 'Frontend Frameworks'
|
|
},
|
|
{
|
|
value: 'express',
|
|
label: 'Express.js',
|
|
description: 'Fast, unopinionated, minimalist web framework',
|
|
icon: '🚂',
|
|
group: 'Backend Frameworks'
|
|
},
|
|
{
|
|
value: 'nextjs',
|
|
label: 'Next.js',
|
|
description: 'The React framework for production',
|
|
icon: '▲',
|
|
group: 'Full-Stack Frameworks'
|
|
},
|
|
{
|
|
value: 'nuxt',
|
|
label: 'Nuxt.js',
|
|
description: 'The intuitive Vue framework',
|
|
icon: '🟩',
|
|
group: 'Full-Stack Frameworks'
|
|
},
|
|
{
|
|
value: 'gatsby',
|
|
label: 'Gatsby',
|
|
description: 'Fast static site generator for React',
|
|
icon: '🟣',
|
|
group: 'Static Site Generators'
|
|
}
|
|
];
|
|
|
|
// Team member options
|
|
const teamOptions = [
|
|
{
|
|
value: 'john',
|
|
label: 'John Smith',
|
|
description: 'Frontend Developer',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
},
|
|
{
|
|
value: 'sarah',
|
|
label: 'Sarah Johnson',
|
|
description: 'UX Designer',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
},
|
|
{
|
|
value: 'mike',
|
|
label: 'Mike Chen',
|
|
description: 'Backend Engineer',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
},
|
|
{
|
|
value: 'emma',
|
|
label: 'Emma Wilson',
|
|
description: 'Product Manager',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
},
|
|
{
|
|
value: 'alex',
|
|
label: 'Alex Brown',
|
|
description: 'DevOps Engineer',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
},
|
|
{
|
|
value: 'lisa',
|
|
label: 'Lisa Davis',
|
|
description: 'QA Engineer',
|
|
icon: `<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path></svg>`
|
|
}
|
|
];
|
|
|
|
// Country loader simulation
|
|
const loadCountries = async (search) => {
|
|
// Simulate API delay
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
const countries = [
|
|
{ value: 'us', label: 'United States', description: 'North America' },
|
|
{ value: 'ca', label: 'Canada', description: 'North America' },
|
|
{ value: 'mx', label: 'Mexico', description: 'North America' },
|
|
{ value: 'uk', label: 'United Kingdom', description: 'Europe' },
|
|
{ value: 'de', label: 'Germany', description: 'Europe' },
|
|
{ value: 'fr', label: 'France', description: 'Europe' },
|
|
{ value: 'it', label: 'Italy', description: 'Europe' },
|
|
{ value: 'es', label: 'Spain', description: 'Europe' },
|
|
{ value: 'jp', label: 'Japan', description: 'Asia' },
|
|
{ value: 'cn', label: 'China', description: 'Asia' },
|
|
{ value: 'in', label: 'India', description: 'Asia' },
|
|
{ value: 'au', label: 'Australia', description: 'Oceania' },
|
|
{ value: 'nz', label: 'New Zealand', description: 'Oceania' },
|
|
{ value: 'br', label: 'Brazil', description: 'South America' },
|
|
{ value: 'ar', label: 'Argentina', description: 'South America' },
|
|
{ value: 'za', label: 'South Africa', description: 'Africa' },
|
|
{ value: 'eg', label: 'Egypt', description: 'Africa' }
|
|
];
|
|
|
|
if (search) {
|
|
return countries.filter(country =>
|
|
country.label.toLowerCase().includes(search.toLowerCase()) ||
|
|
country.description.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
}
|
|
|
|
return countries;
|
|
};
|
|
|
|
// Initialize dropdowns
|
|
const singleSelect = new EnhancedDropdown(
|
|
document.getElementById('single-select'),
|
|
{
|
|
options: frameworkOptions,
|
|
multiple: false
|
|
}
|
|
);
|
|
|
|
const multiSelect = new EnhancedDropdown(
|
|
document.getElementById('multi-select'),
|
|
{
|
|
options: teamOptions,
|
|
multiple: true
|
|
}
|
|
);
|
|
|
|
const asyncSelect = new EnhancedDropdown(
|
|
document.getElementById('async-select'),
|
|
{
|
|
loadData: loadCountries,
|
|
multiple: false
|
|
}
|
|
);
|
|
|
|
// Update demo values display
|
|
const selectedValuesDisplay = document.getElementById('selected-values');
|
|
const allSelections = {
|
|
'single-select': [],
|
|
'multi-select': [],
|
|
'async-select': []
|
|
};
|
|
|
|
document.addEventListener('selectionChange', (e) => {
|
|
allSelections[e.detail.dropdown] = e.detail.values;
|
|
|
|
const display = Object.entries(allSelections)
|
|
.filter(([_, values]) => values.length > 0)
|
|
.map(([dropdown, values]) => `${dropdown}: [${values.join(', ')}]`)
|
|
.join('\n');
|
|
|
|
selectedValuesDisplay.textContent = display || 'No selections yet';
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |