Remove dependency on tail select component and instead role my own to minmise bundle size

This commit is contained in:
Peter Stockings
2026-01-29 12:49:12 +11:00
parent 04fe00412a
commit 509d11443d
13 changed files with 309 additions and 581 deletions

View File

@@ -1,429 +0,0 @@
@charset "UTF-8";
/*
| tail.select - The vanilla solution to make your HTML select fields AWESOME!
| @file ./css/bootstrap4/tail.select-default.css
| @author SamBrishes <sam@pytes.net>
| @version 0.5.15 - Beta
|
| @website https://github.com/pytesNET/tail.select
| @license X11 / MIT License
| @copyright Copyright © 2014 - 2019 SamBrishes, pytesNET <info@pytes.net>
*/
/* @start GENERAL */
.tail-select, .tail-select *, .tail-select *:before, .tail-select *:after{
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.tail-select, .tail-select *{
outline: none;
user-select: none;
-o-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
.tail-select{
width: 100%;
margin: 1px 0px 0px 0px;
padding: 0px 8px 0px 0px;
display: inline-block;
position: relative;
font-size: 1rem;
line-height: 1.8;
border-radius: 0.5rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.tail-select mark{
color: white;
background-color: #0088CC;
}
.tail-select button{
outline: none;
}
.tail-select button.tail-all, .tail-select button.tail-none{
height: auto;
margin: 0 2px;
padding: 2px 6px;
display: inline-block;
font-size: 10px;
line-height: 14px;
text-shadow: none;
letter-spacing: 0;
text-transform: none;
vertical-align: top;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-radius: 3px;
box-shadow: none;
-webkit-box-shadow: none;
transition: color 142ms linear, border 142ms linear, background 142ms linear;
-webkit-transition: color 142ms linear, border 142ms linear, background 142ms linear;
}
.tail-select button.tail-all{
color: #AAAAAA;
border-color: #CCCCCC;
background-color: transparent;
}
.tail-select button.tail-all:hover{
color: #62C462;
border-color: #62C462;
background-color: transparent;
}
.tail-select button.tail-none{
color: #AAAAAA;
border-color: #CCCCCC;
background-color: transparent;
}
.tail-select button.tail-none:hover{
color: #EE5F5B;
border-color: #EE5F5B;
background-color: transparent;
}
.tail-select.disabled button.tail-all{
color: #CCCCCC;
border-color: #CCCCCC;
background-color: #F0F0F0;
}
.tail-select.disabled button.tail-none{
color: #CCCCCC;
border-color: #CCCCCC;
background-color: #F0F0F0;
}
.tail-select input[type="text"]{
color: #343a40;
width: 100%;
height: 30px;
margin: 0;
padding: 0.25rem 0.5rem;
display: inline-block;
font-size: 0.875rem;
line-height: 1.5;
vertical-align: middle;
background-color: transparent;
border-width: 1px;
border-style: solid;
border-color: #CCCCCC;
border-radius: 0.2rem;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
transition: border 142ms linear, box-shadow 142ms linear;
-webkit-transition: border 142ms linear, box-shadow 142ms linear;
}
.tail-select input[type="text"]:hover{
color: #343a40;
border-color: #b3b3b3;
background-color: transparent;
}
.tail-select input[type="text"]:focus{
color: #0088CC;
border-color: #0088CC;
background-color: transparent;
}
.tail-select.disabled input[type="text"]{
color: #e6e6e6;
border-color: #CCCCCC;
background-color: #e6e6e6;
}
.tail-select-container{
margin: 0;
padding: 3px;
text-align: left;
border-radius: 3px;
}
.tail-select-container .select-handle{
width: auto;
color: white;
cursor: pointer;
margin: 1px;
padding: 0.2em 0.6em 0.3em;
display: inline-block;
position: relative;
font-size: 11.844px;
text-align: left;
font-weight: bold;
line-height: 16px;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
vertical-align: top;
background-color: #0088CC;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 3px;
transition: background 142ms linear;
-webkit-transition: background 142ms linear;
}
.tail-select-container .select-handle:hover{
color: white;
background-color: #005580;
}
.tail-select-container.select-label .select-handle{
margin: 5px 3px;
}
/* @end GENERAL */
/* @start LABEL */
.tail-select .select-label{
cursor: pointer;
color: #343A40;
width: 100%;
margin: 0;
padding: 0 30px 0 0;
display: block;
position: relative;
text-align: left;
background-color: white;
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A//www.w3.org/2000/svg'%20viewBox%3D'0%200%204%205'%3E%3Cpath%20fill%3D'%23343A40'%20d%3D'M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center right 0.75rem;
background-size: 8px 10px;
border-width: 1px;
border-style: solid;
border-color: #CCCCCC;
border-radius: 3px;
transition: border 142ms linear, background 142ms linear, box-shadow 142ms linear;
-webkit-transition: border 142ms linear, background 142ms linear, box-shadow 142ms linear;
}
.tail-select .select-label:hover{
color: #343A40;
border-color: #b9b9b9;
background-color: #ececec;
}
.tail-select .select-label .label-count, .tail-select .select-label .label-inner{
width: auto;
margin: 0;
display: inline-block;
text-align: left;
vertical-align: top;
}
.tail-select .select-label .label-count{
float: left;
color: white;
margin: 10px -3px 0 9px;
padding: 0.25em 0.4em;
display: inline-block;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-shadow: none;
white-space: nowrap;
border-radius: 0.25rem;
background-color: #343A40;
}
.tail-select .select-label .label-inner{
margin: 0;
padding: 0.375rem 0.75rem;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.tail-select.active .select-label{
color: #343A40;
border-color: #0088CC;
background-color: #ececec;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 204, 0.35);
-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 136, 204, 0.35);
}
/* @end LABEL */
/* @start DROPDOWN */
.tail-select .select-dropdown{
top: 100%;
left: 0;
color: #343a40;
width: 100%;
margin: 0.125rem 0 0;
padding: 0;
z-index: 27;
display: none;
position: absolute;
background-color: white;
border-width: 1px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.tail-select .select-dropdown .dropdown-search{
width: 100%;
margin: 0;
padding: 10px;
display: block;
border-width: 0 0 1px 0;
border-style: solid;
border-color: #e6e6e6;
}
.tail-select .select-dropdown .dropdown-inner{
width: 100%;
margin: 0;
padding: 1px 0;
display: block;
overflow-x: hidden;
overflow-y: auto;
}
.tail-select .select-dropdown .dropdown-empty{
margin: 0;
padding: 1.25rem 1.75rem;
display: block;
font-size: 0.875rem;
text-align: center;
font-weight: 400;
line-height: 1.2;
}
.tail-select .select-dropdown .dropdown-action{
top: 8px;
right: 15px;
width: auto;
margin: 0;
padding: 7px 0;
z-index: 35;
display: inline-block;
position: absolute;
text-align: center;
}
.tail-select .select-dropdown ul, .tail-select .select-dropdown ul li{
width: 100%;
margin: 0;
padding: 0;
display: block;
position: relative;
list-style: none;
vertical-align: top;
}
.tail-select .select-dropdown ul li{
color: #343a40;
padding: 0.25rem 1.75rem;
text-align: left;
font-weight: normal;
}
.tail-select .select-dropdown ul li:first-of-type{
margin-top: 0.5rem;
}
.tail-select .select-dropdown ul li:last-of-type{
margin-bottom: 0.5rem;
}
.tail-select .select-dropdown ul li.optgroup-title{
color: rgba(52, 58, 64, 0.7);
cursor: default;
margin: 0;
padding: 0.5rem 1.5rem;
font-size: 11px;
font-weight: bold;
line-height: 20px;
text-shadow: none;
letter-spacing: 1px;
text-transform: uppercase;
}
.tail-select .select-dropdown ul li.optgroup-title b{
font-size: 0.875rem;
font-weight: 400;
line-height: 1.2;
text-shadow: none;
letter-spacing: 0;
text-transform: none;
}
.tail-select .select-dropdown ul li.optgroup-title button{
float: right;
opacity: 0;
}
.tail-select .select-dropdown ul:hover li button{
opacity: 1;
}
.tail-select .select-dropdown ul li.dropdown-option{
cursor: pointer;
color: #343a40;
}
.tail-select .select-dropdown ul li.dropdown-option:before{
top: 0;
left: 0;
width: 30px;
height: 33px;
margin: 0;
padding: 0;
z-index: 21;
display: inline-block;
content: "";
position: absolute;
vertical-align: top;
background-repeat: no-repeat;
background-position: center center;
}
.tail-select .select-dropdown ul li.dropdown-option .option-description{
color: rgba(52, 58, 64, 0.7);
width: auto;
margin: 0;
padding: 0;
display: block;
font-size: 10px;
text-align: left;
line-height: 14px;
vertical-align: top;
}
.tail-select.hide-selected .select-dropdown ul li.selected,
.tail-select.hide-disabled .select-dropdown ul li.disabled{
display: none;
}
/* Selected */
.tail-select .select-dropdown ul li.dropdown-option.selected{
color: #0088CC;
background-color: transparent;
}
.tail-select .select-dropdown ul li.dropdown-option.selected:before{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBkPSJNMTIgNWwtOCA4LTQtN\
CAxLjUtMS41TDQgMTBsNi41LTYuNUwxMiA1eiIvPjwvc3ZnPg==");
}
.tail-select .select-dropdown ul li.dropdown-option.selected .option-description{
color: rgba(52, 58, 64, 0.7);
}
/* Unselect */
.tail-select.deselect .select-dropdown ul li.dropdown-option.selected:hover:before,
.tail-select.multiple .select-dropdown ul li.dropdown-option.selected:hover:before,
.tail-select.deselect .select-dropdown ul li.dropdown-option.selected.hover:before,
.tail-select.multiple .select-dropdown ul li.dropdown-option.selected.hover:before{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBkPSJNNy40OCA4bDMuNzUgM\
y43NS0xLjQ4IDEuNDhMNiA5LjQ4bC0zLjc1IDMuNzUtMS40OC0xLjQ4TDQuNTIgOCAuNzcgNC4yNWwxLjQ4LTEuNDhMNiA2\
LjUybDMuNzUtMy43NSAxLjQ4IDEuNDhMNy40OCA4eiIvPjwvc3ZnPg==");
}
/* Hover */
.tail-select .select-dropdown ul li.dropdown-option{
transition: all 0.3s ease-in;
}
.tail-select .select-dropdown ul li.dropdown-option:hover,
.tail-select .select-dropdown ul li.dropdown-option.hover{
transition: all 0.4s ease;
color: #343a40;
background-color: #ffffff;
}
.tail-select .select-dropdown ul li.dropdown-option:hover .option-description,
.tail-select .select-dropdown ul li.dropdown-option.hover .option-description{
color: rgba(52, 58, 64, 0.7);
}
/* Disabled */
.tail-select.disabled .select-dropdown ul li.dropdown-option,
.tail-select .select-dropdown ul li.dropdown-option.disabled{
cursor: not-allowed;
color: rgba(52, 58, 64, 0.35);
text-shadow: 0px 1px 0px rgba(122, 135, 147, 0.1), 0px -1px 0px rgba(0, 0, 0, 0.1);
background-color: rgba(52, 58, 64, 0.02);
}
.tail-select.disabled .select-dropdown ul li.dropdown-option .option-description,
.tail-select .select-dropdown ul li.dropdown-option.disabled .option-description{
text-shadow: 0px 1px 0px rgba(63, 71, 78, 0.05), 0px -1px 0px rgba(41, 45, 50, 0.05);
}
.tail-select.disabled .select-dropdown ul li.dropdown-option .option-description,
.tail-select .select-dropdown ul li.dropdown-option.disabled .option-description,
.tail-select.disabled .select-dropdown ul li.dropdown-option:hover .option-description,
.tail-select .select-dropdown ul li.dropdown-option.disabled:hover .option-description,
.tail-select.disabled .select-dropdown ul li.dropdown-option.hover .option-description,
.tail-select .select-dropdown ul li.dropdown-option.disabled.hover .option-description{
color: rgba(52, 58, 64, 0.7);
}
/* @end DROPDOWN */
/*# sourceMappingURL=tail.select-default.map */

1
static/css/tailwind.css Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,8 +11,6 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap" rel="stylesheet" />
<link rel="stylesheet" type="text/css"
href="https://cdn.rawgit.com/dreampulse/computer-modern-web-font/master/fonts.css">
<link type="text/css" rel="stylesheet" href="/static/css/tail.select.min.css">
<script src="/static/js/tail.select.min.js"></script>
<link href="/static/css/tailwind.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">

View File

@@ -40,14 +40,22 @@
</div>
<div class="mr-4">
<select name="view" hx-get="{{ url_for('calendar.get_calendar', person_id=person_id) }}"
hx-target="#container" hx-vals='{"date": "{{ date }}"}' hx-push-url="true"
_="init js(me) tail.select(me, {}) end" class="h-10 invisible">
<option value="month" {% if view=='month' %}selected{% endif %}>Month</option>
<option value="year" {% if view=='year' %}selected{% endif %}>Year</option>
<option value="notes">Notes</option>
<option value="overview">Overview</option>
</select>
{{ render_partial('partials/custom_select.html',
name='view',
options=[
{'id': 'month', 'name': 'Month', 'selected': (view == 'month')},
{'id': 'year', 'name': 'Year', 'selected': (view == 'year')},
{'id': 'notes', 'name': 'Notes', 'selected': (view == 'notes')},
{'id': 'overview', 'name': 'Overview', 'selected': (view == 'overview')}
],
multiple=false,
search=false,
placeholder='View',
hx_get=url_for('calendar.get_calendar', person_id=person_id),
hx_target='#container',
hx_vals='{"date": "' + date.strftime('%Y-%m-%d') + '"}',
hx_push_url=true)
}}
</div>
</div>

View File

@@ -10,21 +10,17 @@
<div class="mb-3 w-full"><label
class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
for="grid-city">People</label>
<select class="bg-gray-50 border border-gray-300 h-10 invisible"
hx-get="{{ url_for('dashboard') }}"
hx-include="[name='min_date'],[name='max_date'],[name='exercise_id']" hx-push-url="true"
hx-target="#container" multiple="" name="person_id" _="init js(me)
tail.select(me, {
multiple: true,
search: true,
placeholder: 'Filter people',
})
end">
{% for person in people %}
<option value="{{ person.id }}" {% if person.selected %}selected{% endif %}>{{ person.name
}}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='person_id',
options=people,
multiple=true,
search=true,
placeholder='Filter people',
hx_get=url_for('dashboard'),
hx_include="input[name='min_date'],input[name='max_date'],#select-exercise_id",
hx_target='#container',
hx_push_url=true)
}}
</div>
</div>
</div>
@@ -35,21 +31,17 @@
<div class="mb-3 w-full"><label
class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
for="grid-city">Exercises</label>
<select class="bg-gray-50 border border-gray-300 h-10 invisible"
hx-get="{{ url_for('dashboard') }}"
hx-include="[name='min_date'],[name='max_date'],[name='person_id']" hx-push-url="true"
hx-target="#container" multiple="" name="exercise_id" _="init js(me)
tail.select(me, {
multiple: true,
search: true,
placeholder: 'Filter exercises',
})
end">
{% for exercise in exercises %}
<option value="{{ exercise.id }}" {% if exercise.selected %}selected{% endif %}>
{{ exercise.name }}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='exercise_id',
options=exercises,
multiple=true,
search=true,
placeholder='Filter exercises',
hx_get=url_for('dashboard'),
hx_include="input[name='min_date'],input[name='max_date'],#select-person_id",
hx_target='#container',
hx_push_url=true)
}}
</div>
</div>
</div>
@@ -68,7 +60,7 @@
</svg></div><input
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 w-full"
hx-get="{{ url_for('dashboard') }}"
hx-include="[name='min_date'],[name='max_date'],[name='person_id'],[name='exercise_id']"
hx-include="input[name='min_date'],input[name='max_date'],#select-person_id,#select-exercise_id"
hx-push-url="true" hx-target="#container" hx-trigger="change" name="min_date" type="date"
value="{{ min_date }}">
</div>
@@ -98,7 +90,7 @@
</div>
<div class="hidden" hx-get="{{ url_for('get_people_graphs') }}"
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date'],[name='person_id']" hx-trigger="load"
hx-include="#select-exercise_id,input[name='min_date'],input[name='max_date'],#select-person_id" hx-trigger="load"
hx-target="this" hx-swap="outerHTML">
</div>
@@ -195,7 +187,7 @@
<div class="hidden" hx-get="{{ url_for('get_stats') }}"
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date'],[name='person_id']" hx-trigger="load"
hx-include="#select-exercise_id,input[name='min_date'],input[name='max_date'],#select-person_id" hx-trigger="load"
hx-target="#stats" hx-swap="innerHTML">
</div>

