fix: replace native date inputs with custom DateInput/DateTimeInput components

Native <input type='date'> and <input type='datetime-local'> render
their display format (mm/dd/yyyy vs dd/mm/yyyy, 12h vs 24h) based on
browser/OS locale, ignoring HTML lang attributes in Firefox and
inconsistently in Chrome. The previous lang=en-GB fix was unreliable.

Create DateInput.svelte and DateTimeInput.svelte components that show
dd/mm/yyyy (and DD/MM/YYYY HH:MM for datetime) by formatting the ISO
value in JS, while delegating the actual picker to a hidden native
input triggered via showPicker(). Supported in Chrome 99+, Firefox
101+, Safari 16+ (covers all modern browsers).

Updated 8 component files across CollectionModal, ChecklistModal,
NoteModal, ImmichSelect, CollectionMap, TransportationDetails,
LodgingDetails, and LocationVisits.
This commit is contained in:
2026-03-06 15:14:02 +00:00
parent 52299c1ff2
commit 04fb1dfb40
10 changed files with 218 additions and 79 deletions

View File

@@ -5,6 +5,7 @@
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
import DateInput from '$lib/components/DateInput.svelte';
import CheckboxIcon from '~icons/mdi/checkbox-multiple-marked-outline';
@@ -290,15 +291,14 @@
>
</div>
{/if}
<input
type="date"
<DateInput
id="date"
name="date"
readonly={isReadOnly}
min={constrainDates ? collection.start_date : ''}
max={constrainDates ? collection.end_date : ''}
bind:value={newChecklist.date}
class="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
/>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
import DateInput from '$lib/components/DateInput.svelte';
import { addToast } from '$lib/toasts';
import { copyToClipboard } from '$lib/index';
import type { Collection, ContentImage, SlimCollection } from '$lib/types';
@@ -343,12 +344,10 @@
{$t('adventures.start_date')}
</span>
</label>
<input
type="date"
<DateInput
id="start_date"
name="start_date"
bind:value={collection.start_date}
class="input input-bordered w-full"
/>
</div>
@@ -360,12 +359,10 @@
{$t('adventures.end_date')}
</span>
</label>
<input
type="date"
<DateInput
id="end_date"
name="end_date"
bind:value={collection.end_date}
class="input input-bordered w-full"
/>
</div>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let value: string | null = ''; // ISO yyyy-mm-dd
export let id: string = '';
export let name: string = '';
export let inputClass: string = 'input input-bordered w-full';
export let disabled: boolean = false;
export let min: string | null | undefined = undefined;
export let max: string | null | undefined = undefined;
export let required: boolean = false;
export let readonly: boolean = false;
const dispatch = createEventDispatcher<{ change: Event }>();
let nativeInput: HTMLInputElement;
$: normalizedValue = value ?? '';
$: normalizedMin = min ?? undefined;
$: normalizedMax = max ?? undefined;
$: displayDate = formatDate(normalizedValue);
function formatDate(iso: string): string {
if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return '';
const [y, m, d] = iso.split('-');
return `${d}/${m}/${y}`;
}
function openPicker() {
if (!disabled && !readonly) nativeInput?.showPicker?.();
}
function handleChange(e: Event) {
value = (e.currentTarget as HTMLInputElement).value;
dispatch('change', e);
}
</script>
<div class="relative w-full">
<button
type="button"
class="{inputClass} text-left flex items-center justify-between"
on:click={openPicker}
disabled={disabled || readonly}
>
<span class={displayDate ? '' : 'opacity-40'}>{displayDate || 'DD/MM/YYYY'}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-60" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H5V8h14v13zM7 10h5v5H7z"/>
</svg>
</button>
<input
bind:this={nativeInput}
type="date"
{id}
{name}
value={normalizedValue}
min={normalizedMin}
max={normalizedMax}
{required}
{readonly}
on:change={handleChange}
{disabled}
tabindex="-1"
aria-hidden="true"
style="position:absolute;opacity:0;width:1px;height:1px;top:0;left:0;pointer-events:none;z-index:-1"
/>
</div>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let value: string | null = ''; // ISO yyyy-mm-ddTHH:MM
export let id: string = '';
export let name: string = '';
export let inputClass: string = 'input input-bordered w-full';
export let disabled: boolean = false;
export let min: string | null | undefined = undefined;
export let max: string | null | undefined = undefined;
export let required: boolean = false;
export let readonly: boolean = false;
const dispatch = createEventDispatcher<{ change: Event }>();
let nativeInput: HTMLInputElement;
$: normalizedValue = value ?? '';
$: normalizedMin = min ?? undefined;
$: normalizedMax = max ?? undefined;
$: displayDateTime = formatDateTime(normalizedValue);
function formatDateTime(iso: string): string {
if (!iso) return '';
const [datePart, timePart] = iso.split('T');
if (!datePart) return '';
const [y, m, d] = datePart.split('-');
if (!y || !m || !d) return '';
const timeStr = timePart ? timePart.slice(0, 5) : '';
return timeStr ? `${d}/${m}/${y} ${timeStr}` : `${d}/${m}/${y}`;
}
function openPicker() {
if (!disabled && !readonly) nativeInput?.showPicker?.();
}
function handleChange(e: Event) {
value = (e.currentTarget as HTMLInputElement).value;
dispatch('change', e);
}
</script>
<div class="relative w-full">
<button
type="button"
class="{inputClass} text-left flex items-center justify-between"
on:click={openPicker}
disabled={disabled || readonly}
>
<span class={displayDateTime ? '' : 'opacity-40'}>{displayDateTime || 'DD/MM/YYYY HH:MM'}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-60" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H5V8h14v13zM7 10h5v5H7z"/>
</svg>
</button>
<input
bind:this={nativeInput}
type="datetime-local"
{id}
{name}
value={normalizedValue}
min={normalizedMin}
max={normalizedMax}
{required}
{readonly}
on:change={handleChange}
{disabled}
tabindex="-1"
aria-hidden="true"
style="position:absolute;opacity:0;width:1px;height:1px;top:0;left:0;pointer-events:none;z-index:-1"
/>
</div>

