Improves event edit modal; updates 4day and day views to properly show all-day events; cleans up the all-day event row
This commit is contained in:
parent
236da90af2
commit
864e0d938a
@ -140,7 +140,8 @@ class CalendarController extends Controller
|
||||
|
||||
$allDayEvents = $events->filter(fn ($event) => !empty($event['all_day']));
|
||||
$timeEvents = $events->filter(fn ($event) => empty($event['all_day']));
|
||||
$hasAllDayEvents = $allDayEvents->isNotEmpty();
|
||||
$allDayDisplay = $this->buildAllDayDisplay($allDayEvents);
|
||||
$hasAllDayEvents = $allDayDisplay->isNotEmpty();
|
||||
|
||||
if ($request->boolean('debug_all_day')) {
|
||||
$calendarIds = $calendars->pluck('id')->values();
|
||||
@ -297,7 +298,7 @@ class CalendarController extends Controller
|
||||
'hgroup' => $viewBuilder->viewHeaders($view, $range, $tz, $weekStart),
|
||||
'events' => $events, // keyed by occurrence
|
||||
'events_time' => $timeEvents,
|
||||
'events_all_day'=> $allDayEvents,
|
||||
'events_all_day'=> $allDayDisplay,
|
||||
'has_all_day' => $hasAllDayEvents,
|
||||
'grid' => $grid, // day objects hold only ID-sets
|
||||
'mini' => $mini, // mini calendar days with events for indicators
|
||||
@ -371,6 +372,52 @@ class CalendarController extends Controller
|
||||
return view('calendar.index', $payload);
|
||||
}
|
||||
|
||||
private function buildAllDayDisplay($allDayEvents)
|
||||
{
|
||||
$items = collect();
|
||||
$groups = $allDayEvents->groupBy(fn ($event) => (int) ($event['start_col'] ?? 1));
|
||||
|
||||
foreach ($groups as $col => $events) {
|
||||
$sorted = $events->sortBy('start')->values();
|
||||
$total = $sorted->count();
|
||||
|
||||
if ($total <= 3) {
|
||||
foreach ($sorted as $index => $event) {
|
||||
$items->push(array_merge($event, [
|
||||
'all_day_row' => $index + 1,
|
||||
'all_day_more' => false,
|
||||
'all_day_overflow' => false,
|
||||
]));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($sorted as $index => $event) {
|
||||
$items->push(array_merge($event, [
|
||||
'all_day_row' => $index + 1,
|
||||
'all_day_more' => false,
|
||||
'all_day_overflow' => $index >= 2,
|
||||
]));
|
||||
}
|
||||
|
||||
$items->push([
|
||||
'id' => 'more-'.$col,
|
||||
'calendar_slug' => $sorted[0]['calendar_slug'] ?? null,
|
||||
'start_col' => $col,
|
||||
'all_day_row' => 3,
|
||||
'all_day_more' => true,
|
||||
'all_day_overflow' => false,
|
||||
'all_day_more_count' => $total - 2,
|
||||
'visible' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
return $items->sortBy([
|
||||
['start_col', 'asc'],
|
||||
['all_day_row', 'asc'],
|
||||
])->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* create sabre calendar + meta
|
||||
*/
|
||||
|
||||
@ -58,6 +58,7 @@ class EventController extends Controller
|
||||
'tz',
|
||||
'rrule',
|
||||
);
|
||||
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
|
||||
|
||||
if ($request->header('HX-Request') === 'true') {
|
||||
return view('event.partials.form-modal', $data);
|
||||
@ -96,6 +97,7 @@ class EventController extends Controller
|
||||
?? '';
|
||||
|
||||
$data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
|
||||
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
|
||||
|
||||
if ($request->header('HX-Request') === 'true') {
|
||||
return view('event.partials.form-modal', $data);
|
||||
@ -561,6 +563,112 @@ class EventController extends Controller
|
||||
: Carbon::createFromFormat('Y-m-d\\TH:i', $value, $tz)->seconds(0);
|
||||
}
|
||||
|
||||
private function buildRecurrenceFormData(Request $request, string $startValue, string $tz, ?string $rrule): array
|
||||
{
|
||||
$rruleValue = trim((string) ($rrule ?? ''));
|
||||
$rruleParts = [];
|
||||
foreach (array_filter(explode(';', $rruleValue)) as $chunk) {
|
||||
if (!str_contains($chunk, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = explode('=', $chunk, 2);
|
||||
$rruleParts[strtoupper($key)] = $value;
|
||||
}
|
||||
|
||||
$freq = strtolower($rruleParts['FREQ'] ?? '');
|
||||
$interval = (int) ($rruleParts['INTERVAL'] ?? 1);
|
||||
if ($interval < 1) {
|
||||
$interval = 1;
|
||||
}
|
||||
|
||||
$byday = array_filter(explode(',', $rruleParts['BYDAY'] ?? ''));
|
||||
$bymonthday = array_filter(explode(',', $rruleParts['BYMONTHDAY'] ?? ''));
|
||||
$bysetpos = $rruleParts['BYSETPOS'] ?? null;
|
||||
|
||||
$startDate = null;
|
||||
if ($startValue !== '') {
|
||||
try {
|
||||
$startDate = Carbon::parse($startValue, $tz);
|
||||
} catch (\Throwable $e) {
|
||||
$startDate = null;
|
||||
}
|
||||
}
|
||||
$startDate = $startDate ?? Carbon::now($tz);
|
||||
|
||||
$weekdayMap = [
|
||||
'Sun' => 'SU',
|
||||
'Mon' => 'MO',
|
||||
'Tue' => 'TU',
|
||||
'Wed' => 'WE',
|
||||
'Thu' => 'TH',
|
||||
'Fri' => 'FR',
|
||||
'Sat' => 'SA',
|
||||
];
|
||||
$defaultWeekday = $weekdayMap[$startDate->format('D')] ?? 'MO';
|
||||
$defaultMonthDay = (int) $startDate->format('j');
|
||||
|
||||
$weekMap = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'];
|
||||
$startWeek = $startDate->copy();
|
||||
$isLastWeek = $startWeek->copy()->addWeek()->month !== $startWeek->month;
|
||||
$defaultMonthWeek = $isLastWeek ? 'last' : ($weekMap[$startDate->weekOfMonth] ?? 'first');
|
||||
|
||||
$monthMode = 'days';
|
||||
if (!empty($bymonthday)) {
|
||||
$monthMode = 'days';
|
||||
} elseif (!empty($byday) && $bysetpos) {
|
||||
$monthMode = 'weekday';
|
||||
}
|
||||
|
||||
$setposMap = ['1' => 'first', '2' => 'second', '3' => 'third', '4' => 'fourth', '-1' => 'last'];
|
||||
|
||||
$repeatFrequency = $request->old('repeat_frequency', $freq ?: '');
|
||||
$repeatInterval = $request->old('repeat_interval', $interval);
|
||||
$repeatWeekdays = $request->old('repeat_weekdays', $byday ?: [$defaultWeekday]);
|
||||
$repeatMonthDays = $request->old('repeat_month_days', $bymonthday ?: [$defaultMonthDay]);
|
||||
$repeatMonthMode = $request->old('repeat_monthly_mode', $monthMode);
|
||||
$repeatMonthWeek = $request->old('repeat_month_week', $setposMap[(string) $bysetpos] ?? $defaultMonthWeek);
|
||||
$repeatMonthWeekday = $request->old('repeat_month_weekday', $byday[0] ?? $defaultWeekday);
|
||||
|
||||
$rruleOptions = [
|
||||
'daily' => __('calendar.event.recurrence.daily'),
|
||||
'weekly' => __('calendar.event.recurrence.weekly'),
|
||||
'monthly' => __('calendar.event.recurrence.monthly'),
|
||||
'yearly' => __('calendar.event.recurrence.yearly'),
|
||||
];
|
||||
|
||||
$weekdayOptions = [
|
||||
'SU' => __('calendar.event.recurrence.weekdays.sun_short'),
|
||||
'MO' => __('calendar.event.recurrence.weekdays.mon_short'),
|
||||
'TU' => __('calendar.event.recurrence.weekdays.tue_short'),
|
||||
'WE' => __('calendar.event.recurrence.weekdays.wed_short'),
|
||||
'TH' => __('calendar.event.recurrence.weekdays.thu_short'),
|
||||
'FR' => __('calendar.event.recurrence.weekdays.fri_short'),
|
||||
'SA' => __('calendar.event.recurrence.weekdays.sat_short'),
|
||||
];
|
||||
$weekdayLong = [
|
||||
'SU' => __('calendar.event.recurrence.weekdays.sun'),
|
||||
'MO' => __('calendar.event.recurrence.weekdays.mon'),
|
||||
'TU' => __('calendar.event.recurrence.weekdays.tue'),
|
||||
'WE' => __('calendar.event.recurrence.weekdays.wed'),
|
||||
'TH' => __('calendar.event.recurrence.weekdays.thu'),
|
||||
'FR' => __('calendar.event.recurrence.weekdays.fri'),
|
||||
'SA' => __('calendar.event.recurrence.weekdays.sat'),
|
||||
];
|
||||
|
||||
return compact(
|
||||
'repeatFrequency',
|
||||
'repeatInterval',
|
||||
'repeatWeekdays',
|
||||
'repeatMonthDays',
|
||||
'repeatMonthMode',
|
||||
'repeatMonthWeek',
|
||||
'repeatMonthWeekday',
|
||||
'rruleOptions',
|
||||
'weekdayOptions',
|
||||
'weekdayLong'
|
||||
);
|
||||
}
|
||||
|
||||
private function mergeRecurrenceExtra(array $extra, ?string $rrule, string $tz, Request $request): array
|
||||
{
|
||||
if ($rrule === null) {
|
||||
|
||||
@ -58,14 +58,40 @@ class CalendarViewBuilder
|
||||
}
|
||||
|
||||
if (empty($occurrences) && !$isRecurring) {
|
||||
$startUtc = $e->meta?->start_at
|
||||
? Carbon::parse($e->meta->start_at)->utc()
|
||||
: Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
|
||||
$endUtc = $e->meta?->end_at
|
||||
? Carbon::parse($e->meta->end_at)->utc()
|
||||
: ($e->lastoccurence
|
||||
$meta = $e->meta;
|
||||
$isAllDay = (bool) ($meta?->all_day ?? false);
|
||||
$startUtc = null;
|
||||
$endUtc = null;
|
||||
|
||||
if ($isAllDay && $meta?->start_on && $meta?->end_on) {
|
||||
$allDayTz = $meta?->tzid
|
||||
?? ($meta?->extra['tzid'] ?? null)
|
||||
?? $timezone;
|
||||
$startUtc = Carbon::parse($meta->start_on->toDateString(), $allDayTz)
|
||||
->startOfDay()
|
||||
->utc();
|
||||
$endUtc = Carbon::parse($meta->end_on->toDateString(), $allDayTz)
|
||||
->startOfDay()
|
||||
->utc();
|
||||
} else {
|
||||
if ($meta?->start_at instanceof Carbon) {
|
||||
$startUtc = $meta->start_at->copy()->utc();
|
||||
} elseif ($meta?->start_at) {
|
||||
$startUtc = Carbon::parse($meta->start_at, 'UTC');
|
||||
} else {
|
||||
$startUtc = Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
|
||||
}
|
||||
|
||||
if ($meta?->end_at instanceof Carbon) {
|
||||
$endUtc = $meta->end_at->copy()->utc();
|
||||
} elseif ($meta?->end_at) {
|
||||
$endUtc = Carbon::parse($meta->end_at, 'UTC');
|
||||
} else {
|
||||
$endUtc = $e->lastoccurence
|
||||
? Carbon::createFromTimestamp($e->lastoccurence, 'UTC')
|
||||
: $startUtc->copy());
|
||||
: $startUtc->copy();
|
||||
}
|
||||
}
|
||||
|
||||
$occurrences[] = [
|
||||
'start' => $startUtc,
|
||||
@ -86,12 +112,18 @@ class CalendarViewBuilder
|
||||
$colorFg,
|
||||
$gridStartMinutes,
|
||||
$gridEndMinutes,
|
||||
$daytimeHours
|
||||
$daytimeHours,
|
||||
$spanStartUtc,
|
||||
$spanEndUtc
|
||||
) {
|
||||
$startUtc = $occ['start'];
|
||||
$endUtc = $occ['end'];
|
||||
$isAllDay = (bool) ($e->meta?->all_day ?? false);
|
||||
|
||||
if ($endUtc->lte($spanStartUtc) || $startUtc->gte($spanEndUtc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$startLocal = $startUtc->copy()->timezone($timezone);
|
||||
$endLocal = $endUtc->copy()->timezone($timezone);
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@ return [
|
||||
'notes' => 'Notes',
|
||||
'no_description' => 'No description yet.',
|
||||
'all_day_events' => 'All-day events',
|
||||
'show_more' => ':count more',
|
||||
'recurrence' => [
|
||||
'label' => 'Repeat',
|
||||
'frequency' => 'Frequency',
|
||||
|
||||
@ -82,6 +82,7 @@ return [
|
||||
'notes' => 'Note',
|
||||
'no_description' => 'Nessuna descrizione.',
|
||||
'all_day_events' => 'Eventi di tutto il giorno',
|
||||
'show_more' => 'Mostra altri :count',
|
||||
'recurrence' => [
|
||||
'label' => 'Ripeti',
|
||||
'frequency' => 'Frequenza',
|
||||
|
||||
@ -271,6 +271,16 @@ main {
|
||||
&#settings {
|
||||
/* */
|
||||
}
|
||||
|
||||
/* main content */
|
||||
section {
|
||||
|
||||
/* content footer defaults */
|
||||
footer {
|
||||
transition: padding 250ms ease-in-out;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* expanded */
|
||||
@ -404,6 +414,10 @@ main {
|
||||
@apply pl-6;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply pb-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +83,14 @@ button,
|
||||
box-shadow: var(--shadows);
|
||||
--shadows: none;
|
||||
|
||||
&.button--icon {
|
||||
@apply rounded-none h-full top-0;
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-200;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-200;
|
||||
}
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* calendar
|
||||
*
|
||||
* z-index: top is currently 10; overlapping events increment, assuming this won't be 10!
|
||||
*/
|
||||
|
||||
/**
|
||||
* month view
|
||||
**/
|
||||
*/
|
||||
.calendar.month {
|
||||
@apply grid col-span-3 pb-6 2xl:pb-8 pt-2;
|
||||
/*grid-template-rows: 2rem 1fr; */
|
||||
@ -166,7 +172,7 @@
|
||||
|
||||
/* top day bar */
|
||||
hgroup {
|
||||
@apply bg-white col-span-2 border-b-2 border-primary pl-24 sticky z-10;
|
||||
@apply bg-white col-span-2 border-b-2 border-primary pl-24 sticky z-11;
|
||||
@apply top-20;
|
||||
|
||||
span.name {
|
||||
@ -206,24 +212,29 @@
|
||||
|
||||
/* all day bar */
|
||||
ol.day {
|
||||
@apply sticky top-42 grid col-span-2 bg-white py-2 border-b border-primary col-span-2 pl-24 z-2 overflow-x-hidden;
|
||||
box-shadow: 0 0.25rem 0.5rem -0.25rem var(--color-gray-200);
|
||||
@apply sticky top-42 grid col-span-2 bg-white border-b border-primary z-10 overflow-x-hidden;
|
||||
box-shadow: 0 0.25rem 0.5rem -0.25rem rgba(0,0,0,0.15);
|
||||
padding: 0.25rem 0 0.2rem 6rem;
|
||||
|
||||
&::before {
|
||||
@apply absolute left-0 top-1/2 -translate-y-1/2 w-24 pr-4 text-right;
|
||||
@apply absolute left-0 top-1.5 w-24 pr-4 text-right;
|
||||
@apply uppercase text-xs font-mono text-secondary font-medium;
|
||||
content: 'All day';
|
||||
}
|
||||
|
||||
li.events {
|
||||
@apply flex flex-col gap-1 relative overflow-x-hidden;
|
||||
li.event-wrapper {
|
||||
@apply flex relative overflow-x-hidden;
|
||||
grid-row-start: var(--event-row);
|
||||
grid-row-end: var(--event-end);
|
||||
grid-column-start: var(--event-col);
|
||||
grid-column-end: calc(var(--event-col) + 1);
|
||||
|
||||
&.event-wrapper--overflow {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
a.event {
|
||||
@apply flex items-center text-xs gap-1 px-1 py-px font-medium rounded-sm w-full;
|
||||
@apply flex items-center text-xs gap-1 px-1 py-px mb-2px font-medium rounded-sm w-full;
|
||||
background-color: var(--event-bg);
|
||||
color: var(--event-fg);
|
||||
|
||||
@ -235,6 +246,27 @@
|
||||
background-color: color-mix(in srgb, var(--event-bg) 100%, #000 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.event-wrapper--more {
|
||||
label.event--more {
|
||||
@apply text-xs cursor-pointer rounded m-1 h-3.5 leading-none;
|
||||
@apply focus:outline-2 focus:outline-offset-2 focus:outline-cyan-600;
|
||||
|
||||
input.more-checkbox {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input.more-checkbox:checked) {
|
||||
li.event-wrapper--overflow {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
li.event-wrapper--more {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,10 +21,10 @@ dialog {
|
||||
@apply relative rounded-xl bg-white border-gray-200 p-0;
|
||||
@apply flex flex-col items-start col-start-1 translate-y-4;
|
||||
@apply overscroll-contain overflow-y-auto;
|
||||
max-height: calc(100vh - 5em);
|
||||
max-height: calc(100dvh - 5rem);
|
||||
width: 91.666667%;
|
||||
max-width: 36rem;
|
||||
transition: all 150ms cubic-bezier(0,0,.2,1);
|
||||
transition: translate 150ms cubic-bezier(0,0,.2,1);
|
||||
box-shadow: 0 1.5rem 4rem -0.5rem rgba(0, 0, 0, 0.4);
|
||||
|
||||
> .close-modal {
|
||||
@ -36,7 +36,7 @@ dialog {
|
||||
|
||||
/* modal header */
|
||||
header {
|
||||
@apply relative flex items-center px-6 h-20 z-2;
|
||||
@apply sticky top-0 bg-white flex items-center px-6 h-20 z-2;
|
||||
|
||||
h2 {
|
||||
@apply pr-12;
|
||||
@ -55,7 +55,7 @@ dialog {
|
||||
|
||||
/* footer */
|
||||
footer {
|
||||
@apply px-6 py-4 border-t-md border-gray-400 flex justify-between;
|
||||
@apply sticky bottom-0 bg-white px-6 py-4 border-t-md border-gray-400 flex justify-between;
|
||||
}
|
||||
|
||||
/* event modal with a map */
|
||||
@ -84,3 +84,60 @@ dialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tabs using <details> and <summary>
|
||||
*/
|
||||
dialog:has(.modal-tabs) {
|
||||
@apply items-start justify-items-center;
|
||||
|
||||
#modal {
|
||||
@apply relative;
|
||||
max-height: 85dvh;
|
||||
max-width: 48rem;
|
||||
top: 10dvh;
|
||||
}
|
||||
|
||||
form.modal {
|
||||
@apply !block;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-tab {
|
||||
|
||||
> summary {
|
||||
@apply sticky left-6 w-32 flex flex-row gap-2;
|
||||
top: 6rem;
|
||||
|
||||
&::before {
|
||||
@apply bg-gray-10;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
> summary { top: 8.75rem; }
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
> summary { top: 11.5rem; }
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
> summary { top: 14.25rem; }
|
||||
}
|
||||
&:nth-of-type(5) {
|
||||
> summary { top: 17rem; }
|
||||
}
|
||||
|
||||
&[open] {
|
||||
> summary::before {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
div.tab-content {
|
||||
@apply flex flex-col gap-4 pl-40 pt-4 min-h-48;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,11 @@ const SELECTORS = {
|
||||
monthlyMode: '[data-monthly-mode]',
|
||||
monthlyDays: '[data-monthly-days]',
|
||||
monthlyWeekday: '[data-monthly-weekday]',
|
||||
modalDialog: 'dialog',
|
||||
modalContent: '#modal',
|
||||
tabsRoot: '[data-tabs]',
|
||||
tabButton: '[role=\"tab\"]',
|
||||
tabPanel: '[role=\"tabpanel\"]',
|
||||
monthDay: '.calendar.month .day',
|
||||
monthDayEvent: 'a.event',
|
||||
monthDayMore: '[data-day-more]',
|
||||
@ -43,6 +48,14 @@ document.addEventListener('htmx:configRequest', (evt) => {
|
||||
if (token) evt.detail.headers['X-CSRF-TOKEN'] = token
|
||||
})
|
||||
|
||||
// modal htmx tracking
|
||||
document.addEventListener('htmx:beforeSwap', (evt) => {
|
||||
const target = evt.detail?.target || evt.target;
|
||||
if (target && target.id === 'modal') {
|
||||
target.dataset.prevUrl = window.location.href;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* global auth expiry redirect (fetch/axios)
|
||||
@ -254,6 +267,74 @@ function initRecurrenceControls(root = document) {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* modal behaviors (backdrop close + url restore)
|
||||
*/
|
||||
function initModalHandlers(root = document) {
|
||||
const dialog = root.querySelector(SELECTORS.modalDialog);
|
||||
if (!dialog || dialog.__modalWired) return;
|
||||
dialog.__modalWired = true;
|
||||
|
||||
dialog.addEventListener('click', (event) => {
|
||||
if (event.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener('close', () => {
|
||||
const modal = dialog.querySelector(SELECTORS.modalContent);
|
||||
if (!modal) return;
|
||||
|
||||
const isEvent = modal.querySelector('[data-modal-kind="event"]');
|
||||
const prevUrl = modal.dataset.prevUrl;
|
||||
modal.innerHTML = '';
|
||||
|
||||
if (isEvent && prevUrl) {
|
||||
history.replaceState({}, '', prevUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* tabs (simple modal panels)
|
||||
*/
|
||||
function initTabs(root = document) {
|
||||
root.querySelectorAll(SELECTORS.tabsRoot).forEach((tabs) => {
|
||||
if (tabs.__tabsWired) return;
|
||||
tabs.__tabsWired = true;
|
||||
|
||||
const buttons = tabs.querySelectorAll(SELECTORS.tabButton);
|
||||
const panels = tabs.querySelectorAll(SELECTORS.tabPanel);
|
||||
if (!buttons.length || !panels.length) return;
|
||||
|
||||
const activate = (button) => {
|
||||
buttons.forEach((btn) => {
|
||||
const isActive = btn === button;
|
||||
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
panels.forEach((panel) => {
|
||||
const id = button.getAttribute('aria-controls');
|
||||
panel.hidden = panel.id !== id;
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
activate(btn);
|
||||
});
|
||||
});
|
||||
|
||||
const current = tabs.querySelector('[role="tab"][aria-selected="true"]') || buttons[0];
|
||||
if (current) {
|
||||
activate(current);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* auto-scroll time views to 8am on load (when daytime hours are disabled)
|
||||
*/
|
||||
function initTimeViewAutoScroll(root = document)
|
||||
@ -266,8 +347,8 @@ function initTimeViewAutoScroll(root = document)
|
||||
if (calendar.dataset.autoscrolled === '1') return;
|
||||
if (calendar.dataset.daytimeHoursEnabled === '1') return;
|
||||
|
||||
// find the target minute (7:45am)
|
||||
const target = calendar.querySelector('[data-slot-minutes="465"]');
|
||||
// find the target minute (8:00am)
|
||||
const target = calendar.querySelector('[data-slot-minutes="480"]');
|
||||
if (!target) return;
|
||||
|
||||
// get the scroll container and offset
|
||||
@ -540,6 +621,8 @@ function initUI() {
|
||||
initColorPickers();
|
||||
initEventAllDayToggles();
|
||||
initRecurrenceControls();
|
||||
initModalHandlers();
|
||||
initTabs();
|
||||
initTimeViewAutoScroll();
|
||||
initMonthOverflow();
|
||||
}
|
||||
@ -549,9 +632,16 @@ document.addEventListener('DOMContentLoaded', initUI);
|
||||
|
||||
// rebind in htmx for swapped content
|
||||
document.addEventListener('htmx:afterSwap', (e) => {
|
||||
const target = e.detail?.target || e.target;
|
||||
if (target && target.id === 'modal') {
|
||||
target.closest('dialog')?.showModal();
|
||||
}
|
||||
|
||||
initColorPickers(e.target);
|
||||
initEventAllDayToggles(e.target);
|
||||
initRecurrenceControls(e.target);
|
||||
initModalHandlers(e.target);
|
||||
initTabs(e.target);
|
||||
initTimeViewAutoScroll(e.target);
|
||||
initMonthOverflow(e.target);
|
||||
});
|
||||
|
||||
1
resources/svg/icons/repeat.svg
Normal file
1
resources/svg/icons/repeat.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>
|
||||
|
After Width: | Height: | Size: 310 B |
@ -121,6 +121,7 @@
|
||||
type="submit"
|
||||
name="date"
|
||||
value="{{ $nav['prev'] }}"
|
||||
class="button--icon"
|
||||
aria-label="Go back 1 month">
|
||||
<x-icon-chevron-left />
|
||||
</x-button.group-button>
|
||||
@ -135,6 +136,7 @@
|
||||
type="submit"
|
||||
name="date"
|
||||
value="{{ $nav['next'] }}"
|
||||
class="button--icon"
|
||||
aria-label="Go forward 1 month">
|
||||
<x-icon-chevron-right />
|
||||
</x-button.group-button>
|
||||
|
||||
@ -2,13 +2,32 @@
|
||||
'event' => [],
|
||||
])
|
||||
|
||||
<li class="events"
|
||||
@php
|
||||
$row = (int) ($event['all_day_row'] ?? 1);
|
||||
$end = $row + 1;
|
||||
$isMore = (bool) ($event['all_day_more'] ?? false);
|
||||
$isOverflow = (bool) ($event['all_day_overflow'] ?? false);
|
||||
@endphp
|
||||
|
||||
<li class="event-wrapper{{ $isMore ? ' event-wrapper--more' : '' }}{{ $isOverflow ? ' event-wrapper--overflow' : '' }}"
|
||||
data-event-id="{{ $event['occurrence_id'] ?? $event['id'] }}"
|
||||
data-calendar-id="{{ $event['calendar_slug'] }}"
|
||||
data-calendar-id="{{ $event['calendar_slug'] ?? '' }}"
|
||||
data-more-count="{{ $event['all_day_more_count'] ?? '' }}"
|
||||
style="
|
||||
--event-col: {{ $event['start_col'] ?? 1 }};
|
||||
--event-bg: {{ $event['color'] }};
|
||||
--event-fg: {{ $event['color_fg'] }};">
|
||||
--event-row: {{ $row }};
|
||||
--event-end: {{ $end }};
|
||||
--event-bg: {{ $event['color'] ?? 'var(--color-gray-100)' }};
|
||||
--event-fg: {{ $event['color_fg'] ?? 'var(--color-primary)' }};">
|
||||
@if($isMore)
|
||||
@php
|
||||
$moreId = 'more-col-'.$event['start_col'];
|
||||
@endphp
|
||||
<label class="event event--more" for="{{ $moreId }}" tabindex="0">
|
||||
<input id="{{ $moreId }}" type="checkbox" class="more-checkbox" />
|
||||
<span>{{ __('calendar.event.show_more', ['count' => $event['all_day_more_count'] ?? 0]) }}</span>
|
||||
</label>
|
||||
@else
|
||||
@php
|
||||
$showParams = [$event['calendar_slug'], $event['id']];
|
||||
if (!empty($event['occurrence'])) {
|
||||
@ -24,4 +43,5 @@
|
||||
data-calendar="{{ $event['calendar_slug'] }}">
|
||||
<span>{{ $event['title'] }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</li>
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
<dialog
|
||||
hx-on:click="if(event.target === this) this.close()"
|
||||
hx-on:close="const modal = document.getElementById('modal'); const isEvent = modal?.querySelector('[data-modal-kind=\'event\']'); const prevUrl = modal?.dataset?.prevUrl; modal.innerHTML=''; if (isEvent && prevUrl) history.replaceState({}, '', prevUrl);"
|
||||
>
|
||||
<dialog>
|
||||
<div id="modal"
|
||||
hx-target="this"
|
||||
hx-on::before-swap="this.dataset.prevUrl = window.location.href"
|
||||
hx-on::after-swap="this.closest('dialog')?.showModal()"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight">
|
||||
{{ $event->exists ? __('Edit Event') : __('Create Event') }}
|
||||
{{ $event->exists ? __('Edit event details') : __('Create a new event') }}
|
||||
</h2>
|
||||
|
||||
{{-- “Back” breadcrumb --}}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<x-modal.content>
|
||||
<x-modal.content class="modal-tabs">
|
||||
<x-modal.title>
|
||||
<h2>{{ $event->exists ? __('Edit Event') : __('Create Event') }}</h2>
|
||||
<h2>{{ $event->exists ? __('Edit event details') : __('Create a new event') }}</h2>
|
||||
</x-modal.title>
|
||||
<x-modal.body>
|
||||
@include('event.partials.form', [
|
||||
@ -17,7 +17,7 @@
|
||||
{{ __('common.cancel') }}
|
||||
</x-button>
|
||||
<x-button variant="primary" type="submit" form="event-form">
|
||||
{{ $event->exists ? __('Save') : __('Create') }}
|
||||
{{ $event->exists ? __('common.save') : __('Create event') }}
|
||||
</x-button>
|
||||
</x-modal.footer>
|
||||
</x-modal.content>
|
||||
|
||||
@ -3,83 +3,6 @@
|
||||
$formAction = $event->exists
|
||||
? route('calendar.event.update', [$calendar, $event])
|
||||
: route('calendar.event.store', $calendar);
|
||||
$rruleValue = trim((string) ($rrule ?? ''));
|
||||
$rruleParts = [];
|
||||
foreach (array_filter(explode(';', $rruleValue)) as $chunk) {
|
||||
if (!str_contains($chunk, '=')) continue;
|
||||
[$key, $value] = explode('=', $chunk, 2);
|
||||
$rruleParts[strtoupper($key)] = $value;
|
||||
}
|
||||
|
||||
$freq = strtolower($rruleParts['FREQ'] ?? '');
|
||||
$interval = (int) ($rruleParts['INTERVAL'] ?? 1);
|
||||
if ($interval < 1) $interval = 1;
|
||||
|
||||
$byday = array_filter(explode(',', $rruleParts['BYDAY'] ?? ''));
|
||||
$bymonthday = array_filter(explode(',', $rruleParts['BYMONTHDAY'] ?? ''));
|
||||
$bysetpos = $rruleParts['BYSETPOS'] ?? null;
|
||||
|
||||
$startDefault = old('start_at', $start ?? null);
|
||||
$startDate = $startDefault ? \Carbon\Carbon::parse($startDefault) : \Carbon\Carbon::now();
|
||||
$weekdayMap = [
|
||||
'Sun' => 'SU',
|
||||
'Mon' => 'MO',
|
||||
'Tue' => 'TU',
|
||||
'Wed' => 'WE',
|
||||
'Thu' => 'TH',
|
||||
'Fri' => 'FR',
|
||||
'Sat' => 'SA',
|
||||
];
|
||||
$defaultWeekday = $weekdayMap[$startDate->format('D')] ?? 'MO';
|
||||
$defaultMonthDay = (int) $startDate->format('j');
|
||||
|
||||
$weekMap = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'];
|
||||
$startWeek = $startDate->copy();
|
||||
$isLastWeek = $startWeek->copy()->addWeek()->month !== $startWeek->month;
|
||||
$defaultMonthWeek = $isLastWeek ? 'last' : ($weekMap[$startDate->weekOfMonth] ?? 'first');
|
||||
|
||||
$monthMode = 'days';
|
||||
if (!empty($bymonthday)) {
|
||||
$monthMode = 'days';
|
||||
} elseif (!empty($byday) && $bysetpos) {
|
||||
$monthMode = 'weekday';
|
||||
}
|
||||
|
||||
$repeatFrequency = old('repeat_frequency', $freq ?: '');
|
||||
$repeatInterval = old('repeat_interval', $interval);
|
||||
$repeatWeekdays = old('repeat_weekdays', $byday ?: [$defaultWeekday]);
|
||||
$repeatMonthDays = old('repeat_month_days', $bymonthday ?: [$defaultMonthDay]);
|
||||
$repeatMonthMode = old('repeat_monthly_mode', $monthMode);
|
||||
|
||||
$setposMap = ['1' => 'first', '2' => 'second', '3' => 'third', '4' => 'fourth', '-1' => 'last'];
|
||||
$repeatMonthWeek = old('repeat_month_week', $setposMap[(string) $bysetpos] ?? $defaultMonthWeek);
|
||||
$repeatMonthWeekday = old('repeat_month_weekday', $byday[0] ?? $defaultWeekday);
|
||||
|
||||
$rruleOptions = [
|
||||
'daily' => __('calendar.event.recurrence.daily'),
|
||||
'weekly' => __('calendar.event.recurrence.weekly'),
|
||||
'monthly' => __('calendar.event.recurrence.monthly'),
|
||||
'yearly' => __('calendar.event.recurrence.yearly'),
|
||||
];
|
||||
|
||||
$weekdayOptions = [
|
||||
'SU' => __('calendar.event.recurrence.weekdays.sun_short'),
|
||||
'MO' => __('calendar.event.recurrence.weekdays.mon_short'),
|
||||
'TU' => __('calendar.event.recurrence.weekdays.tue_short'),
|
||||
'WE' => __('calendar.event.recurrence.weekdays.wed_short'),
|
||||
'TH' => __('calendar.event.recurrence.weekdays.thu_short'),
|
||||
'FR' => __('calendar.event.recurrence.weekdays.fri_short'),
|
||||
'SA' => __('calendar.event.recurrence.weekdays.sat_short'),
|
||||
];
|
||||
$weekdayLong = [
|
||||
'SU' => __('calendar.event.recurrence.weekdays.sun'),
|
||||
'MO' => __('calendar.event.recurrence.weekdays.mon'),
|
||||
'TU' => __('calendar.event.recurrence.weekdays.tue'),
|
||||
'WE' => __('calendar.event.recurrence.weekdays.wed'),
|
||||
'TH' => __('calendar.event.recurrence.weekdays.thu'),
|
||||
'FR' => __('calendar.event.recurrence.weekdays.fri'),
|
||||
'SA' => __('calendar.event.recurrence.weekdays.sat'),
|
||||
];
|
||||
@endphp
|
||||
|
||||
<form method="POST" id="event-form" action="{{ $formAction }}" class="settings modal">
|
||||
@ -88,6 +11,9 @@
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="event-tabs" data-tabs>
|
||||
<div class="tab-panels">
|
||||
<section id="tab-details" role="tabpanel" aria-labelledby="tab-btn-details">
|
||||
{{-- Title --}}
|
||||
<div class="input-row input-row--1">
|
||||
<div class="input-cell">
|
||||
@ -188,14 +114,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Recurrence (advanced) --}}
|
||||
<section id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
|
||||
<div class="input-row input-row--1">
|
||||
<div class="input-cell">
|
||||
<details>
|
||||
<summary class="cursor-pointer text-sm text-gray-600">
|
||||
{{ __('calendar.event.recurrence.label') }}
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<x-input.label for="repeat_frequency" :value="__('calendar.event.recurrence.frequency')" />
|
||||
<x-input.select
|
||||
@ -300,7 +223,32 @@
|
||||
{{ __('calendar.event.recurrence.yearly_hint') }}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-invitees" role="tabpanel" aria-labelledby="tab-btn-invitees" hidden>
|
||||
<div class="input-row input-row--1">
|
||||
<div class="input-cell">
|
||||
<p class="text-sm text-gray-600">More to come</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="tab-buttons" role="tablist" aria-orientation="vertical">
|
||||
<button type="button" id="tab-btn-details" role="tab" aria-controls="tab-details" aria-selected="true">
|
||||
<x-icon-info-circle width="20" />
|
||||
<span>Details</span>
|
||||
</button>
|
||||
<button type="button" id="tab-btn-repeat" role="tab" aria-controls="tab-repeat" aria-selected="false">
|
||||
<x-icon-repeat width="20" />
|
||||
<span>Repeat</span>
|
||||
</button>
|
||||
<button type="button" id="tab-btn-invitees" role="tab" aria-controls="tab-invitees" aria-selected="false">
|
||||
<x-icon-user-circle width="20" />
|
||||
<span>Invitees</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user