View File

@@ -12,13 +12,21 @@
</div>
<div class="mr-4">
<select name="view" hx-get="{{ url_for('calendar.get_calendar', person_id=person_id) }}"
hx-target="#container" x-push-url="true" _="init js(me) tail.select(me, {}) end" class="h-10 invisible">
<option value="month">Month</option>
<option value="year">Year</option>
<option value="notes" selected>Notes</option>
<option value="overview">Overview</option>
</select>
{{ render_partial('partials/custom_select.html',
name='view',
options=[
{'id': 'month', 'name': 'Month'},
{'id': 'year', 'name': 'Year'},
{'id': 'notes', 'name': 'Notes', 'selected': true},
{'id': 'overview', 'name': 'Overview'}
],
multiple=false,
search=false,
placeholder='View',
hx_get=url_for('calendar.get_calendar', person_id=person_id),
hx_target='#container',
hx_push_url=true)
}}
</div>
</div>

View File

@@ -0,0 +1,196 @@
<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>

View File

@@ -46,7 +46,9 @@
_="on htmx:afterRequest toggle .hidden on #show-add-tag-form-btn then toggle .hidden on me then set me.tag_name.value to ''">
{# Hide form, show button, clear input after submit #}
<input type="hidden" name="person_id" value="{{ person_id | default('', true) }}">
{% if person_id %}
<input type="hidden" name="person_id" value="{{ person_id }}">
{% endif %}
<input type="hidden" name="current_filter" value="{{ request.query_string.decode() | default('', true) }}">
{# Pass
context

View File

@@ -21,19 +21,13 @@
</div>
{% else %}
<div class="w-full">
<select name="exercise_id"
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
_="init js(me)
tail.select(me, {
search: true,
placeholder: 'Filter exercises',
})
end">
{% for exercise in exercises|default([], true) %}
<option value="{{ exercise.exercise_id }}" {% if exercise.exercise_id==exercise_id %}selected{% endif
%}>{{ exercise.name }}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='exercise_id',
options=exercises|default([], true),
multiple=false,
search=true,
placeholder='Filter exercises')
}}
</div>
{% endif %}
</td>

