Files
orion/app/templates/shared/macros/datepicker.html
Samir Boulahtit 8ee8c398ce
Some checks failed
CI / ruff (push) Successful in 14s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
perf: add defer to scripts and lazy loading to images
Add defer attribute to 145 <script> tags across 103 template files
(PERF-067) and loading="lazy" to 22 <img> tags across 13 template
files (PERF-058). Both improve page load performance.

Validator totals: 0 errors, 2 warnings, 1360 info (down from 1527).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:55:52 +01:00

356 lines
15 KiB
HTML

{#
Datepicker Macros
=================
Date and time picker components using Flatpickr with Alpine.js integration.
Prerequisites:
Add Flatpickr CDN to your base template:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css">
<script defer src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
Usage:
{% from 'shared/macros/datepicker.html' import datepicker, daterange_picker, datetime_picker %}
{# Basic date picker #}
{{ datepicker('startDate', 'formData.startDate', label='Start Date') }}
{# Date range picker #}
{{ daterange_picker('dateRange', 'formData.dateRange', label='Date Range') }}
{# Date and time picker #}
{{ datetime_picker('scheduledAt', 'formData.scheduledAt', label='Schedule') }}
#}
{#
Datepicker
==========
A single date picker input.
Parameters:
- id: Unique ID for the input
- x_model: Alpine.js x-model binding
- label: Input label (optional)
- placeholder: Placeholder text (default: 'Select date')
- format: Date format (default: 'Y-m-d')
- min_date: Minimum selectable date (default: none)
- max_date: Maximum selectable date (default: none)
- disabled: Alpine.js expression for disabled state
- required: Whether the field is required
- error: Alpine.js expression for error message
- class_extra: Additional CSS classes
#}
{% macro datepicker(id, x_model, label=none, placeholder='Select date', format='Y-m-d', min_date=none, max_date=none, disabled=none, required=false, error=none, class_extra='') %}
<div class="mb-4 {{ class_extra }}">
{% if label %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
</label>
{% endif %}
<div class="relative">
<input
type="text"
id="{{ id }}"
x-model="{{ x_model }}"
x-init="flatpickr($el, {
dateFormat: '{{ format }}',
{% if min_date %}minDate: '{{ min_date }}',{% endif %}
{% if max_date %}maxDate: '{{ max_date }}',{% endif %}
disableMobile: true,
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
class="block w-full pl-10 pr-4 py-2.5 text-sm text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
{% if error %}:class="{ 'border-red-500 focus:border-red-500 focus:ring-red-500/20': {{ error }} }"{% endif %}
readonly
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
</div>
{% if error %}
<p x-show="{{ error }}" class="mt-1 text-xs text-red-600 dark:text-red-400" x-text="{{ error }}"></p>
{% endif %}
</div>
{% endmacro %}
{#
Date Range Picker
=================
A date range picker for selecting start and end dates.
Parameters:
- id: Unique ID for the input
- x_model: Alpine.js x-model binding (will contain "YYYY-MM-DD to YYYY-MM-DD")
- label: Input label (optional)
- placeholder: Placeholder text (default: 'Select date range')
- format: Date format (default: 'Y-m-d')
- min_date: Minimum selectable date
- max_date: Maximum selectable date
- disabled: Alpine.js expression for disabled state
- required: Whether the field is required
#}
{% macro daterange_picker(id, x_model, label=none, placeholder='Select date range', format='Y-m-d', min_date=none, max_date=none, disabled=none, required=false) %}
<div class="mb-4">
{% if label %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
</label>
{% endif %}
<div class="relative">
<input
type="text"
id="{{ id }}"
x-model="{{ x_model }}"
x-init="flatpickr($el, {
mode: 'range',
dateFormat: '{{ format }}',
{% if min_date %}minDate: '{{ min_date }}',{% endif %}
{% if max_date %}maxDate: '{{ max_date }}',{% endif %}
disableMobile: true,
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
class="block w-full pl-10 pr-4 py-2.5 text-sm text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
readonly
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
</div>
</div>
{% endmacro %}
{#
DateTime Picker
===============
A date and time picker.
Parameters:
- id: Unique ID for the input
- x_model: Alpine.js x-model binding
- label: Input label (optional)
- placeholder: Placeholder text (default: 'Select date and time')
- format: DateTime format (default: 'Y-m-d H:i')
- enable_time: Whether to enable time selection (default: true)
- time_24hr: Use 24-hour time format (default: true)
- minute_increment: Minute increment for time picker (default: 5)
- min_date: Minimum selectable date
- max_date: Maximum selectable date
- disabled: Alpine.js expression for disabled state
- required: Whether the field is required
#}
{% macro datetime_picker(id, x_model, label=none, placeholder='Select date and time', format='Y-m-d H:i', enable_time=true, time_24hr=true, minute_increment=5, min_date=none, max_date=none, disabled=none, required=false) %}
<div class="mb-4">
{% if label %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
</label>
{% endif %}
<div class="relative">
<input
type="text"
id="{{ id }}"
x-model="{{ x_model }}"
x-init="flatpickr($el, {
enableTime: {{ 'true' if enable_time else 'false' }},
time_24hr: {{ 'true' if time_24hr else 'false' }},
minuteIncrement: {{ minute_increment }},
dateFormat: '{{ format }}',
{% if min_date %}minDate: '{{ min_date }}',{% endif %}
{% if max_date %}maxDate: '{{ max_date }}',{% endif %}
disableMobile: true,
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
class="block w-full pl-10 pr-4 py-2.5 text-sm text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
readonly
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
{% endmacro %}
{#
Time Picker
===========
A time-only picker.
Parameters:
- id: Unique ID for the input
- x_model: Alpine.js x-model binding
- label: Input label (optional)
- placeholder: Placeholder text (default: 'Select time')
- format: Time format (default: 'H:i')
- time_24hr: Use 24-hour time format (default: true)
- minute_increment: Minute increment (default: 5)
- disabled: Alpine.js expression for disabled state
- required: Whether the field is required
#}
{% macro time_picker(id, x_model, label=none, placeholder='Select time', format='H:i', time_24hr=true, minute_increment=5, disabled=none, required=false) %}
<div class="mb-4">
{% if label %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
</label>
{% endif %}
<div class="relative">
<input
type="text"
id="{{ id }}"
x-model="{{ x_model }}"
x-init="flatpickr($el, {
enableTime: true,
noCalendar: true,
time_24hr: {{ 'true' if time_24hr else 'false' }},
minuteIncrement: {{ minute_increment }},
dateFormat: '{{ format }}',
disableMobile: true,
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
class="block w-full pl-10 pr-4 py-2.5 text-sm text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
readonly
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
{% endmacro %}
{#
Month Picker
============
A month-only picker for selecting year and month.
Parameters:
- id: Unique ID for the input
- x_model: Alpine.js x-model binding
- label: Input label (optional)
- placeholder: Placeholder text (default: 'Select month')
- format: Month format (default: 'F Y')
- disabled: Alpine.js expression for disabled state
- required: Whether the field is required
#}
{% macro month_picker(id, x_model, label=none, placeholder='Select month', format='F Y', disabled=none, required=false) %}
<div class="mb-4">
{% if label %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
</label>
{% endif %}
<div class="relative">
<input
type="text"
id="{{ id }}"
x-model="{{ x_model }}"
x-init="flatpickr($el, {
plugins: [new monthSelectPlugin({ shorthand: false, dateFormat: '{{ format }}', altFormat: '{{ format }}' })],
disableMobile: true,
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
class="block w-full pl-10 pr-4 py-2.5 text-sm text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
readonly
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
</div>
</div>
{% endmacro %}
{#
Inline Datepicker
=================
An inline calendar picker (always visible).
Parameters:
- id: Unique ID for the element
- x_model: Alpine.js x-model binding
- format: Date format (default: 'Y-m-d')
- min_date: Minimum selectable date
- max_date: Maximum selectable date
#}
{% macro inline_datepicker(id, x_model, format='Y-m-d', min_date=none, max_date=none) %}
<div
id="{{ id }}"
x-init="flatpickr($el, {
inline: true,
dateFormat: '{{ format }}',
{% if min_date %}minDate: '{{ min_date }}',{% endif %}
{% if max_date %}maxDate: '{{ max_date }}',{% endif %}
onChange: function(selectedDates, dateStr) {
{{ x_model }} = dateStr;
}
})"
class="flatpickr-inline"
></div>
{% endmacro %}
{#
Flatpickr Dark Mode Styles
==========================
Include this in your base template for dark mode support.
The dark theme CSS is loaded conditionally.
#}
{% macro datepicker_dark_mode_script() %}
<script>
// Apply dark theme to Flatpickr when dark mode is active
document.addEventListener('DOMContentLoaded', function() {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class') {
const isDark = document.documentElement.classList.contains('dark');
document.querySelectorAll('.flatpickr-calendar').forEach(calendar => {
calendar.classList.toggle('dark', isDark);
});
}
});
});
observer.observe(document.documentElement, { attributes: true });
});
</script>
{% endmacro %}