Open CodeDanger opened 2 months ago
I noticed that the issue is related to the hijri-calendar extension for the dayjs library. We might replace the calendar package in the next major release unless a solution is found.
لدي نفس المشكلة والمشكلة الاخرى ان شهر اثنين عند الضغط على يوم 29 / 2 / 1446 يطلع 01 / 03 / 1446
Because the extension and process are heavily related to the Gregory calendar. PR is welcome.
idk if that gonna help ya all but this how i did it for me simple one
import moment from "moment-hijri";
// import "moment-timezone";
export default function hijriDateTimePickerFormComponent({
displayFormat,
firstDayOfWeek,
isAutofocused,
locale,
shouldCloseOnDateSelection,
timeZone,
state,
}) {
// console.log(moment.tz);
const max_allowed_year = 1500;
const min_allowed_year = 1356;
const timezone = timeZone;
return {
daysInFocusedMonth: [],
displayFormat: displayFormat,
displayText: "",
emptyDaysInFocusedMonth: [],
focusedDate: null,
focusedMonth: null,
focusedYear: null,
hour: null,
isClearingState: false,
minute: null,
second: null,
state,
dayLabels: [],
months: [],
init: function () {
moment.locale(locale);
// Initialize focusedDate using Hijri start of the date
this.focusedDate =
this.getSelectedDate() ?? moment().startOf("iDate");
let date = this.getSelectedDate() ?? moment().startOf("iDate");
if (this.getMaxDate() !== null && date.isAfter(this.getMaxDate())) {
date = null;
} else if (
this.getMinDate() !== null &&
date.isBefore(this.getMinDate())
) {
date = null;
}
this.hour = date?.hour() ?? 0;
this.minute = date?.minute() ?? 0;
this.second = date?.second() ?? 0;
// Set focused date display text
this.setDisplayText();
// Initialize months and day labels
this.setMonths();
this.setDayLabels();
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button)
);
}
this.setupDateWatchers();
this.setupTimeWatchers();
},
setupDateWatchers: function () {
this.$watch("focusedMonth", (new_val, _) => {
var new_month = parseInt(new_val);
if (this.focusedDate.iMonth() === new_month) {
return;
}
this.focusedDate = this.focusedDate.iMonth(new_month); // Set month in Hijri
this.setupDaysGrid();
});
this.$watch("focusedYear", (new_val, _) => {
var new_year = parseInt(new_val);
if (new_year > max_allowed_year) {
this.focusedYear = max_allowed_year;
return;
}
if (new_year?.toString()?.length > 4) {
new_year = parseInt(new_year.toString().substring(0, 4));
}
if (!new_year || new_year?.toString()?.length !== 4) {
return;
}
if (new_year < min_allowed_year) {
this.focusedYear = min_allowed_year;
return;
}
// let year = +this.focusedYear;
if (!Number.isInteger(new_year)) {
new_year = moment().iYear(); // Hijri year
// this.focusedYear = year;
}
if (this.focusedDate.iYear() === new_year) {
return;
}
this.focusedDate = this.focusedDate.iYear(new_year); // Set year in Hijri
this.setupDaysGrid();
});
this.$watch("focusedDate", () => {
let month = this.focusedDate.iMonth();
let year = this.focusedDate.iYear();
if (this.focusedMonth !== month) {
this.focusedMonth = month;
}
if (this.focusedYear !== year) {
this.focusedYear = year;
}
this.setupDaysGrid();
});
},
setupTimeWatchers: function () {
this.$watch("hour", () => {
this.handleTimeChange("hour", 23);
});
this.$watch("minute", () => {
this.handleTimeChange("minute", 59);
});
this.$watch("second", () => {
this.handleTimeChange("second", 59);
});
this.$watch("state", () => {
if (this.state === undefined) {
return;
}
let date = this.getSelectedDate();
if (date === null) {
this.clearState();
return;
}
if (
this.getMaxDate() !== null &&
date?.isAfter(this.getMaxDate())
) {
date = null;
}
if (
this.getMinDate() !== null &&
date?.isBefore(this.getMinDate())
) {
date = null;
}
this.syncTimeWithState(date);
});
},
handleTimeChange: function (unit, max) {
let value = +this[unit];
if (!Number.isInteger(value)) {
this[unit] = 0;
} else if (value > max) {
this[unit] = 0;
} else if (value < 0) {
this[unit] = max;
}
if (this.isClearingState) {
return;
}
let date = this.getSelectedDate() ?? this.focusedDate;
this.setState(date[unit](this[unit] ?? 0));
},
syncTimeWithState: function (date) {
const newHour = date?.hour() ?? 0;
if (this.hour !== newHour) {
this.hour = newHour;
}
const newMinute = date?.minute() ?? 0;
if (this.minute !== newMinute) {
this.minute = newMinute;
}
const newSecond = date?.second() ?? 0;
if (this.second !== newSecond) {
this.second = newSecond;
}
this.setDisplayText();
},
clearState: function () {
this.isClearingState = true;
this.setState(null);
this.hour = 0;
this.minute = 0;
this.second = 0;
this.$nextTick(() => (this.isClearingState = false));
},
dateIsDisabled: function (date) {
return this.isDateDisabled(date, this.$refs?.disabledDates?.value);
},
isDateDisabled: function (date, disabledDates) {
if (
disabledDates &&
JSON.parse(disabledDates).some((disabledDate) => {
if (disabledDate == null) return false;
disabledDate = moment(disabledDate, this.displayFormat);
if (!disabledDate.isValid()) {
return false;
}
return disabledDate.isSame(date, "iDay");
})
) {
return true;
}
// console.log(date.isAfter(this.getMaxDate(), "iDay"));
if (this.getMaxDate() && date.isAfter(this.getMaxDate(), "iDay")) {
return true;
}
if (this.getMinDate() && date.isBefore(this.getMinDate(), "iDay")) {
return true;
}
return false;
},
dayIsDisabled: function (day) {
return this.dateIsDisabled(this.focusedDate.iDate(day));
},
dayIsSelected: function (day) {
let selectedDate = this.getSelectedDate();
return (
selectedDate &&
selectedDate.iDate() === day &&
selectedDate.iMonth() === this.focusedDate.iMonth() &&
selectedDate.iYear() === this.focusedDate.iYear()
);
},
dayIsToday: function (day) {
let today = moment();
return (
today.iDate() === day &&
today.iMonth() === this.focusedDate.iMonth() &&
today.iYear() === this.focusedDate.iYear()
);
},
getMaxDate: function () {
if (
this.$refs.maxDate?.value == null ||
this.$refs.maxDate?.value == ""
)
return null;
let date = moment(this.$refs.maxDate?.value, this.displayFormat);
return date.isValid() ? date : null;
},
getMinDate: function () {
if (
this.$refs.minDate?.value == null ||
this.$refs.minDate?.value == ""
)
return null;
let date = moment(this.$refs.minDate?.value, this.displayFormat);
return date.isValid() ? date : null;
},
getSelectedDate: function () {
if (!this.state) return null;
let date = moment(this.state, this.displayFormat);
return date.isValid() ? date : null;
},
togglePanelVisibility: function () {
if (!this.isOpen()) {
this.focusedDate =
this.getSelectedDate() ?? this.getMinDate() ?? moment();
this.setupDaysGrid();
}
this.$refs.panel.toggle(this.$refs.button);
},
selectDate: function (day = null) {
if (day) this.setFocusedDay(day);
this.setState(this.focusedDate);
if (shouldCloseOnDateSelection) this.togglePanelVisibility();
},
setDisplayText: function () {
this.displayText = this.getSelectedDate()
? this.getSelectedDate().format(this.displayFormat)
: "";
},
setMonths: function () {
this.months =
locale === "ar"
? [
"محرم",
"صفر",
"ربيع الأول",
"ربيع الثاني",
"جمادى الأولى",
"جمادى الآخرة",
"رجب",
"شعبان",
"رمضان",
"شوال",
"ذو القعدة",
"ذو الحجة",
]
: [
"Muharram",
"Safar",
"Rabi al-Awwal",
"Rabi al-Thani",
"Jumada al-Ula",
"Jumada al-Alkhirah",
"Rajab",
"Sha’ban",
"Ramadhan",
"Shawwal",
"Thul-Qi’dah",
"Thul-Hijjah",
];
},
setDayLabels: function () {
this.dayLabels = this.getDayLabels();
},
getDayLabels: function () {
// const labels = dayjs.weekdaysShort();
const labels =
locale === "ar"
? [
"أحد",
"إثنين",
"ثلاثاء",
"أربعاء",
"خميس",
"جمعة",
"سبت",
]
: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
if (firstDayOfWeek === 0) {
return labels;
}
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
];
},
setupDaysGrid: function () {
let date = this.focusedDate ?? moment().startOf("iMonth");
let startDayOfWeek = date.startOf("iMonth").day();
startDayOfWeek = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1;
this.emptyDaysInFocusedMonth = Array.from(
{ length: startDayOfWeek },
(_, i) => i + 1
);
this.daysInFocusedMonth = Array.from(
{ length: date.iDaysInMonth() },
(_, i) => i + 1
);
},
setFocusedDay: function (day) {
this.focusedDate = (this.focusedDate ?? moment()).iDate(day);
},
setState: function (date) {
if (!date || this.dateIsDisabled(date)) return;
// this.state = date.format("YYYY-MM-DD HH:mm:ss");
this.state = date.format(this.displayFormat);
this.setDisplayText();
},
isOpen: function () {
return this.$refs.panel?.style.display === "block";
},
};
}
and this is the html
@php
use Filament\Support\Facades\FilamentView;
$datalistOptions = $getDatalistOptions();
$extraAlpineAttributes = $getExtraAlpineAttributes();
$id = $getId();
$isDisabled = $isDisabled();
$isPrefixInline = $isPrefixInline();
$isSuffixInline = $isSuffixInline();
$prefixActions = $getPrefixActions();
$prefixIcon = $getPrefixIcon();
$prefixLabel = $getPrefixLabel();
$suffixActions = $getSuffixActions();
$suffixIcon = $getSuffixIcon();
$suffixLabel = $getSuffixLabel();
$statePath = $getStatePath();
@endphp
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
:inline-label-vertical-alignment="\Filament\Support\Enums\VerticalAlignment::Center"
>
<x-filament::input.wrapper
:disabled="$isDisabled"
:inline-prefix="$isPrefixInline"
:inline-suffix="$isSuffixInline"
:prefix="$prefixLabel"
:prefix-actions="$prefixActions"
:prefix-icon="$prefixIcon"
:prefix-icon-color="$getPrefixIconColor()"
:suffix="$suffixLabel"
:suffix-actions="$suffixActions"
:suffix-icon="$suffixIcon"
:suffix-icon-color="$getSuffixIconColor()"
:valid="! $errors->has($statePath)"
:attributes="\Filament\Support\prepare_inherited_attributes($getExtraAttributeBag())"
>
<div
x-ignore
@if (FilamentView::hasSpaMode())
ax-load="visible"
@else
ax-load
@endif
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('hijri-date-picker') }}"
x-data="hijriDateTimePickerFormComponent({
displayFormat: '{{ $convert_format_to_moment($getDisplayFormat()) }}',
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
isAutofocused: @js($isAutofocused()),
locale: @js(app()->getLocale()),
shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
timeZone: '{{ env("APP_TIMEZONE","Asia/Riyadh") }}',
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
})"
x-on:keydown.esc="isOpen() && $event.stopPropagation()"
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->merge($getExtraAlpineAttributes(), escape: false)
->class(['filament-hijri-picker'])
}}
>
<input
x-ref="maxDate"
type="hidden"
value="{{ $getMaxDate() }}"
/>
<input
x-ref="minDate"
type="hidden"
value="{{ $getMinDate() }}"
/>
<input
x-ref="disabledDates"
type="hidden"
value="{{ json_encode($getDisabledDates()) }}"
/>
<button
x-ref="button"
x-on:click="togglePanelVisibility()"
x-on:keydown.enter.stop.prevent="
if (! $el.disabled) {
isOpen() ? selectDate() : togglePanelVisibility()
}
"
{{-- x-on:keydown.arrow-left.stop.prevent="if (! $el.disabled) focusPreviousDay()"
x-on:keydown.arrow-right.stop.prevent="if (! $el.disabled) focusNextDay()"
x-on:keydown.arrow-up.stop.prevent="if (! $el.disabled) focusPreviousWeek()"
x-on:keydown.arrow-down.stop.prevent="if (! $el.disabled) focusNextWeek()" --}}
x-on:keydown.backspace.stop.prevent="if (! $el.disabled) clearState()"
x-on:keydown.clear.stop.prevent="if (! $el.disabled) clearState()"
x-on:keydown.delete.stop.prevent="if (! $el.disabled) clearState()"
aria-label="{{ $getPlaceholder() }}"
type="button"
tabindex="-1"
@disabled($isDisabled)
{{
$getExtraTriggerAttributeBag()->class([
'w-full',
])
}}
>
<input
@disabled($isDisabled)
readonly
placeholder="{{ $getPlaceholder() }}"
wire:key="{{ $this->getId() }}.{{ $statePath }}.{{ $field::class }}.display-text"
x-model="displayText"
@if ($id = $getId()) id="{{ $id }}" @endif
@class([
'w-full border-none bg-transparent px-3 py-1.5 text-base text-gray-950 outline-none transition duration-75 placeholder:text-gray-400 focus:ring-0 disabled:text-gray-500 disabled:[-webkit-text-fill-color:theme(colors.gray.500)] dark:text-white dark:placeholder:text-gray-500 dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:theme(colors.gray.400)] sm:text-sm sm:leading-6',
])
/>
</button>
<div
x-ref="panel"
x-cloak
x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }"
wire:ignore
wire:key="{{ $this->getId() }}.{{ $statePath }}.{{ $field::class }}.panel"
@class([
'filament-hijri-picker-panel absolute z-10 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10',
])
>
<div class="grid gap-y-3">
@if ($hasDate())
<div class="flex items-center justify-between">
<select
x-model="focusedMonth"
class="grow cursor-pointer border-none bg-transparent p-0 text-sm font-medium text-gray-950 focus:ring-0 dark:bg-gray-900 dark:text-white"
>
<template
x-for="(month, index) in months"
>
<option
x-bind:value="index"
x-text="month"
></option>
</template>
</select>
<input
type="number"
inputmode="numeric"
x-model.debounce="focusedYear"
max="1500"
min="1356"
class="w-16 border-none bg-transparent p-0 text-right text-sm text-gray-950 focus:ring-0 dark:text-white"
/>
</div>
<div class="grid grid-cols-7 gap-1">
<template
x-for="(day, index) in dayLabels"
x-bind:key="index"
>
<div
x-text="day"
class="text-center text-xs font-medium text-gray-500 dark:text-gray-400"
></div>
</template>
</div>
<div
role="grid"
class="grid grid-cols-[repeat(7,minmax(theme(spacing.7),1fr))] gap-1"
>
<template
x-for="day in emptyDaysInFocusedMonth"
x-bind:key="day"
>
<div></div>
</template>
<template
x-for="day in daysInFocusedMonth"
x-bind:key="day"
>
<div
x-text="day"
x-on:click="dayIsDisabled(day) || selectDate(day)"
x-on:mouseenter="setFocusedDay(day)"
role="option"
x-bind:aria-selected="focusedDate.iDate() === day && focusedDate.iMonth() === focusedMonth && focusedDate.iYear() === focusedYear"
x-bind:class="{
'text-gray-950 dark:text-white':
! dayIsToday(day) &&
! dayIsSelected(day) &&
(focusedDate.iDate() !== day || focusedDate.iMonth() !== focusedMonth || focusedDate.iYear() !== focusedYear), // Ensure it's not selected unless all conditions match
'cursor-pointer':
! dayIsDisabled(day),
'text-primary-600 dark:text-primary-400':
dayIsToday(day) &&
! dayIsSelected(day) &&
focusedDate.iDate() !== day &&
! dayIsDisabled(day) &&
focusedDate.iMonth() === focusedMonth &&
focusedDate.iYear() === focusedYear, // Only apply if same month and year
'bg-gray-50 dark:bg-white/5':
focusedDate.iDate() === day &&
! dayIsSelected(day) &&
focusedDate.iMonth() === focusedMonth &&
focusedDate.iYear() === focusedYear, // Also check month and year
'text-primary-600 bg-gray-50 dark:bg-white/5 dark:text-primary-400':
dayIsSelected(day) &&
focusedDate.iMonth() === focusedMonth &&
focusedDate.iYear() === focusedYear, // Only selected if month and year match
'pointer-events-none':
dayIsDisabled(day),
'opacity-50':
focusedDate.iDate() !== day &&
dayIsDisabled(day) &&
focusedDate.iMonth() === focusedMonth &&
focusedDate.iYear() === focusedYear // Apply opacity check with month and year
}"
class="rounded-full text-center text-sm leading-loose transition duration-75"
></div>
</template>
</div>
@endif
@if ($hasTime())
<div
class="flex items-center justify-center rtl:flex-row-reverse"
>
<input
max="23"
min="0"
step="{{ $getHoursStep() }}"
type="number"
inputmode="numeric"
x-model.debounce="hour"
class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
/>
<span
class="text-sm font-medium text-gray-500 dark:text-gray-400"
>
:
</span>
<input
max="59"
min="0"
step="{{ $getMinutesStep() }}"
type="number"
inputmode="numeric"
x-model.debounce="minute"
class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
/>
@if ($hasSeconds())
<span
class="text-sm font-medium text-gray-500 dark:text-gray-400"
>
:
</span>
<input
max="59"
min="0"
step="{{ $getSecondsStep() }}"
type="number"
inputmode="numeric"
x-model.debounce="second"
class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
/>
@endif
</div>
@endif
</div>
</div>
</div>
</x-filament::input.wrapper>
@if ($datalistOptions)
<datalist id="{{ $id }}-list">
@foreach ($datalistOptions as $option)
<option value="{{ $option }}" />
@endforeach
</datalist>
@endif
</x-dynamic-component>
it's just temp sol till find another one
Thank you @CodeDanger. I'll give it a try.
What happened?
so whenever i try to set hijri date picker to live mode and give it some after state updated there is 2 problem appears first one is assume today one of these days [21,22,23,24,25] some days depend in month day count 30 or 29 if you hover with mouse over todays date then go again to the end date hover it the current month will be automaticly switched to the next month i have attached video for this process so you can understand
https://github.com/user-attachments/assets/4ba3d050-3909-47a5-97d4-88d95dc9e56e
the second point is when ever try to select last day in current month it doesn't selected instead it select the next day which is the first date in the next month
here is specific part of code causes error ` // Hijri Date
`
and here is the full resource code
`<?php
namespace App\Filament\Resources;
use App\Filament\Actions\Tables\ExportPdfAction; use App\Filament\Exports\ContractExporter; use App\Models\Contract; use Filament\Resources\Resource; use App\Filament\Resources\ContractResource\Pages; use Filament\Forms\Form; use Filament\Forms\Components{Actions, Component, Select, DatePicker, TextInput, Textarea, Repeater, Section, Tabs}; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Actions\EditAction; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Table; use Illuminate\Validation\Rule; use Filament\Resources\Pages\CreateRecord; use Filament\Tables\Actions\Action; use Filament\Tables\Actions\ActionGroup; use MohamedSabil83\FilamentHijriPicker\Forms\Components\HijriDatePicker; use Filament\Support\Colors\Color; use Carbon\Carbon; use Carbon\CarbonInterval;
class ContractResource extends Resource { protected static ?string $model = Contract::class; protected static ?string $navigationIcon = 'heroicon-o-document-text'; protected static ?int $navigationSort = 4;
}`
How to reproduce the bug
just do as example that i told in what happend
Package Version
latest
PHP Version
8.3.10
Laravel Version
11
Which operating systems does with happen with?
Windows
Notes
No response