View File

@@ -34,24 +34,15 @@
</label>
<div class="relative">
<div class="w-full">
<select multiple name="tag_id"
hx-post="{{ url_for('tags.add_tag_to_workout', workout_id=workout_id) }}"
hx-target="#tag-wrapper-w-{{ workout_id }}"
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
_="init js(me)
tail.select(me, {
search: true,
multiple: true,
placeholder: 'Select tags',
})
end">
{% for tag in tags %}
<option value="{{ tag.tag_id }}" {% if tag.is_selected %}selected{% endif %}>
{{
tag.tag_name
}}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='tag_id',
options=tags,
multiple=true,
search=true,
placeholder='Select tags',
hx_post=url_for('tags.add_tag_to_workout', workout_id=workout_id),
hx_target='#tag-wrapper-w-' + workout_id|string)
}}
</div>
</div>
</div>

View File

@@ -12,14 +12,21 @@
</div>
<div>
<div>
<select name="view" hx-get="{{ url_for('calendar.get_calendar', person_id=person_id) }}"
hx-target="#container" hx-push-url="true" _="init js(me) tail.select(me, {}) end"
class="h-10 invisible">
<option value="month">Month</option>
<option value="year">Year</option>
<option value="notes">Notes</option>
<option value="overview" selected>Overview</option>
</select>
{{ render_partial('partials/custom_select.html',
name='view',
options=[
{'id': 'month', 'name': 'Month'},
{'id': 'year', 'name': 'Year'},
{'id': 'notes', 'name': 'Notes'},
{'id': 'overview', 'name': 'Overview', 'selected': true}
],
multiple=false,
search=false,
placeholder='View',
hx_get=url_for('calendar.get_calendar', person_id=person_id),
hx_target='#container',
hx_push_url=true)
}}
</div>
</div>
</div>
@@ -30,22 +37,17 @@
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-city">
Exercises
</label>
<select data-te-select-filter="true" data-te-select-size="lg" name="exercise_id"
class="bg-gray-50 border border-gray-300 " multiple
hx-get="{{ url_for('person_overview', person_id=person_id) }}"
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date'],[name='graph_axis']"
hx-target="#container" hx-push-url="true" _="init js(me)
tail.select(me, {
multiple: true,
search: true,
placeholder: 'Filter exercises',
})
end">
{% for exercise in exercises %}
<option value="{{ exercise.id }}" {% if exercise.selected %}selected{% endif %}>
{{ exercise.name }}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='exercise_id',
options=exercises,
multiple=true,
search=true,
placeholder='Filter exercises',
hx_get=url_for('person_overview', person_id=person_id),
hx_include="[name='exercise_id'],[name='min_date'],[name='max_date'],[name='graph_axis']",
hx_target='#container',
hx_push_url=true)
}}
</div>
</div>
<div class="w-full md:w-1/3 px-2 md:px-3 mb-6 md:mb-0">