View File

@@ -3,6 +3,7 @@
import { t } from 'svelte-i18n';
import CheckIcon from '~icons/mdi/check';
import CloseIcon from '~icons/mdi/close';
import DateInput from '$lib/components/DateInput.svelte';
import type { ImmichAlbum } from '$lib/types';
import { debounce } from '$lib';
@@ -26,16 +27,18 @@
const dispatch = createEventDispatcher();
// Reactive statements
$: {
if (searchCategory === 'album' && currentAlbum) {
immichImages = [];
fetchAlbumAssets(currentAlbum);
} else if (searchCategory === 'date' && selectedDate) {
clearAlbumSelection();
searchImmich();
} else if (searchCategory === 'search') {
clearAlbumSelection();
}
$: if (searchCategory === 'album' && currentAlbum) {
immichImages = [];
fetchAlbumAssets(currentAlbum);
}
$: if (searchCategory === 'date' && selectedDate) {
clearAlbumSelection();
searchImmich();
}
$: if (searchCategory === 'search') {
clearAlbumSelection();
}
// Helper functions
@@ -149,7 +152,11 @@
}
// Event handlers
const searchImmich = debounce(() => {
function searchImmich() {
debouncedSearchImmich();
}
const debouncedSearchImmich = debounce(() => {
_searchImmich();
}, 500);
@@ -245,11 +252,10 @@
</div>
{:else if searchCategory === 'date'}
<div class="flex gap-2 items-center">
<input
<DateInput
id="date-picker"
type="date"
bind:value={selectedDate}
class="input input-bordered flex-1"
inputClass="input input-bordered flex-1"
disabled={loading}
/>
</div>

View File

@@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
import DateInput from '$lib/components/DateInput.svelte';
let modal: HTMLDialogElement;
import { marked } from 'marked'; // Import the markdown parser
@@ -306,15 +307,14 @@
>
</div>
{/if}
<input
type="date"
<DateInput
id="date"
name="date"
readonly={isReadOnly}
min={constrainDates ? collection.start_date : ''}
max={constrainDates ? collection.end_date : ''}
bind:value={newNote.date}
class="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
/>
</div>
</div>

View File

@@ -10,6 +10,7 @@
import PinIcon from '~icons/mdi/map-marker';
import Clear from '~icons/mdi/close';
import NewLocationModal from '$lib/components/locations/LocationModal.svelte';
import DateInput from '$lib/components/DateInput.svelte';
import { t } from 'svelte-i18n';
import { get as getStore } from 'svelte/store';
import type { Collection, Location, User } from '$lib/types';
@@ -855,26 +856,28 @@
<!-- Date Range Filter -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label class="form-control">
<div class="form-control">
<span class="label label-text text-xs">{$t('adventures.start_date')}</span>
<input
type="date"
<DateInput
id="start-date-filter"
name="start-date-filter"
bind:value={startDateFilter}
class="input input-sm input-bordered w-full"
inputClass="input input-sm input-bordered w-full"
min={collectionStartDateISO}
max={collectionEndDateISO}
/>
</label>
<label class="form-control">
</div>
<div class="form-control">
<span class="label label-text text-xs">{$t('adventures.end_date')}</span>
<input
type="date"
<DateInput
id="end-date-filter"
name="end-date-filter"
bind:value={endDateFilter}
class="input input-sm input-bordered w-full"
inputClass="input input-sm input-bordered w-full"
min={collectionStartDateISO}
max={collectionEndDateISO}
/>
</label>
</div>
</div>
<!-- Routes & Activities Filter -->

View File

@@ -8,6 +8,8 @@
TransportationVisit
} from '$lib/types';
import TimezoneSelector from '../TimezoneSelector.svelte';
import DateInput from '$lib/components/DateInput.svelte';
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
import { t } from 'svelte-i18n';
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
import { onMount } from 'svelte';
@@ -818,20 +820,18 @@
{typeConfig.startLabel}
</label>
{#if allDay}
<input
<DateInput
id="start-date-input"
type="date"
class="input input-bordered w-full mt-1"
inputClass="input input-bordered w-full mt-1"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : ''}
max={constrainDates ? constraintEndDate : ''}
/>
{:else}
<input
<DateTimeInput
id="start-date-input"
type="datetime-local"
class="input input-bordered w-full mt-1"
inputClass="input input-bordered w-full mt-1"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : ''}
@@ -847,20 +847,18 @@
{typeConfig.endLabel}
</label>
{#if allDay}
<input
<DateInput
id="end-date-input"
type="date"
class="input input-bordered w-full mt-1"
inputClass="input input-bordered w-full mt-1"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? localStartDate : ''}
max={constrainDates ? constraintEndDate : ''}
/>
{:else}
<input
<DateTimeInput
id="end-date-input"
type="datetime-local"
class="input input-bordered w-full mt-1"
inputClass="input input-bordered w-full mt-1"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? localStartDate : ''}
@@ -1193,13 +1191,12 @@
class="label-text text-xs font-medium"
for="start-date-{visit.id}">{$t('adventures.start_date')}</label
>
<input
id="start-date-{visit.id}"
type="datetime-local"
class="input input-bordered input-sm w-full mt-1"
bind:value={activityForm.start_date}
readonly={!!pendingStravaImport[visit.id]}
/>
<DateTimeInput
id="start-date-{visit.id}"
inputClass="input input-bordered input-sm w-full mt-1"
bind:value={activityForm.start_date}
readonly={!!pendingStravaImport[visit.id]}
/>
</div>
<!-- Elevation Gain -->

