An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
502 lines
28 KiB
PHP
502 lines
28 KiB
PHP
{{-- A date field with M3's date pickers: docked under the field, modal, or modal text input.
|
||
|
||
`mode` picks M3's three:
|
||
|
||
- `docked` (the default): a text field that takes a typed date in the locale's numeric format
|
||
(`13.09.2026` in `de`, `09/13/2026` in `en-US`), with the calendar dropping open under it —
|
||
on a press of the field, its calendar button, or ArrowDown. On a compact window (below
|
||
600px, `medium`) the calendar opens as the modal picker instead, where a docked one would not
|
||
fit.
|
||
- `modal`: the field only shows the date; pressing it (or Enter, Space, ArrowDown) opens the
|
||
calendar in a dialog, with a pencil to switch to typing.
|
||
- `input`: the same dialog, opened on its text field, with a calendar icon to switch back.
|
||
|
||
A `range` picker on a compact window (below `medium`) is M3's full-screen range picker instead
|
||
of the 360px dialog: an app bar with a close button and **Save**, the range as the headline
|
||
under the supporting text, and the months in one vertically scrolling list rather than stepped
|
||
one at a time. Nothing changes for a single date, or for a docked picker at `medium` and up.
|
||
|
||
The calendar and the dialog's text field pick a draft; OK (or Enter on a day) makes it the
|
||
value, Cancel or Escape leaves the value alone and returns focus to the field. What is typed
|
||
into the docked field is the value as soon as it is a whole, allowed date.
|
||
|
||
`wire:model` stores `Y-m-d` strings (`x-model` without Livewire). `range` picks a start and an
|
||
end, bound as one array, `['start' => 'Y-m-d', 'end' => 'Y-m-d']` (either may be null) —
|
||
one property rather than two, because a `wire:model` names one property, Livewire sends a
|
||
range as one update so `after_or_equal:period.start` validates against the end it came
|
||
with, and the errors for `period`, `period.start` and `period.end` all belong to this field.
|
||
`min` and `max` (`Y-m-d` or a date object) disable the days outside them and keep the
|
||
keyboard inside them. `label`, `hint`, `icon`, `variant` (`outlined`, `filled`), `size` and
|
||
`full` (the 40rem bound taken off) are the field's; `clearable` adds a button that empties it
|
||
(a date, or both ends of a range) once it holds one; `name` adds hidden inputs carrying
|
||
`Y-m-d` for a plain form post. `class` and
|
||
`style` land on the root; every other attribute (`required`, `disabled`, `readonly`) reaches
|
||
the text field. Errors under
|
||
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||
|
||
Month and weekday names, the first day of the week and the typed format come from `Intl` for
|
||
`app()->getLocale()` (resources/js/datepicker.js). An application that lets each person choose
|
||
overrides the last two: `week-start` is the first day of the week, 0 (Sunday) to 6 (Saturday),
|
||
and `format` the typed and displayed format, `dd`, `MM` and `yyyy` in any order around one
|
||
delimiter (`.`, `/` or `-`: `dd.MM.yyyy`, `MM/dd/yyyy`, `yyyy-MM-dd`). The field, the typed-date
|
||
reader, the calendar's columns and weekday header, Home and End, and the dialog's text fields
|
||
all follow them; the names stay the locale's, and `wire:model` still stores `Y-m-d`. A value
|
||
that is neither (`week-start="7"`, `format="d.M.yy"`) is ignored, as `null` is. Replaces
|
||
ReStride's flatpickr picker: its `config` becomes `min`, `max`, `range` and `mode`.
|
||
|
||
M3's date pickers (DatePickerModalTokens and DateInputModalTokens from androidx Compose
|
||
Material 3, androidx commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, with the layout of
|
||
DatePicker.kt, DateRangePicker.kt and DateInput.kt; the docked picker's values from
|
||
material-web's md-comp-date-picker-docked tokens, v0_192, as Compose has none; all Apache-2.0):
|
||
surface-container-high at elevation 3, 360px wide, the extra-large corner (modal) or large
|
||
(docked), 40px days in 48px cells, today outlined in primary, the chosen day in primary and a
|
||
range's middle in secondary-container. The styles are resources/css/components/datepicker.css;
|
||
the year, the menu buttons and the list options render `md-state-layer md-focus-ring`, the
|
||
month's announcement `md-visually-hidden`, and nothing else here writes a class. --}}
|
||
|
||
@props([
|
||
'label' => null,
|
||
'hint' => null,
|
||
'icon' => null,
|
||
'size' => 'md',
|
||
'variant' => null,
|
||
'mode' => 'docked',
|
||
'range' => false,
|
||
'min' => null,
|
||
'max' => null,
|
||
'value' => null,
|
||
'clearable' => false,
|
||
'weekStart' => null,
|
||
'format' => null,
|
||
'full' => false,
|
||
])
|
||
|
||
@php
|
||
$model = $attributes->wire('model')->value() ?: null;
|
||
$mode = in_array($mode, ['docked', 'modal', 'input'], true) ? $mode : 'docked';
|
||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|datepicker'), 0, 12);
|
||
$anchor = '--material-datepicker-'.preg_replace('/[^A-Za-z0-9_-]/', '-', $id);
|
||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||
$errorKey = $field::key($model, $attributes->get('name'));
|
||
$messages = $field::messages($errors ?? null, $errorKey, wildcard: true);
|
||
$locale = str_replace('_', '-', app()->getLocale());
|
||
$weekStart = (is_int($weekStart) || is_string($weekStart)) && preg_match('/^[0-6]\z/', (string) $weekStart) === 1 ? (int) $weekStart : null;
|
||
$format = is_string($format) && preg_match('/^(dd|MM|yyyy)([.\/-])(dd|MM|yyyy)\2(dd|MM|yyyy)\z/', $format, $units) === 1 && count(array_unique([$units[1], $units[3], $units[4]])) === 3
|
||
? $format
|
||
: null;
|
||
|
||
$toIso = function (mixed $date): ?string {
|
||
if ($date instanceof \DateTimeInterface) {
|
||
return $date->format('Y-m-d');
|
||
}
|
||
|
||
if (is_string($date) && preg_match('/^(\d{4})-(\d{2})-(\d{2})(?:$|[T ])/', $date, $match) === 1 && checkdate((int) $match[2], (int) $match[3], (int) $match[1])) {
|
||
return substr($date, 0, 10);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
// Rendered as the bound property already says, so the field does not change once Alpine starts.
|
||
$current = $field::bound($model, $value);
|
||
$current = $range
|
||
? ['start' => $toIso(data_get($current, 'start')), 'end' => $toIso(data_get($current, 'end'))]
|
||
: $toIso($current);
|
||
|
||
// The typed format: `format`, or as resources/js/datepicker.js derives it, from ICU's short date when PHP has intl.
|
||
$pattern = $format ?? 'yyyy-MM-dd';
|
||
if ($format === null && class_exists(\IntlDateFormatter::class)) {
|
||
$short = (string) (new \IntlDateFormatter(str_replace('-', '_', $locale), \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE))->getPattern();
|
||
$candidate = rtrim(str_replace('My', 'M/y', (string) preg_replace(['/[^dMy\/\-.]/', '/d{1,2}/', '/M{1,2}/', '/y{1,4}/'], ['', 'dd', 'MM', 'yyyy'], $short)), '.');
|
||
if (preg_match('/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[\/\-.][dMy]+[\/\-.][dMy]+$/', $candidate) === 1) {
|
||
$pattern = $candidate;
|
||
}
|
||
}
|
||
$typed = fn (?string $date): string => $date === null ? '' : strtr($pattern, ['yyyy' => substr($date, 0, 4), 'MM' => substr($date, 5, 2), 'dd' => substr($date, 8, 2)]);
|
||
$display = $range
|
||
? ($current['start'] === null && $current['end'] === null ? '' : $typed($current['start']).' – '.$typed($current['end']))
|
||
: $typed($current);
|
||
|
||
$title = $range ? __('Select dates') : __('Select date');
|
||
$inputTitle = $range ? __('Enter dates') : __('Select date');
|
||
$fieldName = $attributes->get('name');
|
||
$invalid = $messages !== [];
|
||
|
||
$config = [
|
||
'locale' => $locale,
|
||
'mode' => $mode,
|
||
'range' => (bool) $range,
|
||
'min' => $toIso($min),
|
||
'max' => $toIso($max),
|
||
'weekStart' => $weekStart,
|
||
'format' => $format,
|
||
'disabled' => (bool) $attributes->get('disabled'),
|
||
'readonly' => (bool) $attributes->get('readonly'),
|
||
'strings' => [
|
||
'selected' => __('Selected date'),
|
||
'entered' => __('Entered date'),
|
||
'start' => __('Start date'),
|
||
'end' => __('End date'),
|
||
'pattern' => __('Date does not match expected pattern: :pattern'),
|
||
'yearRange' => __('Date out of expected year range :start - :end'),
|
||
'notAllowed' => __('Date not allowed: :date'),
|
||
'invalidRange' => __('Invalid date range input'),
|
||
],
|
||
];
|
||
|
||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||
@endphp
|
||
|
||
<div
|
||
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||
x-data="materialDatepicker({
|
||
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
||
...@js($config),
|
||
})"
|
||
@if ($model === null) x-modelable="value" @endif
|
||
x-on:focusout="leave($event)"
|
||
x-on:pointerdown.outside="open && presentation === 'docked' && cancel(false)"
|
||
data-md-datepicker
|
||
>
|
||
<div style="anchor-name: {{ $anchor }}">
|
||
<x-livewire-material::field
|
||
:$id
|
||
:$label
|
||
:$icon
|
||
:$size
|
||
:$variant
|
||
:$full
|
||
:data-md-invalid="$invalid ? '' : null"
|
||
:data-md-readonly="$attributes->get('readonly') ? '' : null"
|
||
x-bind:data-md-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
||
>
|
||
<input
|
||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['class', 'style', 'id', 'wire:key', 'x-model', 'name', 'value', 'placeholder', 'type']) }}
|
||
x-ref="input"
|
||
id="{{ $id }}"
|
||
type="text"
|
||
role="combobox"
|
||
aria-haspopup="dialog"
|
||
aria-controls="{{ $id }}-picker"
|
||
aria-expanded="false"
|
||
x-bind:aria-expanded="open.toString()"
|
||
aria-describedby="{{ $id }}-support"
|
||
@if ($invalid) aria-invalid="true" @endif
|
||
x-bind:aria-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? 'true' : null"
|
||
autocomplete="off"
|
||
value="{{ $display }}"
|
||
placeholder=" "
|
||
data-md-field-control
|
||
x-model="text"
|
||
@if ($mode === 'docked')
|
||
x-bind:placeholder="placeholder"
|
||
x-on:input="typeInField()"
|
||
x-on:change="commitField()"
|
||
x-on:keydown.enter="commitField()"
|
||
x-on:click="show(false)"
|
||
x-on:keydown.arrow-down.prevent="open ? focusInside() : show()"
|
||
x-on:keydown.escape="if (open) { $event.preventDefault(); $event.stopPropagation(); cancel(); }"
|
||
@else
|
||
readonly
|
||
x-on:click="show()"
|
||
x-on:keydown.enter.prevent="show()"
|
||
x-on:keydown.space.prevent="show()"
|
||
x-on:keydown.arrow-down.prevent="show()"
|
||
@endif
|
||
/>
|
||
|
||
<x-slot:trailing>
|
||
@if ($clearable)
|
||
<button
|
||
type="button"
|
||
aria-label="{{ __('Clear') }}"
|
||
x-on:click="clear()"
|
||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||
data-md-field-trailing
|
||
data-md-field-button
|
||
data-md-field-clear
|
||
>
|
||
<x-livewire-material::icon name="close" :size="$fieldIconSize" />
|
||
</button>
|
||
@endif
|
||
|
||
<button
|
||
type="button"
|
||
data-md-field-trailing
|
||
data-md-field-button
|
||
aria-label="{{ $title }}"
|
||
aria-haspopup="dialog"
|
||
aria-controls="{{ $id }}-picker"
|
||
x-bind:aria-expanded="open.toString()"
|
||
x-on:click="open ? cancel() : show()"
|
||
@if ($mode !== 'docked') tabindex="-1" @endif
|
||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||
data-md-datepicker-toggle
|
||
>
|
||
<x-livewire-material::icon name="calendar_today" :size="$fieldIconSize" />
|
||
</button>
|
||
</x-slot:trailing>
|
||
</x-livewire-material::field>
|
||
</div>
|
||
|
||
<div id="{{ $id }}-support" data-md-datepicker-support data-md-size="{{ in_array($size, ['sm', 'xs'], true) ? $size : 'md' }}">
|
||
<p x-cloak x-show="fieldError !== ''" x-text="fieldError" role="alert" data-md-datepicker-error></p>
|
||
|
||
@if ($invalid)
|
||
<div x-show="fieldError === ''" role="alert" data-md-datepicker-error>
|
||
@foreach ($messages as $message)
|
||
<p>{{ $message }}</p>
|
||
@endforeach
|
||
</div>
|
||
@elseif (filled($hint))
|
||
<p x-show="fieldError === ''">{{ $hint }}</p>
|
||
@endif
|
||
</div>
|
||
|
||
@if (filled($fieldName))
|
||
@if ($range)
|
||
<input type="hidden" name="{{ $fieldName }}[start]" value="{{ $current['start'] }}" x-bind:value="serialised.start ?? ''" />
|
||
<input type="hidden" name="{{ $fieldName }}[end]" value="{{ $current['end'] }}" x-bind:value="serialised.end ?? ''" />
|
||
@else
|
||
<input type="hidden" name="{{ $fieldName }}" value="{{ $current }}" x-bind:value="serialised" />
|
||
@endif
|
||
@endif
|
||
|
||
<dialog
|
||
wire:ignore
|
||
x-ref="dialog"
|
||
id="{{ $id }}-picker"
|
||
popover="manual"
|
||
aria-label="{{ $title }}"
|
||
style="position-anchor: {{ $anchor }}"
|
||
x-bind:data-md-presentation="presentation"
|
||
x-on:cancel.prevent="cancel()"
|
||
x-on:click.self="presentation === 'modal' && cancel()"
|
||
x-on:keydown.escape.prevent.stop="cancel()"
|
||
data-md-datepicker-picker
|
||
>
|
||
<div tabindex="-1" data-md-datepicker-surface>
|
||
{{-- M3's full-screen range picker (compact): an app bar with a close button and Save,
|
||
then the supporting text and the range as the headline, over a divider. --}}
|
||
<div data-md-datepicker-full x-cloak x-show="presentation === 'full'">
|
||
<div data-md-datepicker-app-bar>
|
||
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="cancel()" data-md-datepicker-close />
|
||
|
||
<span data-md-datepicker-app-bar-actions>
|
||
<span x-show="! typing">
|
||
<x-livewire-material::button icon="edit" :tooltip="__('Switch to text input mode')" x-on:click="toggleTyping()" data-md-datepicker-switch />
|
||
</span>
|
||
<span x-show="typing">
|
||
<x-livewire-material::button icon="date_range" :tooltip="__('Switch to calendar input mode')" x-on:click="toggleTyping()" data-md-datepicker-switch />
|
||
</span>
|
||
|
||
<x-livewire-material::button :label="__('Save')" x-on:click="confirm()" data-md-datepicker-save />
|
||
</span>
|
||
</div>
|
||
|
||
<div data-md-datepicker-full-headline>
|
||
<p data-md-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p>
|
||
<p data-md-datepicker-headline aria-live="polite" x-text="headline"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div data-md-datepicker-header x-show="presentation === 'modal'" @if ($range) data-md-range @endif>
|
||
<p data-md-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p>
|
||
|
||
<div data-md-datepicker-headline-row>
|
||
<p data-md-datepicker-headline aria-live="polite" x-text="headline"></p>
|
||
|
||
<span x-show="! typing">
|
||
<x-livewire-material::button icon="edit" :tooltip="__('Switch to text input mode')" x-on:click="toggleTyping()" data-md-datepicker-switch />
|
||
</span>
|
||
<span x-show="typing">
|
||
<x-livewire-material::button icon="date_range" :tooltip="__('Switch to calendar input mode')" x-on:click="toggleTyping()" data-md-datepicker-switch />
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="! typing" x-on:scroll="extendMonths($event)" data-md-datepicker-calendar>
|
||
<span id="{{ $id }}-month" class="md-visually-hidden" aria-live="polite" x-text="monthYear"></span>
|
||
|
||
<div data-md-datepicker-nav x-show="presentation === 'modal'">
|
||
<button
|
||
type="button"
|
||
class="md-state-layer md-focus-ring"
|
||
data-md-datepicker-menu-button="years"
|
||
x-on:click="toggleView('years')"
|
||
x-bind:aria-expanded="(view === 'years').toString()"
|
||
x-bind:aria-label="monthYear + ', ' + @js(__('Switch to selecting a year'))"
|
||
>
|
||
<span x-text="monthYear"></span>
|
||
<x-livewire-material::icon name="arrow_drop_down" size="18" data-md-datepicker-menu-arrow />
|
||
</button>
|
||
|
||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" data-md-datepicker-previous />
|
||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" data-md-datepicker-next />
|
||
</span>
|
||
</div>
|
||
|
||
<div data-md-datepicker-nav data-md-docked x-show="presentation === 'docked'">
|
||
{{-- Months and years: the same stepper either side of a menu button, a step of
|
||
1 or 12 apart. --}}
|
||
@foreach ([
|
||
['unit' => 'months', 'step' => 1, 'label' => 'monthLabel', 'previous' => __('Previous month'), 'next' => __('Next month'), 'switch' => __('Switch to selecting a month')],
|
||
['unit' => 'years', 'step' => 12, 'label' => 'yearLabel', 'previous' => __('Previous year'), 'next' => __('Next year'), 'switch' => __('Switch to selecting a year')],
|
||
] as $stepper)
|
||
<span data-md-datepicker-stepper>
|
||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||
<x-livewire-material::button icon="chevron_left" :tooltip="$stepper['previous']" x-on:click="step(-{{ $stepper['step'] }})" x-bind:disabled="view !== 'days' || ! canStep(-{{ $stepper['step'] }})" />
|
||
</span>
|
||
<button
|
||
type="button"
|
||
class="md-state-layer md-focus-ring"
|
||
data-md-datepicker-menu-button="{{ $stepper['unit'] }}"
|
||
x-on:click="toggleView('{{ $stepper['unit'] }}')"
|
||
x-bind:aria-expanded="(view === '{{ $stepper['unit'] }}').toString()"
|
||
x-bind:aria-label="{{ $stepper['label'] }} + ', ' + @js($stepper['switch'])"
|
||
>
|
||
<span x-text="{{ $stepper['label'] }}"></span>
|
||
<x-livewire-material::icon name="arrow_drop_down" size="18" data-md-datepicker-menu-arrow />
|
||
</button>
|
||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||
<x-livewire-material::button icon="chevron_right" :tooltip="$stepper['next']" x-on:click="step({{ $stepper['step'] }})" x-bind:disabled="view !== 'days' || ! canStep({{ $stepper['step'] }})" />
|
||
</span>
|
||
</span>
|
||
@endforeach
|
||
</div>
|
||
|
||
<table role="grid" aria-labelledby="{{ $id }}-month" x-show="view === 'days'" x-on:keydown="gridKey($event)" data-md-datepicker-grid>
|
||
<thead>
|
||
<tr>
|
||
<template x-for="weekday in weekdays" :key="weekday.long">
|
||
<th scope="col" x-bind:abbr="weekday.long" x-text="weekday.narrow"></th>
|
||
</template>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-for="row in rows" :key="row.key">
|
||
<tr>
|
||
{{-- Full screen: a month's name heads its weeks, M3's month label. --}}
|
||
<template x-if="row.label">
|
||
<th colspan="7" scope="colgroup" x-text="row.label" data-md-datepicker-month-label></th>
|
||
</template>
|
||
|
||
<template x-for="(cell, column) in (row.cells ?? [])" :key="column">
|
||
<td
|
||
role="gridcell"
|
||
x-bind:tabindex="cell.blank ? null : (cell.focused ? 0 : -1)"
|
||
x-bind:aria-label="cell.blank ? null : cell.name"
|
||
x-bind:aria-selected="! cell.blank && (cell.selected || cell.between) ? 'true' : null"
|
||
x-bind:aria-disabled="! cell.blank && cell.disabled ? 'true' : null"
|
||
x-bind:aria-current="! cell.blank && cell.today ? 'date' : null"
|
||
x-bind:data-md-value="cell.blank ? null : cell.value"
|
||
x-bind:data-md-blank="cell.blank ? '' : null"
|
||
x-bind:data-md-outside="cell.outside ? '' : null"
|
||
x-bind:data-md-today="cell.today ? '' : null"
|
||
x-bind:data-md-selected="cell.selected ? '' : null"
|
||
x-bind:data-md-start="cell.start ? '' : null"
|
||
x-bind:data-md-end="cell.end ? '' : null"
|
||
x-bind:data-md-between="cell.between ? '' : null"
|
||
x-on:click="choose(cell)"
|
||
data-md-datepicker-day
|
||
><span x-text="cell.blank ? '' : cell.label"></span></td>
|
||
</template>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div role="listbox" aria-label="{{ __('Years') }}" x-show="view === 'years' && presentation === 'modal'" x-on:keydown="listKey($event, 3)" data-md-datepicker-list data-md-datepicker-years>
|
||
<template x-for="year in years" :key="year.value">
|
||
<button
|
||
type="button"
|
||
role="option"
|
||
x-bind:aria-selected="year.selected.toString()"
|
||
x-bind:tabindex="year.selected ? 0 : -1"
|
||
x-bind:data-md-current="year.current ? '' : null"
|
||
x-on:click="showMonthOf(year.value, +shown.slice(5, 7))"
|
||
x-text="year.label"
|
||
class="md-state-layer md-focus-ring"
|
||
data-md-datepicker-year
|
||
></button>
|
||
</template>
|
||
</div>
|
||
|
||
<div role="listbox" aria-label="{{ __('Months') }}" x-show="view === 'months' && presentation === 'docked'" x-on:keydown="listKey($event)" data-md-datepicker-list data-md-datepicker-menu>
|
||
<template x-for="month in months" :key="month.value">
|
||
<button
|
||
type="button"
|
||
role="option"
|
||
x-bind:aria-selected="month.selected.toString()"
|
||
x-bind:aria-disabled="month.disabled ? 'true' : null"
|
||
x-bind:tabindex="month.selected ? 0 : -1"
|
||
x-on:click="month.disabled || showMonthOf(+shown.slice(0, 4), month.value)"
|
||
class="md-state-layer md-focus-ring"
|
||
data-md-datepicker-option
|
||
>
|
||
<x-livewire-material::icon name="check" data-md-datepicker-check />
|
||
<span x-text="month.label"></span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
|
||
<div role="listbox" aria-label="{{ __('Years') }}" x-show="view === 'years' && presentation === 'docked'" x-on:keydown="listKey($event)" data-md-datepicker-list data-md-datepicker-menu>
|
||
<template x-for="year in years" :key="year.value">
|
||
<button
|
||
type="button"
|
||
role="option"
|
||
x-bind:aria-selected="year.selected.toString()"
|
||
x-bind:tabindex="year.selected ? 0 : -1"
|
||
x-on:click="showMonthOf(year.value, +shown.slice(5, 7))"
|
||
class="md-state-layer md-focus-ring"
|
||
data-md-datepicker-option
|
||
>
|
||
<x-livewire-material::icon name="check" data-md-datepicker-check />
|
||
<span x-text="year.label"></span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="typing" data-md-datepicker-entry>
|
||
<div @if ($range) data-md-range @endif data-md-datepicker-entry-fields>
|
||
@foreach ($range ? [
|
||
['id' => "{$id}-entry", 'label' => __('Start date'), 'model' => 'entry', 'ref' => true],
|
||
['id' => "{$id}-entry-end", 'label' => __('End date'), 'model' => 'entryEnd', 'ref' => false],
|
||
] : [
|
||
['id' => "{$id}-entry", 'label' => __('Date'), 'model' => 'entry', 'ref' => true],
|
||
] as $entry)
|
||
<x-livewire-material::field id="{{ $entry['id'] }}" :label="$entry['label']" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||
<input
|
||
id="{{ $entry['id'] }}"
|
||
type="text"
|
||
autocomplete="off"
|
||
placeholder=" "
|
||
x-bind:placeholder="format.placeholder"
|
||
x-bind:aria-invalid="entryError !== '' ? 'true' : null"
|
||
aria-describedby="{{ $id }}-entry-support"
|
||
@if ($entry['ref']) x-ref="entry" @endif
|
||
x-model="{{ $entry['model'] }}"
|
||
x-on:input="typeEntry()"
|
||
x-on:keydown.enter.prevent="confirm()"
|
||
data-md-field-control
|
||
/>
|
||
</x-livewire-material::field>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div id="{{ $id }}-entry-support" data-md-datepicker-support>
|
||
<p x-show="entryError !== ''" x-text="entryError" role="alert" data-md-datepicker-error></p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- The full-screen picker confirms from its app bar, so it draws no action row. --}}
|
||
<div x-show="presentation !== 'full' && (view === 'days' || typing || presentation === 'modal')" data-md-datepicker-actions>
|
||
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-md-datepicker-cancel />
|
||
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-md-datepicker-confirm />
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
</div>
|