View File

@@ -97,17 +97,14 @@
{# Nested Template for a single exercise row within a session #}
<template id="exercise-row-template">
<div class="exercise-row flex items-center space-x-2">
{# Wrapper div for tail.select - Added position: relative #}
<div class="flex-grow relative">
{# Note: tail.select might hide the original select, apply styling to its container if needed #}
<select name="exercises_SESSION_INDEX_PLACEHOLDER" required class="exercise-select-original w-full"> {#
Keep original select for form submission, tail.select will enhance it #}
<option value="">Select Exercise...</option>
{# Render options directly here using the exercises passed to the main template #}
{% for exercise in exercises %}
<option value="{{ exercise.exercise_id }}">{{ exercise.name }}</option>
{% endfor %}
</select>
{{ render_partial('partials/custom_select.html',
name='exercises_SESSION_INDEX_PLACEHOLDER',
options=exercises,
multiple=false,
search=true,
placeholder='Select Exercise...')
}}
</div>
<button type="button" class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
title="Remove Exercise">
@@ -182,33 +179,14 @@
// --- Function to add an exercise select row to a specific session ---
function addExerciseSelect(container, sessionIndex) {
const newExFragment = exerciseTemplate.content.cloneNode(true);
const originalSelect = newExFragment.querySelector('.exercise-select-original');
const nativeSelect = newExFragment.querySelector('.native-select');
const removeBtn = newExFragment.querySelector('.remove-exercise-btn');
if (!originalSelect || !removeBtn) {
console.error("Failed to find original select or remove button in exercise template clone.");
return;
if (nativeSelect) {
nativeSelect.name = `exercises_${sessionIndex}`;
}
// Set the name attribute correctly for getlist
originalSelect.name = `exercises_${sessionIndex}`;
container.appendChild(newExFragment);
// Find the newly added select element *after* appending
const newSelectElement = container.querySelector('.exercise-row:last-child .exercise-select-original');
// Initialize tail.select on the new element
if (newSelectElement && typeof tail !== 'undefined' && tail.select) {
tail.select(newSelectElement, {
search: true,
placeholder: 'Select Exercise...',
// classNames: "w-full" // Add tailwind classes if needed for the generated dropdown
});
} else {
console.warn("tail.select library not found or new select element not found. Using standard select.");
}
// Attach remove listener to the new exercise row's button
attachExerciseRemoveListener(removeBtn);
}
@@ -274,7 +252,7 @@
}
// Update names for the exercise selects within this session
const exerciseSelects = row.querySelectorAll('.exercise-select-original'); // Target original selects
const exerciseSelects = row.querySelectorAll('.native-select'); // Target hidden selects
exerciseSelects.forEach(select => {
select.name = `exercises_${newIndex}`;
});