date input: full keyboard nav, use internationalized/CalendarDate
This commit is contained in:
11
package.json
11
package.json
@@ -36,10 +36,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@internationalized/date": "^3.8.2",
|
||||||
"@jsrob/svelte-portal": "^0.2.1",
|
"@jsrob/svelte-portal": "^0.2.1",
|
||||||
"@svelte-toolkit/validate": "^0.0.0",
|
"@svelte-toolkit/validate": "^0.0.0",
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/node": "^24.0.9",
|
"@types/node": "^24.0.9",
|
||||||
"country-state-city": "^3.2.1",
|
"country-state-city": "^3.2.1",
|
||||||
@@ -57,13 +60,11 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.2.5"
|
||||||
"tailwindcss": "^4.1.11",
|
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
|
||||||
"tailwindcss-animate": "^1.0.7"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -21,6 +21,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.30.0
|
version: 9.30.0
|
||||||
|
'@internationalized/date':
|
||||||
|
specifier: ^3.8.2
|
||||||
|
version: 3.8.2
|
||||||
'@jsrob/svelte-portal':
|
'@jsrob/svelte-portal':
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1(svelte@5.34.9)
|
version: 0.2.1(svelte@5.34.9)
|
||||||
@@ -346,6 +349,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@internationalized/date@3.8.2':
|
||||||
|
resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
|
||||||
|
|
||||||
'@isaacs/fs-minipass@4.0.1':
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -524,6 +530,9 @@ packages:
|
|||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
vite: ^6.0.0
|
vite: ^6.0.0
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
'@tailwindcss/container-queries@0.1.1':
|
'@tailwindcss/container-queries@0.1.1':
|
||||||
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1491,6 +1500,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4'
|
typescript: '>=4.8.4'
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1748,6 +1760,10 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@internationalized/date@3.8.2':
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
'@isaacs/fs-minipass@4.0.1':
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
@@ -1900,6 +1916,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.11)':
|
'@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.11)':
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 4.1.11
|
tailwindcss: 4.1.11
|
||||||
@@ -2802,6 +2822,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
||||||
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { CalendarDate } from '@internationalized/date';
|
||||||
import { InputValidatorEvent, liveValidator, validate } from '@svelte-toolkit/validate';
|
import { InputValidatorEvent, liveValidator, validate } from '@svelte-toolkit/validate';
|
||||||
import { CalendarBlank } from 'phosphor-svelte';
|
import { CalendarBlank } from 'phosphor-svelte';
|
||||||
import { type Snippet } from 'svelte';
|
import { type Snippet } from 'svelte';
|
||||||
import type { ClassValue, KeyboardEventHandler } from 'svelte/elements';
|
import type { ClassValue, FormEventHandler, KeyboardEventHandler } from 'svelte/elements';
|
||||||
import { generateIdentifier } from './util';
|
import { generateIdentifier } from './util';
|
||||||
import Label from './Label.svelte';
|
import Label from './Label.svelte';
|
||||||
|
|
||||||
@@ -15,9 +16,9 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: Date;
|
value?: CalendarDate;
|
||||||
min?: Date;
|
min?: CalendarDate;
|
||||||
max?: Date;
|
max?: CalendarDate;
|
||||||
label?: string;
|
label?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
invalidMessage?: string;
|
invalidMessage?: string;
|
||||||
@@ -27,11 +28,11 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
name,
|
name,
|
||||||
value = $bindable<Date | undefined>(),
|
value = $bindable<CalendarDate | undefined>(),
|
||||||
/** min specifies lower bounds for the date input (WARNING: NOT IMPLEMENTED) */
|
/** min specifies lower bounds for the date input (WARNING: NOT IMPLEMENTED) */
|
||||||
min = new Date(1900, 0, 1),
|
min = new CalendarDate(1900, 0, 1),
|
||||||
/** max specifies upper bounds for the date input (WARNING: NOT IMPLEMENTED) */
|
/** max specifies upper bounds for the date input (WARNING: NOT IMPLEMENTED) */
|
||||||
max = new Date(2100, 11, 31),
|
max = new CalendarDate(2100, 11, 31),
|
||||||
label,
|
label,
|
||||||
required = false,
|
required = false,
|
||||||
invalidMessage = 'Valid date is required',
|
invalidMessage = 'Valid date is required',
|
||||||
@@ -41,41 +42,34 @@
|
|||||||
|
|
||||||
const id = $derived(generateIdentifier('dateinput', name));
|
const id = $derived(generateIdentifier('dateinput', name));
|
||||||
|
|
||||||
const inputSnippets = $derived.by(() => {
|
const controls: Record<
|
||||||
const found: Partial<Record<FormatString, boolean>> = {};
|
FormatString,
|
||||||
const arr: Snippet[] = [];
|
{ snippet: Snippet<[i: number]>; ref: HTMLDivElement | null; prevCaretPos?: caretPos }
|
||||||
|
> = $state({
|
||||||
|
year: { snippet: year, ref: null, beforeInput: null },
|
||||||
|
month: { snippet: month, ref: null, beforeInput: null },
|
||||||
|
day: { snippet: day, ref: null, beforeInput: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Throw an error if format includes duplicate or unsupported controls
|
||||||
|
const found: Partial<Record<FormatString, boolean>> = {};
|
||||||
format.forEach((f) => {
|
format.forEach((f) => {
|
||||||
if (found[f]) throw new Error(`Duplicate format string: ${f}`);
|
if (found[f]) throw new Error(`Duplicate format string: ${f}`);
|
||||||
found[f] = true;
|
found[f] = true;
|
||||||
|
if (!controls[f]) throw new Error(`Unsupported format string: ${f}`);
|
||||||
switch (f) {
|
|
||||||
case 'year':
|
|
||||||
return arr.push(year);
|
|
||||||
case 'month':
|
|
||||||
return arr.push(month);
|
|
||||||
case 'day':
|
|
||||||
return arr.push(day);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown format string: ${f}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return arr;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let valid = $state(true);
|
let valid = $state(true);
|
||||||
let containerElement: HTMLDivElement;
|
let containerElement: HTMLDivElement;
|
||||||
let yearElement = $state<HTMLDivElement | null>(null);
|
|
||||||
let monthElement = $state<HTMLDivElement | null>(null);
|
|
||||||
let dayElement = $state<HTMLDivElement | null>(null);
|
|
||||||
let previousYearValue = $state<string | undefined>(undefined);
|
let previousYearValue = $state<string | undefined>(undefined);
|
||||||
let yearValue = $derived.by(() => {
|
let yearValue = $derived.by(() => {
|
||||||
if (!value && previousYearValue) {
|
if (!value && previousYearValue) {
|
||||||
return previousYearValue;
|
return previousYearValue;
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
return value.getFullYear().toString();
|
return value.year.toString();
|
||||||
}
|
}
|
||||||
return blankState['year'];
|
return blankState['year'];
|
||||||
});
|
});
|
||||||
@@ -85,7 +79,7 @@
|
|||||||
return previousMonthValue;
|
return previousMonthValue;
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
return String(value.getMonth() + 1).padStart(2, '0');
|
return String(value.month).padStart(2, '0');
|
||||||
}
|
}
|
||||||
return blankState['month'];
|
return blankState['month'];
|
||||||
});
|
});
|
||||||
@@ -95,7 +89,7 @@
|
|||||||
return previousDayValue;
|
return previousDayValue;
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
return String(value.getDate()).padStart(2, '0');
|
return String(value.day).padStart(2, '0');
|
||||||
}
|
}
|
||||||
return blankState['day'];
|
return blankState['day'];
|
||||||
});
|
});
|
||||||
@@ -108,8 +102,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let year: number | undefined = undefined;
|
let year: number | undefined = undefined;
|
||||||
if (yearElement) {
|
if (controls.year.ref) {
|
||||||
year = parseInt(yearElement.textContent || '', 10);
|
year = parseInt(controls.year.ref.textContent || '', 10);
|
||||||
if (isNaN(year) || year < 0 || year > 9999) {
|
if (isNaN(year) || year < 0 || year > 9999) {
|
||||||
if (value) {
|
if (value) {
|
||||||
setPrevious();
|
setPrevious();
|
||||||
@@ -121,8 +115,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let month: number | undefined = undefined;
|
let month: number | undefined = undefined;
|
||||||
if (monthElement) {
|
if (controls.month.ref) {
|
||||||
month = parseInt(monthElement.textContent || '', 10);
|
month = parseInt(controls.month.ref.textContent || '', 10);
|
||||||
if (isNaN(month) || month < 1 || month > 12) {
|
if (isNaN(month) || month < 1 || month > 12) {
|
||||||
if (value) {
|
if (value) {
|
||||||
setPrevious();
|
setPrevious();
|
||||||
@@ -134,8 +128,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let day: number | undefined = undefined;
|
let day: number | undefined = undefined;
|
||||||
if (dayElement) {
|
if (controls.day.ref) {
|
||||||
day = parseInt(dayElement.textContent || '', 10);
|
day = parseInt(controls.day.ref.textContent || '', 10);
|
||||||
if (isNaN(day) || day < 1 || day > 31) {
|
if (isNaN(day) || day < 1 || day > 31) {
|
||||||
if (value) {
|
if (value) {
|
||||||
setPrevious();
|
setPrevious();
|
||||||
@@ -146,21 +140,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDate = new Date(
|
const newDate = new CalendarDate(
|
||||||
year ?? (value ? value.getFullYear() : min.getFullYear()),
|
year ?? (value ? value.year : min.year),
|
||||||
month ? month - 1 : value ? value.getMonth() : min.getMonth(),
|
month ? month : value ? value.month : min.month,
|
||||||
day ?? (value ? value.getDate() : min.getDate())
|
day ?? (value ? value.day : min.day)
|
||||||
);
|
);
|
||||||
value = newDate;
|
value = newDate;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeydown: KeyboardEventHandler<HTMLDivElement> = (e) => {
|
type caretPos = { start: number; end: number } | null;
|
||||||
const target = e.target as HTMLDivElement;
|
/** getCaretPosition returns the start and end of the text selection in a particular div */
|
||||||
|
const getCaretPosition = (div: HTMLDivElement): caretPos => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || selection.rangeCount === 0) return null;
|
||||||
|
|
||||||
if (target.isContentEditable && e.key === 'Enter') {
|
const range = selection.getRangeAt(0);
|
||||||
e.preventDefault();
|
|
||||||
target.blur(); // Remove focus to trigger blur event
|
// Ensure the selection is within the target div
|
||||||
|
if (!div.contains(range.startContainer) || !div.contains(range.endContainer)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a range from the start of the div to the selection start
|
||||||
|
const preRange = document.createRange();
|
||||||
|
preRange.selectNodeContents(div);
|
||||||
|
preRange.setEnd(range.startContainer, range.startOffset);
|
||||||
|
const start = preRange.toString().length;
|
||||||
|
|
||||||
|
// Create a range from the start of the div to the selection end
|
||||||
|
const postRange = document.createRange();
|
||||||
|
postRange.selectNodeContents(div);
|
||||||
|
postRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
const end = postRange.toString().length;
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** focusNext focuses the next input */
|
||||||
|
const focusNext = (i: number) => {
|
||||||
|
if (i < format.length - 1) {
|
||||||
|
const next = controls[format[i + 1]].ref;
|
||||||
|
next?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** focusPrevious focuses the previous input */
|
||||||
|
const focusPrevious = (i: number) => {
|
||||||
|
if (i > 0) {
|
||||||
|
const prev = controls[format[i - 1]].ref;
|
||||||
|
prev?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHandleKeydown = (i: number): KeyboardEventHandler<HTMLDivElement> => {
|
||||||
|
return (e) => {
|
||||||
|
if (!e.target) return;
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
|
||||||
|
const pos = getCaretPosition(target);
|
||||||
|
controls[format[i]].prevCaretPos = pos;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(e.key === 'Backspace' || e.key === 'Delete') &&
|
||||||
|
target.textContent === blankState[format[i]]
|
||||||
|
) {
|
||||||
|
target.textContent = '';
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHandleKeyup = (i: number): KeyboardEventHandler<HTMLDivElement> => {
|
||||||
|
return (e) => {
|
||||||
|
if (!e.target) return;
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
const prevPos = controls[format[i]].prevCaretPos;
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
target.blur(); // Remove focus to trigger blur event
|
||||||
|
// Deselect any selected text at window-level
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
// If the select has reached the end of this input, go on to the next
|
||||||
|
// note: add `prevPos?.start === prevPos?.end` to disable moving with an active selection.
|
||||||
|
if (prevPos?.end === target.textContent?.length) {
|
||||||
|
focusNext(i);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
// If the select has reached the start of this input, go back to the previous
|
||||||
|
// note: add `prevPos?.start === prevPos?.end` to disable moving with an active selection.
|
||||||
|
if (prevPos?.start === 0) {
|
||||||
|
focusPrevious(i);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Backspace') {
|
||||||
|
// If previous caret end position is at the start of the input, go back to the previous
|
||||||
|
if (prevPos?.end === 0 && prevPos?.start === 0) {
|
||||||
|
focusPrevious(i);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Delete') {
|
||||||
|
// If previous caret start position is at the end of the input, go to the next
|
||||||
|
if (
|
||||||
|
prevPos?.start === target.textContent?.length &&
|
||||||
|
prevPos?.end === target.textContent?.length
|
||||||
|
) {
|
||||||
|
focusNext(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHandleInput = (i: number, maxLength: number): FormEventHandler<HTMLDivElement> => {
|
||||||
|
return (e) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
|
||||||
|
// If we've typed to the end of the input, go on to the next input
|
||||||
|
if (target.textContent?.length === maxLength) {
|
||||||
|
focusNext(i);
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = (e: FocusEvent) => {
|
const handleFocus = (e: FocusEvent) => {
|
||||||
@@ -168,11 +267,13 @@
|
|||||||
|
|
||||||
// Select all text in the input when focused
|
// Select all text in the input when focused
|
||||||
if (target.isContentEditable) {
|
if (target.isContentEditable) {
|
||||||
const range = document.createRange();
|
setTimeout(() => {
|
||||||
const selection = window.getSelection();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(target);
|
const selection = window.getSelection();
|
||||||
selection?.removeAllRanges();
|
range.selectNodeContents(target);
|
||||||
selection?.addRange(range);
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(range);
|
||||||
|
}, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,18 +317,12 @@
|
|||||||
!valid && 'border-red-500!'
|
!valid && 'border-red-500!'
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<input
|
<input type="hidden" {name} {id} value={value?.toString() ?? ''} use:validate={{ required }} />
|
||||||
type="hidden"
|
|
||||||
{name}
|
|
||||||
{id}
|
|
||||||
value={value?.toISOString() ?? ''}
|
|
||||||
use:validate={{ required }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#each inputSnippets as snippet, i}
|
{#each format as key, i (key)}
|
||||||
{@render snippet()}
|
{@render controls[key].snippet(i)}
|
||||||
|
|
||||||
{#if i < inputSnippets.length - 1}
|
{#if i < format.length - 1}
|
||||||
<span class="text-sui-text/60 dark:text-sui-background/60">-</span>
|
<span class="text-sui-text/60 dark:text-sui-background/60">-</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -241,9 +336,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#snippet year()}
|
{#snippet year(i: number)}
|
||||||
<div
|
<div
|
||||||
bind:this={yearElement}
|
bind:this={controls.year.ref}
|
||||||
bind:textContent={yearValue}
|
bind:textContent={yearValue}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -252,7 +347,9 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
onfocus={handleFocus}
|
onfocus={handleFocus}
|
||||||
onblur={createHandleBlur(blankState.year)}
|
onblur={createHandleBlur(blankState.year)}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={createHandleKeydown(i)}
|
||||||
|
onkeyup={createHandleKeyup(i)}
|
||||||
|
oninput={createHandleInput(i, 4)}
|
||||||
use:validate={{
|
use:validate={{
|
||||||
baseval: blankState.year,
|
baseval: blankState.year,
|
||||||
pattern: /^\d{1,4}$/
|
pattern: /^\d{1,4}$/
|
||||||
@@ -264,9 +361,9 @@
|
|||||||
></div>
|
></div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet month()}
|
{#snippet month(i: number)}
|
||||||
<div
|
<div
|
||||||
bind:this={monthElement}
|
bind:this={controls.month.ref}
|
||||||
bind:textContent={monthValue}
|
bind:textContent={monthValue}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -275,10 +372,12 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
onfocus={handleFocus}
|
onfocus={handleFocus}
|
||||||
onblur={createHandleBlur(blankState.month)}
|
onblur={createHandleBlur(blankState.month)}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={createHandleKeydown(i)}
|
||||||
|
onkeyup={createHandleKeyup(i)}
|
||||||
|
oninput={createHandleInput(i, 2)}
|
||||||
use:validate={{
|
use:validate={{
|
||||||
baseval: blankState.month,
|
baseval: blankState.month,
|
||||||
pattern: /^(0[1-9]|1[0-2])$/
|
pattern: /^([0-9]|0[1-9]|1[0-2])$/
|
||||||
}}
|
}}
|
||||||
use:liveValidator={{ constrain: true }}
|
use:liveValidator={{ constrain: true }}
|
||||||
onvalidate={(e) => {
|
onvalidate={(e) => {
|
||||||
@@ -287,9 +386,9 @@
|
|||||||
></div>
|
></div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet day()}
|
{#snippet day(i: number)}
|
||||||
<div
|
<div
|
||||||
bind:this={dayElement}
|
bind:this={controls.day.ref}
|
||||||
bind:textContent={dayValue}
|
bind:textContent={dayValue}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -298,10 +397,12 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
onfocus={handleFocus}
|
onfocus={handleFocus}
|
||||||
onblur={createHandleBlur(blankState.day)}
|
onblur={createHandleBlur(blankState.day)}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={createHandleKeydown(i)}
|
||||||
|
onkeyup={createHandleKeyup(i)}
|
||||||
|
oninput={createHandleInput(i, 2)}
|
||||||
use:validate={{
|
use:validate={{
|
||||||
baseval: blankState.day,
|
baseval: blankState.day,
|
||||||
pattern: /^(0[1-9]|[12][0-9]|3[01])$/
|
pattern: /^([0-9]|0[1-9]|[12][0-9]|3[01])$/
|
||||||
}}
|
}}
|
||||||
use:liveValidator={{ constrain: true }}
|
use:liveValidator={{ constrain: true }}
|
||||||
onvalidate={(e) => {
|
onvalidate={(e) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { CalendarDate, today, getLocalTimeZone } from '@internationalized/date';
|
||||||
import Button from '$lib/Button.svelte';
|
import Button from '$lib/Button.svelte';
|
||||||
import ActionSelect from '$lib/ActionSelect.svelte';
|
import ActionSelect from '$lib/ActionSelect.svelte';
|
||||||
import Checkbox, { type CheckboxState } from '$lib/Checkbox.svelte';
|
import Checkbox, { type CheckboxState } from '$lib/Checkbox.svelte';
|
||||||
@@ -27,10 +28,16 @@
|
|||||||
TextStrikethrough,
|
TextStrikethrough,
|
||||||
TextUnderline
|
TextUnderline
|
||||||
} from 'phosphor-svelte';
|
} from 'phosphor-svelte';
|
||||||
|
import type { Option } from '$lib';
|
||||||
|
|
||||||
let dateInputValue = $state<Date | undefined>(undefined);
|
let dateInputValue = $state<CalendarDate | undefined>(undefined);
|
||||||
let checkboxValue = $state<CheckboxState>('indeterminate');
|
let checkboxValue = $state<CheckboxState>('indeterminate');
|
||||||
let dialogOpen = $state(false);
|
let dialogOpen = $state(false);
|
||||||
|
let toggleOptions: Option[] = $state([
|
||||||
|
'item one',
|
||||||
|
'item two',
|
||||||
|
{ value: 'complex', label: 'Complex item' }
|
||||||
|
]);
|
||||||
|
|
||||||
const toolbar = new Toolbar();
|
const toolbar = new Toolbar();
|
||||||
const fontGroup = toolbar.group();
|
const fontGroup = toolbar.group();
|
||||||
@@ -141,7 +148,7 @@
|
|||||||
<Button
|
<Button
|
||||||
icon={{ component: Plus }}
|
icon={{ component: Plus }}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
dateInputValue = new Date();
|
dateInputValue = today(getLocalTimeZone());
|
||||||
console.log('Dateinput value set to:', dateInputValue);
|
console.log('Dateinput value set to:', dateInputValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -208,10 +215,13 @@
|
|||||||
|
|
||||||
<ToggleSelect name="example-toggle-select" class="mb-4">Toggle Me!</ToggleSelect>
|
<ToggleSelect name="example-toggle-select" class="mb-4">Toggle Me!</ToggleSelect>
|
||||||
|
|
||||||
<ToggleGroup
|
<ToggleGroup label="Toggle Group" options={toggleOptions} />
|
||||||
label="Toggle Group"
|
|
||||||
options={['item one', 'item two', { value: 'complex', label: 'Complex item' }]}
|
<Button
|
||||||
/>
|
onclick={() => {
|
||||||
|
toggleOptions.push({ value: 'new', label: 'New Option' });
|
||||||
|
}}>Add Option</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="component">
|
<div class="component">
|
||||||
|
|||||||
Reference in New Issue
Block a user