196 lines
10 KiB
HTML
196 lines
10 KiB
HTML
<div class="relative w-full custom-select-container" data-multiple="{{ 'true' if multiple else 'false' }}"
|
|
data-placeholder="{{ placeholder|default('Select options') }}">
|
|
|
|
<!-- Display area -->
|
|
<button type="button"
|
|
class="toggle-btn w-full bg-gray-50 border border-gray-300 rounded-lg p-2.5 text-sm text-gray-900 flex justify-between items-center focus:ring-blue-500 focus:border-blue-500">
|
|
<span class="selected-label truncate border-none outline-none">
|
|
{% set ns = namespace(selected_count=0, selected_name='') %}
|
|
{% for option in options %}
|
|
{% set opt_selected = option.selected if option.selected is defined else (option.is_selected if
|
|
option.is_selected is defined else false) %}
|
|
{% if opt_selected %}
|
|
{% set ns.selected_count = ns.selected_count + 1 %}
|
|
{% set opt_name = option.name if option.name is defined else (option.tag_name if option.tag_name is defined
|
|
else (option.exercise_name if option.exercise_name is defined else option.label)) %}
|
|
{% set ns.selected_name = opt_name %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if multiple %}
|
|
<span class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">{{
|
|
ns.selected_count }}</span>
|
|
{{ placeholder|default('Select options') }}
|
|
{% else %}
|
|
{{ ns.selected_name if ns.selected_count > 0 else (placeholder|default('Select an option')) }}
|
|
{% endif %}
|
|
</span>
|
|
<svg class="w-4 h-4 ml-2 pointer-events-none" 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>
|
|
</button>
|
|
|
|
<!-- Hidden native select for HTMX/Form submission -->
|
|
<select name="{{ name }}" id="select-{{ name }}" {% if multiple %}multiple{% endif %} class="hidden native-select"
|
|
{% if hx_get %}hx-get="{{ hx_get }}" {% endif %} {% if hx_post %}hx-post="{{ hx_post }}" {% endif %} {% if
|
|
hx_include %}hx-include="{{ hx_include }}" {% endif %} {% if hx_target %}hx-target="{{ hx_target }}" {% endif %}
|
|
{% if hx_push_url is sameas true %}hx-push-url="true" {% elif hx_push_url %}hx-push-url="{{ hx_push_url }}" {%
|
|
endif %} {% if hx_vals %}hx-vals='{{ hx_vals|safe }}' {% endif %} {% if hx_swap %}hx-swap="{{ hx_swap }}" {%
|
|
endif %}>
|
|
{% for option in options %}
|
|
{% set opt_id = option.id if option.id is defined else (option.tag_id if option.tag_id is defined else
|
|
(option.exercise_id if option.exercise_id is defined else option.value)) %}
|
|
{% set opt_name = option.name if option.name is defined else (option.tag_name if option.tag_name is defined else
|
|
(option.exercise_name if option.exercise_name is defined else option.label)) %}
|
|
{% set opt_selected = option.selected if option.selected is defined else (option.is_selected if
|
|
option.is_selected is defined else false) %}
|
|
<option value="{{ opt_id }}" {% if opt_selected %}selected{% endif %}>{{ opt_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
|
|
<!-- Dropdown -->
|
|
<div
|
|
class="dropdown-menu hidden absolute z-40 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden">
|
|
{% if search %}
|
|
<div class="p-2 border-b bg-gray-50">
|
|
<input type="text" placeholder="Search..."
|
|
class="search-input w-full p-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
{% endif %}
|
|
<ul class="options-list max-h-60 overflow-y-auto p-1">
|
|
{% for option in options %}
|
|
{% set opt_id = option.id if option.id is defined else (option.tag_id if option.tag_id is defined else
|
|
(option.exercise_id if option.exercise_id is defined else option.value)) %}
|
|
{% set opt_name = option.name if option.name is defined else (option.tag_name if option.tag_name is defined
|
|
else (option.exercise_name if option.exercise_name is defined else option.label)) %}
|
|
{% set opt_selected = option.selected if option.selected is defined else (option.is_selected if
|
|
option.is_selected is defined else false) %}
|
|
<li class="option flex items-center p-2 hover:bg-gray-100 cursor-pointer rounded-md {% if opt_selected %}bg-blue-50{% endif %}"
|
|
data-index="{{ loop.index0 }}" data-value="{{ opt_id }}">
|
|
{% if multiple %}
|
|
<input type="checkbox" class="mr-2 pointer-events-none" {% if opt_selected %}checked{% endif %}>
|
|
{% endif %}
|
|
<span class="truncate opt-text">{{ opt_name }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
function initSelect(container) {
|
|
if (container.dataset.initialized) return;
|
|
container.dataset.initialized = 'true';
|
|
|
|
const toggleBtn = container.querySelector('.toggle-btn');
|
|
const dropdown = container.querySelector('.dropdown-menu');
|
|
const searchInput = container.querySelector('.search-input');
|
|
const nativeSelect = container.querySelector('.native-select');
|
|
const label = container.querySelector('.selected-label');
|
|
const options = container.querySelectorAll('.option');
|
|
const isMultiple = container.dataset.multiple === 'true';
|
|
const placeholder = container.dataset.placeholder;
|
|
|
|
function refreshUI() {
|
|
if (isMultiple) {
|
|
let count = 0;
|
|
for (let i = 0; i < nativeSelect.options.length; i++) {
|
|
if (nativeSelect.options[i].selected) count++;
|
|
}
|
|
label.innerHTML = `<span class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">${count}</span> ${placeholder}`;
|
|
} else {
|
|
const selectedOpt = nativeSelect.options[nativeSelect.selectedIndex];
|
|
label.textContent = selectedOpt ? selectedOpt.text : placeholder;
|
|
}
|
|
}
|
|
|
|
// Toggle dropdown
|
|
toggleBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const isOpen = dropdown.classList.toggle('hidden');
|
|
if (!isOpen) { // is now Open
|
|
container.style.zIndex = '50';
|
|
if (searchInput) searchInput.focus();
|
|
} else {
|
|
container.style.zIndex = '';
|
|
}
|
|
});
|
|
|
|
// Close on click outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!container.contains(e.target)) {
|
|
dropdown.classList.add('hidden');
|
|
container.style.zIndex = '';
|
|
}
|
|
});
|
|
|
|
// Search functionality
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
const val = e.target.value.toLowerCase();
|
|
options.forEach(opt => {
|
|
const text = opt.querySelector('.opt-text').textContent.toLowerCase();
|
|
opt.style.display = text.includes(val) ? 'flex' : 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
// Selection logic
|
|
options.forEach(opt => {
|
|
opt.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const index = parseInt(opt.dataset.index);
|
|
const nativeOption = nativeSelect.options[index];
|
|
|
|
if (isMultiple) {
|
|
const checkbox = opt.querySelector('input[type="checkbox"]');
|
|
const newState = !nativeOption.selected;
|
|
|
|
nativeOption.selected = newState;
|
|
if (newState) nativeOption.setAttribute('selected', '');
|
|
else nativeOption.removeAttribute('selected');
|
|
|
|
checkbox.checked = newState;
|
|
opt.classList.toggle('bg-blue-50', newState);
|
|
refreshUI();
|
|
} else {
|
|
// Single select
|
|
options.forEach(o => o.classList.remove('bg-blue-50'));
|
|
for (let i = 0; i < nativeSelect.options.length; i++) {
|
|
nativeSelect.options[i].selected = false;
|
|
nativeSelect.options[i].removeAttribute('selected');
|
|
}
|
|
|
|
nativeOption.selected = true;
|
|
nativeOption.setAttribute('selected', '');
|
|
opt.classList.add('bg-blue-50');
|
|
refreshUI();
|
|
dropdown.classList.add('hidden');
|
|
container.style.zIndex = '';
|
|
}
|
|
|
|
// Trigger HTMX
|
|
nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
});
|
|
|
|
// Initial UI refresh to match server state
|
|
refreshUI();
|
|
}
|
|
|
|
// Run on load and HTMX swaps
|
|
if (window.htmx) {
|
|
htmx.onLoad(function (content) {
|
|
if (!content) return;
|
|
const selects = content.querySelectorAll ? content.querySelectorAll('.custom-select-container') : [];
|
|
selects.forEach(initSelect);
|
|
if (content.classList && content.classList.contains('custom-select-container')) {
|
|
initSelect(content);
|
|
}
|
|
});
|
|
} else {
|
|
document.querySelectorAll('.custom-select-container').forEach(initSelect);
|
|
}
|
|
})();
|
|
</script> |