View File

@@ -16,6 +16,8 @@
import MarkdownEditor from '../MarkdownEditor.svelte';
import TimezoneSelector from '../TimezoneSelector.svelte';
import MoneyInput from '../shared/MoneyInput.svelte';
import DateInput from '$lib/components/DateInput.svelte';
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
import { DEFAULT_CURRENCY, normalizeMoneyPayload, toMoneyValue } from '$lib/money';
// @ts-ignore
import { DateTime } from 'luxon';
@@ -733,20 +735,18 @@
<span class="label-text font-medium">{$t('adventures.check_in')}</span>
</label>
{#if allDay}
<input
<DateInput
id="check-in"
type="date"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
max={constrainDates ? constraintEndDate : undefined}
/>
{:else}
<input
<DateTimeInput
id="check-in"
type="datetime-local"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
@@ -761,20 +761,18 @@
<span class="label-text font-medium">{$t('adventures.check_out')}</span>
</label>
{#if allDay}
<input
<DateInput
id="check-out"
type="date"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
max={constrainDates ? constraintEndDate : undefined}
/>
{:else}
<input
<DateTimeInput
id="check-out"
type="datetime-local"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}

View File

@@ -17,6 +17,8 @@
import MarkdownEditor from '../MarkdownEditor.svelte';
import TimezoneSelector from '../TimezoneSelector.svelte';
import MoneyInput from '../shared/MoneyInput.svelte';
import DateInput from '$lib/components/DateInput.svelte';
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
import { DEFAULT_CURRENCY, normalizeMoneyPayload, toMoneyValue } from '$lib/money';
// @ts-ignore
import { DateTime } from 'luxon';
@@ -868,20 +870,18 @@
<span class="label-text font-medium">{$t('transportation.departure_date')}</span>
</label>
{#if allDay}
<input
<DateInput
id="departure-date"
type="date"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
max={constrainDates ? constraintEndDate : undefined}
/>
{:else}
<input
<DateTimeInput
id="departure-date"
type="datetime-local"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
@@ -896,20 +896,18 @@
<span class="label-text font-medium">{$t('transportation.arrival_date')}</span>
</label>
{#if allDay}
<input
<DateInput
id="arrival-date"
type="date"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}
max={constrainDates ? constraintEndDate : undefined}
/>
{:else}
<input
<DateTimeInput
id="arrival-date"
type="datetime-local"
class="input input-bordered bg-base-100/80 focus:bg-base-100"
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? constraintStartDate : undefined}