1
0
mirror of https://github.com/axzilla/templui.git synced 2025-02-22 10:13:27 +00:00

feat: CSP datepicker

This commit is contained in:
axzilla 2024-12-14 16:57:50 +07:00
parent df3f88b7f0
commit b09fd3d494
3 changed files with 302 additions and 184 deletions

View File

@ -1713,11 +1713,6 @@ body {
letter-spacing: -0.025em;
}
.text-accent-foreground {
--tw-text-opacity: 1;
color: hsl(var(--accent-foreground) / var(--tw-text-opacity, 1));
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));

View File

@ -144,8 +144,177 @@ type DatepickerProps struct {
Attributes templ.Attributes // Additional HTML attributes
}
templ datepickerHandler() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepickerHandler', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
@datepickerHandler()
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
@ -157,116 +326,9 @@ templ Datepicker(props DatepickerProps) {
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepickerHandler"
data-input-id={ props.ID }
x-data="{
open: false,
value: null,
format: $el.dataset.format,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: JSON.parse($el.dataset.monthnames) || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: JSON.parse($el.dataset.daynames) || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const initialDate = $el.dataset.value ? new Date(this.parseDate($el.dataset.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if ($el.dataset.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
updatePosition() {
const trigger = document.getElementById($el.dataset.inputId);
const popup = this.$refs.datePickerPopup;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr; // Für ISO und long Format das Datum unverändert lassen
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
default: // iso
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate(day) {
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
}
}"
@resize.window="if (open) updatePosition()"
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
@ -281,17 +343,17 @@ templ Datepicker(props DatepickerProps) {
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
"x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker()",
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker()"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
@ -311,7 +373,7 @@ templ Datepicker(props DatepickerProps) {
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="open = false"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
@ -319,25 +381,23 @@ templ Datepicker(props DatepickerProps) {
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
// States
"top-full mt-1",
),
}
:class="{'top-full mt-1': position === 'bottom','bottom-full mb-1': position === 'top'}"
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="months[currentMonth] + ' ' + currentYear" class="text-sm font-medium"></span>
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="currentMonth--; if(currentMonth < 0) { currentMonth = 11; currentYear--; } calculateDays()"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="currentMonth++; if(currentMonth > 11) { currentMonth = 0; currentYear++; } calculateDays()"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
@ -350,19 +410,16 @@ templ Datepicker(props DatepickerProps) {
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" :key="'blank' + blank">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays" :key="day">
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate(day)"
@click="selectDate"
:class="activeDayClass"
x-text="day"
:class="{
'bg-primary text-primary-foreground': isSelected(day),
'text-red-500': isToday(day) && !isSelected(day),
'hover:bg-accent hover:text-accent-foreground': !isSelected(day)
}"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>

File diff suppressed because one or more lines are too long