feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,721 @@
<svelte:options runes={false} />
<script>
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { readable, get } from 'svelte/store';
import { clipboardCopy } from './clipboardCopy.js';
let styleInit = false;
/**
* @typedef {unknown | Promise<unknown>} EncodeableData
* @typedef {import('svelte/store').Readable<EncodeableData>} EncodeableDataStore
*
* @typedef {EncodeableData | EncodeableDataStore} DebugData
*/
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
export let data;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
export let display = true;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
export let status = true;
/**
* Optional label to identify the component easily.
*/
export let label = '';
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
export let stringTruncate = 120;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
export let ref = undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
export let promise = false;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
export let raw = false;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
export let functions = false;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
export let theme = 'default';
///// Collapse behavior ///////////////////////////////////////////
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
export let collapsible = false;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
export let collapsed = false;
if (browser && collapsible) setCollapse();
/**
* @param {boolean|undefined} status
*/
function setCollapse(status = undefined) {
let data;
const route = $page.route.id ?? '';
try {
if (sessionStorage.SuperDebug) {
data = JSON.parse(sessionStorage.SuperDebug);
}
data = { collapsed: data && data.collapsed ? data.collapsed : {} };
data.collapsed[route] = status === undefined ? (data.collapsed[route] ?? collapsed) : status;
} catch {
data = {
collapsed: {
[route]: collapsed
}
};
}
if (status !== undefined) {
sessionStorage.SuperDebug = JSON.stringify(data);
}
collapsed = data.collapsed[route];
}
/**
* @type {ReturnType<typeof setTimeout> | undefined}
*/
let copied;
/**
* @param {Event} e
*/
async function copyContent(e) {
if (!e.target) return;
const parent = /** @type {HTMLElement} */ (e.target).closest('.super-debug');
if (!parent) return;
const codeEl = /** @type {HTMLPreElement} */ (parent.querySelector('.super-debug--code'));
if (!codeEl) return;
clearTimeout(copied);
await clipboardCopy(codeEl.innerText);
copied = setTimeout(() => (copied = undefined), 900);
}
/**
* @param {File} file
*/
function fileToJSON(file) {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
};
}
///////////////////////////////////////////////////////////////////
/**
* @param {unknown} json
* @returns {string}
*/
function syntaxHighlight(json) {
switch (typeof json) {
case 'function': {
return `<span class="function">[function ${json.name ?? 'unnamed'}]</span>`;
}
case 'symbol': {
return `<span class="symbol">${json.toString()}</span>`;
}
}
const encodedString = JSON.stringify(
json,
function (key, value) {
if (value === undefined) {
return '#}#undefined';
}
if (typeof this === 'object' && this[key] instanceof Date) {
// @ts-expect-error Date checking works with isNaN
return '#}D#' + (isNaN(this[key]) ? 'Invalid Date' : value);
}
if (typeof value === 'number') {
if (value == Number.POSITIVE_INFINITY) return '#}#Inf';
if (value == Number.NEGATIVE_INFINITY) return '#}#-Inf';
if (isNaN(value)) return '#}#NaN';
}
if (typeof value === 'bigint') {
return '#}BI#' + value;
}
if (typeof value === 'function' && functions) {
return '#}F#' + `[function ${value.name}]`;
}
if (value instanceof Error) {
return '#}E#' + `${value.name}: ${value.message || value.cause || '(No error message)'}`;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof Map) {
return Array.from(value.entries());
}
if (
typeof this === 'object' &&
typeof this[key] == 'object' &&
this[key] &&
'toExponential' in this[key]
) {
return '#}DE#' + this[key].toString();
}
if (browser && typeof this === 'object' && this[key] instanceof File) {
return fileToJSON(this[key]);
}
if (browser && typeof this === 'object' && this[key] instanceof FileList) {
/** @type FileList */
const list = this[key];
const output = [];
for (let i = 0; i < list.length; i++) {
const file = list.item(i);
if (file) output.push(fileToJSON(file));
}
return output;
}
return value;
},
2
)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return encodedString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.slice(1, -2) + ':';
} else {
cls = 'string';
match =
stringTruncate > 0 && match.length > stringTruncate
? match.slice(0, stringTruncate / 2) +
`[..${match.length - stringTruncate}/${match.length}..]` +
match.slice(-stringTruncate / 2)
: match;
if (match == '"#}#undefined"') {
cls = 'undefined';
match = 'undefined';
} else if (match.startsWith('"#}D#')) {
cls = 'date';
match = match.slice(5, -1);
} else if (match == '"#}#NaN"') {
cls = 'nan';
match = 'NaN';
} else if (match == '"#}#Inf"') {
cls = 'nan';
match = 'Infinity';
} else if (match == '"#}#-Inf"') {
cls = 'nan';
match = '-Infinity';
} else if (match.startsWith('"#}BI#')) {
cls = 'bigint';
match = match.slice(6, -1) + 'n';
} else if (match.startsWith('"#}F#')) {
cls = 'function';
match = match.slice(5, -1);
} else if (match.startsWith('"#}E#')) {
cls = 'error';
match = match.slice(5, -1);
} else if (match.startsWith('"#}DE#')) {
cls = 'number';
match = match.slice(6, -1);
}
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
}
);
}
/**
* @param {EncodeableData} data
* @param {boolean} raw
* @param {boolean} promise
* @returns {data is Promise<unknown>}
*/
function assertPromise(data, raw, promise) {
if (raw) {
return false;
}
return (
promise ||
(typeof data === 'object' &&
data !== null &&
'then' in data &&
typeof data['then'] === 'function')
);
}
/**
* @param {DebugData} data
* @param {boolean} raw
* @returns {data is EncodeableDataStore}
*/
function assertStore(data, raw) {
if (raw) {
return false;
}
return (
typeof data === 'object' &&
data !== null &&
'subscribe' in data &&
typeof data['subscribe'] === 'function'
);
}
$: themeStyle =
theme === 'vscode'
? `
--sd-vscode-bg-color: #1f1f1f;
--sd-vscode-label-color: #cccccc;
--sd-vscode-code-default: #8c8a89;
--sd-vscode-code-key: #9cdcfe;
--sd-vscode-code-string: #ce9171;
--sd-vscode-code-number: #b5c180;
--sd-vscode-code-boolean: #4a9cd6;
--sd-vscode-code-null: #4a9cd6;
--sd-vscode-code-undefined: #4a9cd6;
--sd-vscode-code-nan: #4a9cd6;
--sd-vscode-code-symbol: #4de0c5;
--sd-vscode-sb-thumb-color: #35373a;
--sd-vscode-sb-thumb-color-focus: #4b4d50;
`
: undefined;
/** @type {import('svelte/store').Readable<EncodeableData>} */
$: debugData = assertStore(data, raw) ? data : readable(data);
</script>
{#if !styleInit}
<style>
.super-debug--absolute {
position: absolute;
}
.super-debug--top-0 {
top: 0;
}
.super-debug--inset-x-0 {
left: 0px;
right: 0px;
}
.super-debug--hidden {
height: 0;
overflow: hidden;
}
.super-debug--hidden:not(.super-debug--with-label) {
height: 1.5em;
}
.super-debug--rotated {
transform: rotate(180deg);
}
.super-debug {
--_sd-bg-color: var(--sd-bg-color, var(--sd-vscode-bg-color, rgb(30, 41, 59)));
position: relative;
background-color: var(--_sd-bg-color);
border-radius: 0.5rem;
overflow: hidden;
}
.super-debug--pre {
overflow-x: auto;
}
.super-debug--collapse {
display: block;
width: 100%;
color: rgba(255, 255, 255, 0.25);
background-color: rgba(255, 255, 255, 0.15);
padding: 5px 0;
display: flex;
justify-content: center;
border-color: transparent;
margin: 0;
padding: 3px 0;
}
.super-debug--collapse:focus {
color: #fafafa;
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--collapse:is(:hover) {
color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--status {
display: flex;
padding: 1em;
padding-bottom: 0;
justify-content: space-between;
font-family:
Inconsolata, Monaco, Consolas, 'Lucida Console', 'Courier New', Courier, monospace;
}
.super-debug--right-status {
display: flex;
gap: 0.55em;
}
.super-debug--copy {
margin: 0;
padding: 0;
padding-top: 2px;
background-color: transparent;
border: 0;
color: #666;
cursor: pointer;
}
.super-debug--copy:hover {
background-color: transparent;
color: #666;
}
.super-debug--copy:focus {
background-color: transparent;
color: #666;
}
.super-debug--label {
color: var(--sd-label-color, var(--sd-vscode-label-color, white));
}
.super-debug--promise-loading {
color: var(--sd-promise-loading-color, var(--sd-vscode-promise-loading-color, #999));
}
.super-debug--promise-rejected {
color: var(--sd-promise-rejected-color, var(--sd-vscode-promise-rejected-color, #ff475d));
}
.super-debug pre {
color: var(--sd-code-default, var(--sd-vscode-code-default, #999));
background-color: var(--_sd-bg-color);
font-size: 1em;
margin-bottom: 0;
padding: 1em 0 1em 1em;
}
.super-debug--info {
color: var(--sd-info, var(--sd-vscode-info, rgb(85, 85, 255)));
}
.super-debug--success {
color: var(--sd-success, var(--sd-vscode-success, #2cd212));
}
.super-debug--redirect {
color: var(--sd-redirect, var(--sd-vscode-redirect, #03cae5));
}
.super-debug--error {
color: var(--sd-error, var(--sd-vscode-error, #ff475d));
}
.super-debug--code .key {
color: var(--sd-code-key, var(--sd-vscode-code-key, #eab308));
}
.super-debug--code .string {
color: var(--sd-code-string, var(--sd-vscode-code-string, #6ec687));
}
.super-debug--code .date {
color: var(--sd-code-date, var(--sd-vscode-code-date, #f06962));
}
.super-debug--code .boolean {
color: var(--sd-code-boolean, var(--sd-vscode-code-boolean, #79b8ff));
}
.super-debug--code .number {
color: var(--sd-code-number, var(--sd-vscode-code-number, #af77e9));
}
.super-debug--code .bigint {
color: var(--sd-code-bigint, var(--sd-vscode-code-bigint, #af77e9));
}
.super-debug--code .null {
color: var(--sd-code-null, var(--sd-vscode-code-null, #238afe));
}
.super-debug--code .nan {
color: var(--sd-code-nan, var(--sd-vscode-code-nan, #af77e9));
}
.super-debug--code .undefined {
color: var(--sd-code-undefined, var(--sd-vscode-code-undefined, #238afe));
}
.super-debug--code .function {
color: var(--sd-code-function, var(--sd-vscode-code-function, #f06962));
}
.super-debug--code .symbol {
color: var(--sd-code-symbol, var(--sd-vscode-code-symbol, #4de0c5));
}
.super-debug--code .error {
color: var(--sd-code-error, var(--sd-vscode-code-error, #ff475d));
}
.super-debug pre::-webkit-scrollbar {
width: var(--sd-sb-width, var(--sd-vscode-sb-width, 1rem));
height: var(--sd-sb-height, var(--sd-vscode-sb-height, 1rem));
}
.super-debug pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color,
var(--sd-vscode-sb-track-color, hsl(0, 0%, 40%, 0.2))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color-focus,
var(--sd-vscode-sb-track-color-focus, hsl(0, 0%, 50%, 0.2))
);
}
.super-debug pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color,
var(--sd-vscode-sb-thumb-color, hsl(217, 50%, 50%, 0.5))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color-focus,
var(--sd-vscode-sb-thumb-color-focus, hsl(217, 50%, 50%))
);
}
</style>
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{@const init = styleInit = true}
{/if}
<!-- eslint-disable svelte/no-at-html-tags -->
{#if display}
<div
class="super-debug"
class:super-debug--collapsible={collapsible}
style={themeStyle}
dir="ltr"
>
<div
class="super-debug--status {label === ''
? 'super-debug--absolute super-debug--inset-x-0 super-debug--top-0'
: ''}"
>
<div class="super-debug--label">{label}</div>
<div class="super-debug--right-status">
<button type="button" class="super-debug--copy" on:click={copyContent}>
{#if !copied}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"
/><path
d="M4.012 16.737A2.005 2.005 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"
/></g
></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path d="M15 12v6m-3-3h6" /><rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></g
></svg
>
{/if}
</button>
{#if status}
<div
class:super-debug--info={$page.status < 200}
class:super-debug--success={$page.status >= 200 && $page.status < 300}
class:super-debug--redirect={$page.status >= 300 && $page.status < 400}
class:super-debug--error={$page.status >= 400}
>
{$page.status}
</div>
{/if}
</div>
</div>
<pre
class="super-debug--pre"
class:super-debug--with-label={label}
class:super-debug--hidden={collapsed}
bind:this={ref}><code class="super-debug--code"
><slot
>{#if assertPromise($debugData, raw, promise)}{#await $debugData}<div
class="super-debug--promise-loading">Loading data...</div>{:then result}{@html syntaxHighlight(
assertStore(result, raw) ? get(result) : result
)}{:catch error}<span class="super-debug--promise-rejected">Rejected:</span
> {@html syntaxHighlight(error)}{/await}{:else}{@html syntaxHighlight(
$debugData
)}{/if}</slot
></code
></pre>
{#if collapsible}
<button
type="button"
on:click|preventDefault={() => setCollapse(!collapsed)}
class="super-debug--collapse"
aria-label="Collapse"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
class:super-debug--rotated={collapsed}
><path
fill="currentColor"
d="M4.08 11.92L12 4l7.92 7.92l-1.42 1.41l-5.5-5.5V22h-2V7.83l-5.5 5.5l-1.42-1.41M12 4h10V2H2v2h10Z"
/></svg
>
</button>
{/if}
</div>
{/if}
<!--
@component
SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
Other use cases includes debugging plain objects, promises, stores and more.
More info: https://superforms.rocks/super-debug
**Short example:**
```svelte
<script>
import SuperDebug from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms';
export let data;
const { errors, form, enhance } = superForm(data.form);
</script>
<SuperDebug data={$form} label="My form data" />
```
-->

View File

@@ -0,0 +1,71 @@
/** @typedef {typeof __propDef.props} SuperDebugProps */
/** @typedef {typeof __propDef.events} SuperDebugEvents */
/** @typedef {typeof __propDef.slots} SuperDebugSlots */
/**
* SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
*
* Other use cases includes debugging plain objects, promises, stores and more.
*
* More info: https://superforms.rocks/super-debug
*
* **Short example:**
*
* ```svelte
* <script>
* import SuperDebug from 'sveltekit-superforms';
* import { superForm } from 'sveltekit-superforms';
*
* export let data;
*
* const { errors, form, enhance } = superForm(data.form);
* </script>
*
* <SuperDebug data={$form} label="My form data" />
* ```
*/
export default class SuperDebug extends SvelteComponentTyped<{
data: unknown;
status?: boolean | undefined;
label?: string | undefined;
display?: boolean | undefined;
stringTruncate?: number | undefined;
ref?: HTMLPreElement | undefined;
promise?: boolean | undefined;
raw?: boolean | undefined;
functions?: boolean | undefined;
theme?: "default" | "vscode" | undefined;
collapsible?: boolean | undefined;
collapsed?: boolean | undefined;
}, {
[evt: string]: CustomEvent<any>;
}, {
default: {};
}> {
}
export type SuperDebugProps = typeof __propDef.props;
export type SuperDebugEvents = typeof __propDef.events;
export type SuperDebugSlots = typeof __propDef.slots;
import { SvelteComponentTyped } from "svelte";
declare const __propDef: {
props: {
data: unknown;
status?: boolean | undefined;
label?: string | undefined;
display?: boolean | undefined;
stringTruncate?: number | undefined;
ref?: HTMLPreElement | undefined;
promise?: boolean | undefined;
raw?: boolean | undefined;
functions?: boolean | undefined;
theme?: "default" | "vscode" | undefined;
collapsible?: boolean | undefined;
collapsed?: boolean | undefined;
};
events: {
[evt: string]: CustomEvent<any>;
};
slots: {
default: {};
};
};
export {};

View File

@@ -0,0 +1,745 @@
<svelte:options runes={true} />
<script lang="ts">
import type { Readable } from 'svelte/store';
import { browser } from '$app/environment';
import { page } from '$app/state';
import { fromStore } from 'svelte/store';
import { clipboardCopy } from './clipboardCopy.js';
type EncodeableData = unknown | Promise<unknown>;
type EncodeableDataStore = Readable<EncodeableData>;
type DebugData = EncodeableData | EncodeableDataStore;
let {
data,
display = true,
status = true,
label = '',
stringTruncate = 120,
ref = $bindable(undefined),
promise = false,
raw = false,
functions = false,
theme = 'default',
collapsible = false,
collapsed = $bindable(false),
children
}: {
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
data: DebugData;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
display?: boolean;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
status?: boolean;
/**
* Optional label to identify the component easily.
*/
label?: string;
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
stringTruncate?: number;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
ref?: HTMLPreElement | undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
promise?: boolean;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
raw?: boolean;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
functions?: boolean;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
theme?: 'default' | 'vscode';
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
collapsible?: boolean;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
collapsed?: boolean;
children?: import('svelte').Snippet;
} = $props();
let copied = $state<ReturnType<typeof setTimeout> | undefined>();
let styleInit = $state(false);
///// Collapse behavior ///////////////////////////////////////////
// svelte-ignore state_referenced_locally
if (browser && collapsible) setCollapse();
/**
* @param {boolean|undefined} status
*/
function setCollapse(status: boolean | undefined = undefined) {
let data;
const route = page.route.id ?? '';
try {
if (sessionStorage.SuperDebug) {
data = JSON.parse(sessionStorage.SuperDebug);
}
data = { collapsed: data && data.collapsed ? data.collapsed : {} };
data.collapsed[route] = status === undefined ? (data.collapsed[route] ?? collapsed) : status;
} catch {
data = {
collapsed: {
[route]: collapsed
}
};
}
if (status !== undefined) {
sessionStorage.SuperDebug = JSON.stringify(data);
}
collapsed = data.collapsed[route];
}
/**
* @param {MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}} e
*/
async function copyContent(
e: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}
) {
if (!e.target) return;
// @ts-expect-error why not use e.currentTarget here?
const parent = e.target.closest('.super-debug');
if (!parent) return;
const codeEl = parent.querySelector<HTMLPreElement>('.super-debug--code');
if (!codeEl) return;
clearTimeout(copied);
await clipboardCopy(codeEl.innerText);
copied = setTimeout(() => (copied = undefined), 900);
}
/**
* @param {File} file
*/
function fileToJSON(file: File) {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
};
}
///////////////////////////////////////////////////////////////////
/**
* @param {unknown} json
* @returns {string}
*/
function syntaxHighlight(json: unknown): string {
switch (typeof json) {
case 'function': {
return `<span class="function">[function ${json.name ?? 'unnamed'}]</span>`;
}
case 'symbol': {
return `<span class="symbol">${json.toString()}</span>`;
}
}
const encodedString = JSON.stringify(
json,
function (key, value) {
if (value === undefined) {
return '#}#undefined';
}
if (typeof this === 'object' && this[key] instanceof Date) {
// @ts-expect-error Date checking works with isNaN
return '#}D#' + (isNaN(this[key]) ? 'Invalid Date' : value);
}
if (typeof value === 'number') {
if (value == Number.POSITIVE_INFINITY) return '#}#Inf';
if (value == Number.NEGATIVE_INFINITY) return '#}#-Inf';
if (isNaN(value)) return '#}#NaN';
}
if (typeof value === 'bigint') {
return '#}BI#' + value;
}
if (typeof value === 'function' && functions) {
return '#}F#' + `[function ${value.name}]`;
}
if (value instanceof Error) {
return '#}E#' + `${value.name}: ${value.message || value.cause || '(No error message)'}`;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof Map) {
return Array.from(value.entries());
}
if (
typeof this === 'object' &&
typeof this[key] == 'object' &&
this[key] &&
'toExponential' in this[key]
) {
return '#}DE#' + this[key].toString();
}
if (browser && typeof this === 'object' && this[key] instanceof File) {
return fileToJSON(this[key]);
}
if (browser && typeof this === 'object' && this[key] instanceof FileList) {
/** @type FileList */
const list: FileList = this[key];
const output = [];
for (let i = 0; i < list.length; i++) {
const file = list.item(i);
if (file) output.push(fileToJSON(file));
}
return output;
}
return value;
},
2
)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return encodedString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.slice(1, -2) + ':';
} else {
cls = 'string';
match =
stringTruncate > 0 && match.length > stringTruncate
? match.slice(0, stringTruncate / 2) +
`[..${match.length - stringTruncate}/${match.length}..]` +
match.slice(-stringTruncate / 2)
: match;
if (match == '"#}#undefined"') {
cls = 'undefined';
match = 'undefined';
} else if (match.startsWith('"#}D#')) {
cls = 'date';
match = match.slice(5, -1);
} else if (match == '"#}#NaN"') {
cls = 'nan';
match = 'NaN';
} else if (match == '"#}#Inf"') {
cls = 'nan';
match = 'Infinity';
} else if (match == '"#}#-Inf"') {
cls = 'nan';
match = '-Infinity';
} else if (match.startsWith('"#}BI#')) {
cls = 'bigint';
match = match.slice(6, -1) + 'n';
} else if (match.startsWith('"#}F#')) {
cls = 'function';
match = match.slice(5, -1);
} else if (match.startsWith('"#}E#')) {
cls = 'error';
match = match.slice(5, -1);
} else if (match.startsWith('"#}DE#')) {
cls = 'number';
match = match.slice(6, -1);
}
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
}
);
}
/**
* @param {EncodeableData} data
* @param {boolean} raw
* @param {boolean} promise
* @returns {data is Promise<unknown>}
*/
function assertPromise(
data: EncodeableData,
raw: boolean,
promise: boolean
): data is Promise<unknown> {
if (raw) {
return false;
}
return (
promise ||
(typeof data === 'object' &&
data !== null &&
'then' in data &&
typeof data['then'] === 'function')
);
}
/**
* @param {DebugData} data
* @param {boolean} raw
* @returns {data is EncodeableDataStore}
*/
function assertStore(data: DebugData, raw: boolean): data is EncodeableDataStore {
if (raw) {
return false;
}
return (
typeof data === 'object' &&
data !== null &&
'subscribe' in data &&
typeof data['subscribe'] === 'function'
);
}
const themeStyle = $derived(
theme === 'vscode'
? `
--sd-vscode-bg-color: #1f1f1f;
--sd-vscode-label-color: #cccccc;
--sd-vscode-code-default: #8c8a89;
--sd-vscode-code-key: #9cdcfe;
--sd-vscode-code-string: #ce9171;
--sd-vscode-code-number: #b5c180;
--sd-vscode-code-boolean: #4a9cd6;
--sd-vscode-code-null: #4a9cd6;
--sd-vscode-code-undefined: #4a9cd6;
--sd-vscode-code-nan: #4a9cd6;
--sd-vscode-code-symbol: #4de0c5;
--sd-vscode-sb-thumb-color: #35373a;
--sd-vscode-sb-thumb-color-focus: #4b4d50;
`
: undefined
);
const debugData = $derived(assertStore(data, raw) ? fromStore(data) : data);
</script>
{#if !styleInit}
<style>
.super-debug--absolute {
position: absolute;
}
.super-debug--top-0 {
top: 0;
}
.super-debug--inset-x-0 {
left: 0px;
right: 0px;
}
.super-debug--hidden {
height: 0;
overflow: hidden;
}
.super-debug--hidden:not(.super-debug--with-label) {
height: 1.5em;
}
.super-debug--rotated {
transform: rotate(180deg);
}
.super-debug {
--_sd-bg-color: var(--sd-bg-color, var(--sd-vscode-bg-color, rgb(30, 41, 59)));
position: relative;
background-color: var(--_sd-bg-color);
border-radius: 0.5rem;
overflow: hidden;
}
.super-debug--pre {
overflow-x: auto;
}
.super-debug--collapse {
display: block;
width: 100%;
color: rgba(255, 255, 255, 0.25);
background-color: rgba(255, 255, 255, 0.15);
padding: 5px 0;
display: flex;
justify-content: center;
border-color: transparent;
margin: 0;
padding: 3px 0;
}
.super-debug--collapse:focus {
color: #fafafa;
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--collapse:is(:hover) {
color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--status {
display: flex;
padding: 1em;
padding-bottom: 0;
justify-content: space-between;
font-family:
Inconsolata, Monaco, Consolas, 'Lucida Console', 'Courier New', Courier, monospace;
}
.super-debug--right-status {
display: flex;
gap: 0.55em;
}
.super-debug--copy {
margin: 0;
padding: 0;
padding-top: 2px;
background-color: transparent;
border: 0;
color: #666;
cursor: pointer;
}
.super-debug--copy:hover {
background-color: transparent;
color: #666;
}
.super-debug--copy:focus {
background-color: transparent;
color: #666;
}
.super-debug--label {
color: var(--sd-label-color, var(--sd-vscode-label-color, white));
}
.super-debug--promise-loading {
color: var(--sd-promise-loading-color, var(--sd-vscode-promise-loading-color, #999));
}
.super-debug--promise-rejected {
color: var(--sd-promise-rejected-color, var(--sd-vscode-promise-rejected-color, #ff475d));
}
.super-debug pre {
color: var(--sd-code-default, var(--sd-vscode-code-default, #999));
background-color: var(--_sd-bg-color);
font-size: 1em;
margin-bottom: 0;
padding: 1em 0 1em 1em;
}
.super-debug--info {
color: var(--sd-info, var(--sd-vscode-info, rgb(85, 85, 255)));
}
.super-debug--success {
color: var(--sd-success, var(--sd-vscode-success, #2cd212));
}
.super-debug--redirect {
color: var(--sd-redirect, var(--sd-vscode-redirect, #03cae5));
}
.super-debug--error {
color: var(--sd-error, var(--sd-vscode-error, #ff475d));
}
.super-debug--code .key {
color: var(--sd-code-key, var(--sd-vscode-code-key, #eab308));
}
.super-debug--code .string {
color: var(--sd-code-string, var(--sd-vscode-code-string, #6ec687));
}
.super-debug--code .date {
color: var(--sd-code-date, var(--sd-vscode-code-date, #f06962));
}
.super-debug--code .boolean {
color: var(--sd-code-boolean, var(--sd-vscode-code-boolean, #79b8ff));
}
.super-debug--code .number {
color: var(--sd-code-number, var(--sd-vscode-code-number, #af77e9));
}
.super-debug--code .bigint {
color: var(--sd-code-bigint, var(--sd-vscode-code-bigint, #af77e9));
}
.super-debug--code .null {
color: var(--sd-code-null, var(--sd-vscode-code-null, #238afe));
}
.super-debug--code .nan {
color: var(--sd-code-nan, var(--sd-vscode-code-nan, #af77e9));
}
.super-debug--code .undefined {
color: var(--sd-code-undefined, var(--sd-vscode-code-undefined, #238afe));
}
.super-debug--code .function {
color: var(--sd-code-function, var(--sd-vscode-code-function, #f06962));
}
.super-debug--code .symbol {
color: var(--sd-code-symbol, var(--sd-vscode-code-symbol, #4de0c5));
}
.super-debug--code .error {
color: var(--sd-code-error, var(--sd-vscode-code-error, #ff475d));
}
.super-debug pre::-webkit-scrollbar {
width: var(--sd-sb-width, var(--sd-vscode-sb-width, 1rem));
height: var(--sd-sb-height, var(--sd-vscode-sb-height, 1rem));
}
.super-debug pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color,
var(--sd-vscode-sb-track-color, hsl(0, 0%, 40%, 0.2))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color-focus,
var(--sd-vscode-sb-track-color-focus, hsl(0, 0%, 50%, 0.2))
);
}
.super-debug pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color,
var(--sd-vscode-sb-thumb-color, hsl(217, 50%, 50%, 0.5))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color-focus,
var(--sd-vscode-sb-thumb-color-focus, hsl(217, 50%, 50%))
);
}
</style>
{/if}
<!-- eslint-disable svelte/no-at-html-tags -->
{#if display}
<div
class="super-debug"
class:super-debug--collapsible={collapsible}
style={themeStyle}
dir="ltr"
>
<div
class="super-debug--status {label === ''
? 'super-debug--absolute super-debug--inset-x-0 super-debug--top-0'
: ''}"
>
<div class="super-debug--label">{label}</div>
<div class="super-debug--right-status">
<button type="button" class="super-debug--copy" onclick={copyContent}>
{#if !copied}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"
/><path
d="M4.012 16.737A2.005 2.005 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"
/></g
></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path d="M15 12v6m-3-3h6" /><rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></g
></svg
>
{/if}
</button>
{#if status}
<div
class:super-debug--info={page.status < 200}
class:super-debug--success={page.status >= 200 && page.status < 300}
class:super-debug--redirect={page.status >= 300 && page.status < 400}
class:super-debug--error={page.status >= 400}
>
{page.status}
</div>
{/if}
</div>
</div>
<pre
class="super-debug--pre"
class:super-debug--with-label={label}
class:super-debug--hidden={collapsed}
bind:this={ref}><code class="super-debug--code"
>{#if children}{@render children()}{:else if assertPromise(debugData, raw, promise)}{#await debugData}<div
class="super-debug--promise-loading">Loading data...</div>{:then result}{@html syntaxHighlight(
assertStore(result, raw) ? fromStore(result) : result
)}{:catch error}<span class="super-debug--promise-rejected">Rejected:</span
> {@html syntaxHighlight(error)}{/await}{:else}{@html syntaxHighlight(
debugData
)}{/if}</code
></pre>
{#if collapsible}
<button
type="button"
onclick={(e) => {
e.preventDefault();
setCollapse(!collapsed);
}}
class="super-debug--collapse"
aria-label="Collapse"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
class:super-debug--rotated={collapsed}
><path
fill="currentColor"
d="M4.08 11.92L12 4l7.92 7.92l-1.42 1.41l-5.5-5.5V22h-2V7.83l-5.5 5.5l-1.42-1.41M12 4h10V2H2v2h10Z"
/></svg
>
</button>
{/if}
</div>
{/if}
<!--
@component
SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
Other use cases includes debugging plain objects, promises, stores and more.
More info: https://superforms.rocks/super-debug
This is the rune (Svelte 5) version of SuperDebug, which will be default in the next major release.
**Short example:**
```svelte
<script>
import SuperDebug from 'sveltekit-superforms/SuperDebug.svelte';
import { superForm } from 'sveltekit-superforms';
const { data } = $props();
const { errors, form, enhance } = superForm(data.form);
</script>
<SuperDebug data={$form} label="My form data" />
```
-->

View File

@@ -0,0 +1,144 @@
import { SvelteComponentTyped } from "svelte";
import type { Readable } from 'svelte/store';
type EncodeableData = unknown | Promise<unknown>;
type EncodeableDataStore = Readable<EncodeableData>;
type DebugData = EncodeableData | EncodeableDataStore;
type $$ComponentProps = {
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
data: DebugData;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
display?: boolean;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
status?: boolean;
/**
* Optional label to identify the component easily.
*/
label?: string;
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
stringTruncate?: number;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
ref?: HTMLPreElement | undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
promise?: boolean;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
raw?: boolean;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
functions?: boolean;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
theme?: 'default' | 'vscode';
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
collapsible?: boolean;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
collapsed?: boolean;
children?: import('svelte').Snippet;
};
declare const __propDef: {
props: $$ComponentProps;
events: {
[evt: string]: CustomEvent<any>;
};
slots: {};
};
export type SuperDebugRunedProps = typeof __propDef.props;
export type SuperDebugRunedEvents = typeof __propDef.events;
export type SuperDebugRunedSlots = typeof __propDef.slots;
/**
* SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
*
* Other use cases includes debugging plain objects, promises, stores and more.
*
* More info: https://superforms.rocks/super-debug
*
* This is the rune (Svelte 5) version of SuperDebug, which will be default in the next major release.
*
* **Short example:**
*
* ```svelte
* <script>
* import SuperDebug from 'sveltekit-superforms/SuperDebug.svelte';
* import { superForm } from 'sveltekit-superforms';
*
* const { data } = $props();
*
* const { errors, form, enhance } = superForm(data.form);
* </script>
*
* <SuperDebug data={$form} label="My form data" />
* ```
*/
export default class SuperDebugRuned extends SvelteComponentTyped<SuperDebugRunedProps, SuperDebugRunedEvents, SuperDebugRunedSlots> {
}
export {};

View File

@@ -0,0 +1,4 @@
/**
* @param {string} text
*/
export function clipboardCopy(text: string): Promise<void>;

View File

@@ -0,0 +1,69 @@
/*! clipboard-copy. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
function makeError() {
return new DOMException('The request is not allowed', 'NotAllowedError');
}
/**
* @param {string} text
*/
async function copyClipboardApi(text) {
// Use the Async Clipboard API when available. Requires a secure browsing
// context (i.e. HTTPS)
if (!navigator.clipboard) {
throw makeError();
}
return navigator.clipboard.writeText(text);
}
/**
* @param {string} text
*/
async function copyExecCommand(text) {
// Put the text to copy into a <span>
const span = document.createElement('span');
span.textContent = text;
// Preserve consecutive spaces and newlines
span.style.whiteSpace = 'pre';
span.style.webkitUserSelect = 'auto';
span.style.userSelect = 'all';
// Add the <span> to the page
document.body.appendChild(span);
// Make a selection object representing the range of text selected by the user
const selection = window.getSelection();
const range = window.document.createRange();
selection?.removeAllRanges();
range.selectNode(span);
selection?.addRange(range);
// Copy text to the clipboard
let success = false;
try {
success = window.document.execCommand('copy');
} finally {
// Cleanup
selection?.removeAllRanges();
window.document.body.removeChild(span);
}
if (!success) throw makeError();
}
/**
* @param {string} text
*/
export async function clipboardCopy(text) {
try {
await copyClipboardApi(text);
} catch (err) {
// ...Otherwise, use document.execCommand() fallback
try {
await copyExecCommand(text);
} catch (err2) {
throw err2 || err || makeError();
}
}
}

View File

@@ -0,0 +1,3 @@
import type { ValidationErrors } from './index.js';
export declare function updateCustomValidity(validityEl: HTMLElement, errors: string[] | undefined): Promise<void>;
export declare function setCustomValidityForm(formElement: HTMLFormElement, errors: ValidationErrors<Record<string, unknown>>): void;

View File

@@ -0,0 +1,34 @@
import { splitPath } from '../stringPath.js';
import { traversePath } from '../traversal.js';
const noCustomValidityDataAttribute = 'noCustomValidity';
export async function updateCustomValidity(validityEl, errors) {
// Always reset validity, in case it has been validated on the server.
if ('setCustomValidity' in validityEl) {
validityEl.setCustomValidity('');
}
if (noCustomValidityDataAttribute in validityEl.dataset)
return;
setCustomValidity(validityEl, errors);
}
export function setCustomValidityForm(formElement, errors) {
for (const el of formElement.querySelectorAll('input,select,textarea,button')) {
if (('dataset' in el && noCustomValidityDataAttribute in el.dataset) || !el.name) {
continue;
}
const path = traversePath(errors, splitPath(el.name));
const error = path && typeof path.value === 'object' && '_errors' in path.value
? path.value._errors
: path?.value;
setCustomValidity(el, error);
if (error)
return;
}
}
function setCustomValidity(el, errors) {
if (!('setCustomValidity' in el))
return;
const message = errors && errors.length ? errors.join('\n') : '';
el.setCustomValidity(message);
if (message)
el.reportValidity();
}

View File

@@ -0,0 +1,10 @@
export declare const isElementInViewport: (el: Element, topOffset?: number) => boolean;
export declare const scrollToAndCenter: (el: Element, offset?: number, behavior?: ScrollBehavior) => void;
/**
* Information about a HTML element, for determining when to display errors.
*/
export declare function inputInfo(el: EventTarget | null): {
immediate: boolean;
multiple: boolean;
file: boolean;
};

View File

@@ -0,0 +1,29 @@
// https://stackoverflow.com/a/7557433/70894
export const isElementInViewport = (el, topOffset = 0) => {
const rect = el.getBoundingClientRect();
return (rect.top >= topOffset &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) /* or $(window).height() */ &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */);
};
// https://stackoverflow.com/a/36499256/70894
export const scrollToAndCenter = (el, offset = 1.125, behavior = 'smooth') => {
const elementRect = el.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const top = absoluteElementTop - window.innerHeight / (2 * offset);
window.scrollTo({ left: 0, top, behavior });
};
const immediateInputTypes = ['checkbox', 'radio', 'range', 'file'];
/**
* Information about a HTML element, for determining when to display errors.
*/
export function inputInfo(el) {
const immediate = !!el &&
(el instanceof HTMLSelectElement ||
(el instanceof HTMLInputElement && immediateInputTypes.includes(el.type)));
const multiple = !!el && el instanceof HTMLSelectElement && el.multiple;
const file = !!el && el instanceof HTMLInputElement && el.type == 'file';
return { immediate, multiple, file };
}

View File

@@ -0,0 +1,3 @@
import type { FormOptions } from './superForm.js';
export declare function cancelFlash<T extends Record<string, unknown>, M>(options: FormOptions<T, M>): void;
export declare function shouldSyncFlash<T extends Record<string, unknown>, M>(options: FormOptions<T, M>): boolean | undefined;

View File

@@ -0,0 +1,13 @@
import { browser } from '$app/environment';
export function cancelFlash(options) {
if (!options.flashMessage || !browser)
return;
if (!shouldSyncFlash(options))
return;
document.cookie = `flash=; Max-Age=0; Path=${options.flashMessage.cookiePath ?? '/'};`;
}
export function shouldSyncFlash(options) {
if (!options.flashMessage || !browser)
return false;
return options.syncFlashMessage;
}

View File

@@ -0,0 +1,19 @@
import type { Writable } from 'svelte/store';
import type { FormOptions } from './superForm.js';
/**
* @DCI-context
*/
export declare function Form<T extends Record<string, unknown>, M>(formElement: HTMLFormElement, timers: {
submitting: Writable<boolean>;
delayed: Writable<boolean>;
timeout: Writable<boolean>;
}, options: FormOptions<T, M>): {
submitting(): void;
completed: (opts: {
cancelled: boolean;
clearAll?: boolean;
}) => void;
scrollToFirstError(): void;
isSubmitting: () => boolean;
};
export declare const scrollToFirstError: <T extends Record<string, unknown>, M>(Form: HTMLFormElement, options: FormOptions<T, M>) => Promise<void>;

View File

@@ -0,0 +1,159 @@
import { afterNavigate } from '$app/navigation';
import { onDestroy, tick } from 'svelte';
import { isElementInViewport, scrollToAndCenter } from './elements.js';
var FetchStatus;
(function (FetchStatus) {
FetchStatus[FetchStatus["Idle"] = 0] = "Idle";
FetchStatus[FetchStatus["Submitting"] = 1] = "Submitting";
FetchStatus[FetchStatus["Delayed"] = 2] = "Delayed";
FetchStatus[FetchStatus["Timeout"] = 3] = "Timeout";
})(FetchStatus || (FetchStatus = {}));
const activeTimers = new Set();
//let _initialized = false;
/**
* @DCI-context
*/
export function Form(formElement, timers, options) {
let state = FetchStatus.Idle;
let delayedTimeout, timeoutTimeout;
//#region Timers
const Timers = activeTimers;
// https://www.nngroup.com/articles/response-times-3-important-limits/
function Timers_start() {
Timers_clear();
Timers_setState(state != FetchStatus.Delayed ? FetchStatus.Submitting : FetchStatus.Delayed);
delayedTimeout = window.setTimeout(() => {
if (delayedTimeout && state == FetchStatus.Submitting)
Timers_setState(FetchStatus.Delayed);
}, options.delayMs);
timeoutTimeout = window.setTimeout(() => {
if (timeoutTimeout && state == FetchStatus.Delayed)
Timers_setState(FetchStatus.Timeout);
}, options.timeoutMs);
Timers.add(Timers_clear);
}
/**
* Clear timers and set state to Idle.
*/
function Timers_clear() {
clearTimeout(delayedTimeout);
clearTimeout(timeoutTimeout);
delayedTimeout = timeoutTimeout = 0;
Timers.delete(Timers_clear);
Timers_setState(FetchStatus.Idle);
}
function Timers_clearAll() {
Timers.forEach((t) => t());
Timers.clear();
}
function Timers_setState(s) {
state = s;
timers.submitting.set(state >= FetchStatus.Submitting);
timers.delayed.set(state >= FetchStatus.Delayed);
timers.timeout.set(state >= FetchStatus.Timeout);
}
//#endregion
//#region ErrorTextEvents
const ErrorTextEvents = formElement;
function ErrorTextEvents__selectText(e) {
const target = e.target;
if (options.selectErrorText)
target.select();
}
function ErrorTextEvents_addErrorTextListeners() {
if (!options.selectErrorText)
return;
ErrorTextEvents.querySelectorAll('input').forEach((el) => {
el.addEventListener('invalid', ErrorTextEvents__selectText);
});
}
function ErrorTextEvents_removeErrorTextListeners() {
if (!options.selectErrorText)
return;
ErrorTextEvents.querySelectorAll('input').forEach((el) => el.removeEventListener('invalid', ErrorTextEvents__selectText));
}
//#endregion
//#region Form
const Form = formElement;
//#endregion
{
ErrorTextEvents_addErrorTextListeners();
const completed = (opts) => {
if (!opts.clearAll)
Timers_clear();
else
Timers_clearAll();
if (!opts.cancelled)
setTimeout(() => scrollToFirstError(Form, options), 1);
};
onDestroy(() => {
ErrorTextEvents_removeErrorTextListeners();
completed({ cancelled: true });
});
afterNavigate(() => {
ErrorTextEvents_removeErrorTextListeners();
completed({ cancelled: false });
});
return {
submitting() {
Timers_start();
},
completed,
scrollToFirstError() {
setTimeout(() => scrollToFirstError(Form, options), 1);
},
isSubmitting: () => state === FetchStatus.Submitting || state === FetchStatus.Delayed
};
}
}
export const scrollToFirstError = async (Form, options) => {
if (options.scrollToError == 'off')
return;
const selector = options.errorSelector;
if (!selector)
return;
// Wait for form to update with errors
await tick();
// Scroll to first form message, if not visible
let el;
el = Form.querySelector(selector);
if (!el)
return;
// Find underlying element if it is a FormGroup element
el = el.querySelector(selector) ?? el;
const nav = options.stickyNavbar
? document.querySelector(options.stickyNavbar)
: null;
if (typeof options.scrollToError != 'string') {
el.scrollIntoView(options.scrollToError);
}
else if (!isElementInViewport(el, nav?.offsetHeight ?? 0)) {
scrollToAndCenter(el, undefined, options.scrollToError);
}
function Form_shouldAutoFocus(userAgent) {
if (typeof options.autoFocusOnError === 'boolean')
return options.autoFocusOnError;
else
return !/iPhone|iPad|iPod|Android/i.test(userAgent);
}
// Don't focus on the element if on mobile, it will open the keyboard
// and probably hide the error message.
if (!Form_shouldAutoFocus(navigator.userAgent))
return;
let focusEl;
focusEl = el;
if (!['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA'].includes(focusEl.tagName)) {
focusEl = focusEl.querySelector('input:not([type="hidden"]):not(.flatpickr-input), select, textarea');
}
if (focusEl) {
try {
focusEl.focus({ preventScroll: true });
if (options.selectErrorText && focusEl.tagName == 'INPUT') {
focusEl.select();
}
}
catch {
// Some hidden inputs like from flatpickr cannot be focused.
}
}
};

View File

@@ -0,0 +1,9 @@
export { superForm } from './superForm.js';
export { intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy, type FieldProxy, type ArrayProxy, type FormFieldProxy } from './proxies.js';
export { defaults, defaultValues } from '../defaults.js';
export { actionResult } from '../actionResult.js';
export { schemaShape } from '../jsonSchema/schemaShape.js';
export { superValidate, message, setMessage, setError, withFiles, removeFiles, fail, type SuperValidated, type SuperValidateOptions, type TaintedFields, type ValidationErrors } from '../superValidate.js';
export type { Infer, InferIn, Schema } from '../adapters/adapters.js';
export type { FormResult, FormOptions, SuperForm, SuperFormData, SuperFormErrors, SuperFormEventList, SuperFormEvents, SuperFormSnapshot, ValidateOptions, TaintOption, ChangeEvent } from './superForm.js';
export { type FormPath, type FormPathLeaves, type FormPathLeavesWithErrors, type FormPathArrays, type FormPathType } from '../stringPath.js';

View File

@@ -0,0 +1,12 @@
// Backwards compatibility, everything should be imported from top-level in v2.
export { superForm } from './superForm.js';
export { intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy } from './proxies.js';
/////////////////////////////////////////////////////////////////////
// Duplicated from server/index.ts,
// because "server" path cannot be imported on client.
export { defaults, defaultValues } from '../defaults.js';
export { actionResult } from '../actionResult.js';
export { schemaShape } from '../jsonSchema/schemaShape.js';
export { superValidate, message, setMessage, setError, withFiles, removeFiles, fail } from '../superValidate.js';
// Exporting from stringPath also, for convenience in components.
export {} from '../stringPath.js';

View File

@@ -0,0 +1,93 @@
import { type Readable, type Updater, type Writable } from 'svelte/store';
import type { InputConstraint } from '../jsonSchema/constraints.js';
import { type FormPath, type FormPathLeaves, type FormPathType } from '../stringPath.js';
import type { FormPathArrays } from '../stringPath.js';
import type { SuperForm, TaintOption } from './superForm.js';
import type { IsAny, Prettify } from '../utils.js';
export type ProxyOptions = {
taint?: TaintOption;
};
type FormPaths<T extends Record<string, unknown>, Type = any> = FormPath<T, Type> | FormPathLeaves<T, Type>;
type CorrectProxyType<In, Out, T extends Record<string, unknown>, Path extends FormPaths<T>> = NonNullable<FormPathType<T, Path>> extends In ? Writable<Out> : never;
type PathType<Type, T, Path extends string> = IsAny<Type> extends true ? FormPathType<T, Path> : Type;
type Nullable<T extends Record<string, unknown>, Path extends FormPaths<T> | FormPathArrays<T> | FormPathLeaves<T>> = null extends FormPathType<T, Path> ? null : never;
type Optional<T extends Record<string, unknown>, Path extends FormPaths<T> | FormPathArrays<T> | FormPathLeaves<T>> = [undefined] extends [FormPathType<T, Path>] ? undefined : never;
type DefaultOptions = {
trueStringValue: string;
dateFormat: 'date' | 'datetime' | 'time' | 'date-utc' | 'datetime-utc' | 'time-utc' | 'date-local' | 'datetime-local' | 'time-local' | 'iso';
delimiter?: '.' | ',';
empty?: 'null' | 'undefined' | 'zero';
initiallyEmptyIfZero?: boolean;
taint?: TaintOption;
step: number;
};
export declare function booleanProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'trueStringValue' | 'empty' | 'taint'>>): CorrectProxyType<boolean, string, T, Path>;
export declare function intProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'empty' | 'initiallyEmptyIfZero' | 'taint'>>): CorrectProxyType<number, string, T, Path>;
export declare function numberProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'empty' | 'delimiter' | 'initiallyEmptyIfZero' | 'taint'>>): CorrectProxyType<number, string, T, Path>;
export declare function dateProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: {
format?: DefaultOptions['dateFormat'];
empty?: Exclude<DefaultOptions['empty'], 'zero'>;
taint?: TaintOption;
step?: number;
}): CorrectProxyType<Date, string, T, Path>;
export declare function stringProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options: {
empty: NonNullable<Exclude<DefaultOptions['empty'], 'zero'>>;
taint?: TaintOption;
}): Writable<string>;
export declare function fileFieldProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, File>>(form: SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): FormFieldProxy<FileList | File | Nullable<T, Path> | Optional<T, Path>, Path>;
export declare function fileProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, File>>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): {
subscribe(this: void, run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(this: void, file: FileList | File | Nullable<T, Path> | Optional<T, Path>): void;
update(this: void): never;
};
export declare function filesFieldProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, File[]>>(form: SuperForm<T>, path: Path, options?: ProxyOptions): {
values: {
subscribe(run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(files: FileList | File[] | Nullable<T, Path> | Optional<T, Path>): void;
update(updater: Updater<File[] | Nullable<T, Path> | Optional<T, Path>>): void;
};
path: Path;
errors: Writable<string[] | undefined>;
valueErrors: Writable<ValueErrors>;
};
export declare function filesProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, File[]>>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): {
subscribe(run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(files: FileList | File[] | Nullable<T, Path> | Optional<T, Path>): void;
update(updater: Updater<File[] | Nullable<T, Path> | Optional<T, Path>>): void;
};
type ValueErrors = any[];
export type ArrayProxy<T, Path = string, Errors = ValueErrors, ExtraValues = never> = {
path: Path;
values: Writable<(T[] & unknown[]) | ExtraValues>;
errors: Writable<string[] | undefined>;
valueErrors: Writable<Errors>;
};
export declare function arrayProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, ArrType>, ArrType = any>(superForm: SuperForm<T>, path: Path, options?: {
taint?: TaintOption;
}): ArrayProxy<FormPathType<T, Path> extends (infer U)[] ? U : never, Path>;
export type FormFieldProxy<T, Path = string> = {
path: Path;
value: SuperFieldProxy<T>;
errors: Writable<string[] | undefined>;
constraints: Writable<InputConstraint | undefined>;
tainted: Writable<boolean | undefined>;
};
export declare function formFieldProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, Type>, Type = any>(superForm: SuperForm<T>, path: Path, options?: ProxyOptions): FormFieldProxy<PathType<Type, T, Path>, Path>;
type SuperFieldProxy<T> = {
subscribe: Readable<T>['subscribe'];
set(this: void, value: T, options?: {
taint?: TaintOption;
}): void;
update(this: void, updater: Updater<T>, options?: {
taint?: TaintOption;
}): void;
};
export type FieldProxy<T> = Writable<T>;
export declare function fieldProxy<T extends Record<string, unknown>, Path extends FormPaths<T, Type>, Type = any>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions): FieldProxy<PathType<Type, T, Path>>;
export {};

View File

@@ -0,0 +1,502 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { derived, get, writable } from 'svelte/store';
import { SuperFormError } from '../errors.js';
import { pathExists, traversePath } from '../traversal.js';
import { splitPath } from '../stringPath.js';
import { browser } from '$app/environment';
const defaultOptions = {
trueStringValue: 'true',
dateFormat: 'iso',
step: 60
};
///// Proxy functions ///////////////////////////////////////////////
export function booleanProxy(form, path, options) {
return _stringProxy(form, path, 'boolean', {
...defaultOptions,
...options
});
}
export function intProxy(form, path, options) {
return _stringProxy(form, path, 'int', {
...defaultOptions,
...options
});
}
export function numberProxy(form, path, options) {
return _stringProxy(form, path, 'number', {
...defaultOptions,
...options
});
}
export function dateProxy(form, path, options) {
return _stringProxy(form, path, 'date', {
...defaultOptions,
dateFormat: options?.format ?? 'iso',
empty: options?.empty,
step: options?.step ?? 60
});
}
export function stringProxy(form, path, options) {
return _stringProxy(form, path, 'string', {
...defaultOptions,
...options
});
}
export function fileFieldProxy(form, path, options) {
const fileField = fileProxy(form, path, options);
const formField = formFieldProxy(form, path, options);
return { ...formField, value: fileField };
}
export function fileProxy(form, path, options) {
const formFile = fieldProxy(form, path, options);
const fileProxy = writable(browser ? new DataTransfer().files : {});
let initialized = false;
let initialValue;
formFile.subscribe((file) => {
if (!browser)
return;
if (!initialized) {
initialValue = options?.empty ? (options.empty === 'undefined' ? undefined : null) : file;
initialized = true;
}
const dt = new DataTransfer();
if (file instanceof File)
dt.items.add(file);
fileProxy.set(dt.files);
});
const fileStore = {
subscribe(run) {
return fileProxy.subscribe(run);
},
set(file) {
if (!browser)
return;
if (!file) {
const dt = new DataTransfer();
fileProxy.set(dt.files);
formFile.set(file);
}
else if (file instanceof File) {
const dt = new DataTransfer();
dt.items.add(file);
fileProxy.set(dt.files);
formFile.set(file);
}
else if (file instanceof FileList) {
fileProxy.set(file);
if (file.length > 0)
formFile.set(file.item(0));
else
formFile.set(initialValue);
}
},
update() {
throw new SuperFormError('You cannot update a fileProxy, only set it.');
}
};
return fileStore;
}
export function filesFieldProxy(form, path, options) {
const filesStore = filesProxy(form, path, options);
const arrayField = arrayProxy(form, path, options);
return { ...arrayField, values: filesStore };
}
export function filesProxy(form, path, options) {
const formFiles = fieldProxy(form, path, options);
const filesProxy = writable(browser ? new DataTransfer().files : {});
let initialized = false;
let initialValue;
formFiles.subscribe((files) => {
if (!browser)
return;
if (!initialized) {
initialValue = options?.empty ? (options.empty === 'undefined' ? undefined : null) : files;
initialized = true;
}
const dt = new DataTransfer();
if (Array.isArray(files)) {
if (files.length && files.every((f) => !f)) {
formFiles.set([]);
return;
}
files.filter((f) => f instanceof File).forEach((file) => dt.items.add(file));
}
filesProxy.set(dt.files);
});
const filesStore = {
subscribe(run) {
return filesProxy.subscribe(run);
},
set(files) {
if (!browser)
return;
if (!(files instanceof FileList)) {
const dt = new DataTransfer();
if (Array.isArray(files))
files.forEach((file) => {
if (file instanceof File)
dt.items.add(file);
});
filesProxy.set(dt.files);
formFiles.set(files);
}
else {
if (files.length > 0) {
const output = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file)
output.push(file);
}
filesProxy.set(files);
formFiles.set(output);
}
else {
formFiles.set(initialValue);
}
}
},
update(updater) {
filesStore.set(updater(get(formFiles)));
}
};
return filesStore;
}
///// Implementation ////////////////////////////////////////////////
/**
* Creates a string store that will pass its value to a field in the form.
* @param form The form
* @param field Form field
* @param type 'number' | 'int' | 'boolean'
*/
function _stringProxy(form, path, type, options) {
function toValue(value) {
if (!value && options.empty !== undefined) {
return options.empty === 'null' ? null : options.empty === 'zero' ? 0 : undefined;
}
if (typeof value === 'number') {
value = value.toString();
}
if (typeof value !== 'string') {
// Can be undefined due to Proxy in Svelte 5
value = '';
}
const stringValue = value;
if (type == 'string')
return stringValue;
else if (type == 'boolean')
return !!stringValue;
else if (type == 'date') {
if (stringValue.indexOf('-') === -1) {
const utc = options.dateFormat.indexOf('utc') >= 0;
const date = utc ? UTCDate(new Date()) : localDate(new Date());
return new Date(date + 'T' + stringValue + (utc ? 'Z' : ''));
}
else
return new Date(stringValue);
}
const numberToConvert = options.delimiter
? stringValue.replace(options.delimiter, '.')
: stringValue;
let num;
if (numberToConvert === '' && options.empty == 'zero')
num = 0;
else if (type == 'number')
num = parseFloat(numberToConvert);
else
num = parseInt(numberToConvert, 10);
return num;
}
const isSuper = isSuperForm(form, options);
const realProxy = isSuper
? superFieldProxy(form, path, { taint: options.taint })
: fieldProxy(form, path);
let updatedValue = null;
let initialized = false;
const proxy = derived(realProxy, (value) => {
if (!initialized) {
initialized = true;
if (options.initiallyEmptyIfZero && !value)
return '';
}
// Prevent proxy updating itself
if (updatedValue !== null) {
const current = updatedValue;
updatedValue = null;
return current;
}
if (value === undefined || value === null)
return '';
if (type == 'string') {
return value;
}
else if (type == 'int' || type == 'number') {
if (value === '') {
// Special case for empty string values in number proxies
// Set the value to 0, to conform to the type.
realProxy.set(0, isSuper ? { taint: false } : undefined);
}
if (typeof value === 'number' && isNaN(value))
return '';
return String(value);
}
else if (type == 'date') {
const date = typeof value === 'string' || typeof value === 'number' ? new Date(value) : value;
if (isNaN(date))
return '';
switch (options.dateFormat) {
case 'iso':
return date.toISOString();
case 'date':
return date.toISOString().slice(0, 10);
case 'datetime':
return date.toISOString().slice(0, options.step % 60 ? 19 : 16);
case 'time':
return date.toISOString().slice(11, options.step % 60 ? 19 : 16);
case 'date-utc':
return UTCDate(date);
case 'datetime-utc':
return UTCDate(date) + 'T' + UTCTime(date, options.step);
case 'time-utc':
return UTCTime(date, options.step);
case 'date-local':
return localDate(date);
case 'datetime-local':
return localDate(date) + 'T' + localTime(date, options.step);
case 'time-local':
return localTime(date, options.step);
}
}
else {
// boolean
return value ? options.trueStringValue : '';
}
});
return {
subscribe: proxy.subscribe,
set(val) {
updatedValue = val;
const newValue = toValue(updatedValue);
realProxy.set(newValue);
},
update(updater) {
realProxy.update((f) => {
updatedValue = updater(String(f));
const newValue = toValue(updatedValue);
return newValue;
});
}
};
}
export function arrayProxy(superForm, path, options) {
const formErrors = fieldProxy(superForm.errors, `${path}`);
const onlyFieldErrors = derived(formErrors, ($errors) => {
const output = [];
for (const key in $errors) {
if (key == '_errors')
continue;
output[key] = $errors[key];
}
return output;
});
function updateArrayErrors(errors, value) {
for (const key in errors) {
if (key == '_errors')
continue;
errors[key] = undefined;
}
if (value !== undefined) {
for (const key in value) {
errors[key] = value[key];
}
}
return errors;
}
const fieldErrors = {
subscribe: onlyFieldErrors.subscribe,
update(upd) {
formErrors.update(($errors) =>
// @ts-expect-error Type is correct
updateArrayErrors($errors, upd($errors)));
},
set(value) {
// @ts-expect-error Type is correct
formErrors.update(($errors) => updateArrayErrors($errors, value));
}
};
const values = superFieldProxy(superForm, path, options);
// If array is shortened, delete all keys above length
// in errors, so they won't be kept if the array is lengthened again.
let lastLength = Array.isArray(get(values)) ? get(values).length : 0;
values.subscribe(($values) => {
const currentLength = Array.isArray($values) ? $values.length : 0;
if (currentLength < lastLength) {
superForm.errors.update(($errors) => {
const node = pathExists($errors, splitPath(path));
if (!node)
return $errors;
for (const key in node.value) {
if (Number(key) < currentLength)
continue;
delete node.value[key];
}
return $errors;
}, { force: true });
}
lastLength = currentLength;
});
return {
path,
values: values,
errors: fieldProxy(superForm.errors, `${path}._errors`),
valueErrors: fieldErrors
};
}
export function formFieldProxy(superForm, path, options) {
const path2 = splitPath(path);
// Filter out array indices, the constraints structure doesn't contain these.
const constraintsPath = path2.filter((p) => /\D/.test(String(p))).join('.');
const taintedProxy = derived(superForm.tainted, ($tainted) => {
if (!$tainted)
return $tainted;
const taintedPath = traversePath($tainted, path2);
return taintedPath ? taintedPath.value : undefined;
});
const tainted = {
subscribe: taintedProxy.subscribe,
update(upd) {
superForm.tainted.update(($tainted) => {
if (!$tainted)
$tainted = {};
const output = traversePath($tainted, path2, (path) => {
if (!path.value)
path.parent[path.key] = {};
return path.parent[path.key];
});
if (output)
output.parent[output.key] = upd(output.value);
return $tainted;
});
},
set(value) {
superForm.tainted.update(($tainted) => {
if (!$tainted)
$tainted = {};
const output = traversePath($tainted, path2, (path) => {
if (!path.value)
path.parent[path.key] = {};
return path.parent[path.key];
});
if (output)
output.parent[output.key] = value;
return $tainted;
});
}
};
return {
path,
value: superFieldProxy(superForm, path, options),
errors: fieldProxy(superForm.errors, path),
constraints: fieldProxy(superForm.constraints, constraintsPath),
tainted
};
}
function updateProxyField(obj, path, updater) {
const output = traversePath(obj, path, ({ parent, key, value }) => {
if (value === undefined)
parent[key] = /\D/.test(key) ? {} : [];
return parent[key];
});
if (output) {
const newValue = updater(output.value);
output.parent[output.key] = newValue;
}
return obj;
}
function superFieldProxy(superForm, path, baseOptions) {
const form = superForm.form;
const path2 = splitPath(path);
const proxy = derived(form, ($form) => {
const data = traversePath($form, path2);
return data?.value;
});
return {
subscribe(...params) {
const unsub = proxy.subscribe(...params);
return () => unsub();
},
update(upd, options) {
form.update((data) => updateProxyField(data, path2, upd), options ?? baseOptions);
},
set(value, options) {
form.update((data) => updateProxyField(data, path2, () => value), options ?? baseOptions);
}
};
}
function isSuperForm(form, options) {
const isSuperForm = 'form' in form;
if (!isSuperForm && options?.taint !== undefined) {
throw new SuperFormError('If options.taint is set, the whole superForm object must be used as a proxy.');
}
return isSuperForm;
}
export function fieldProxy(form, path, options) {
const path2 = splitPath(path);
if (isSuperForm(form, options)) {
return superFieldProxy(form, path, options);
}
const proxy = derived(form, ($form) => {
const data = traversePath($form, path2);
return data?.value;
});
return {
subscribe(...params) {
const unsub = proxy.subscribe(...params);
return () => unsub();
},
update(upd) {
form.update((data) => updateProxyField(data, path2, upd));
},
set(value) {
form.update((data) => updateProxyField(data, path2, () => value));
}
};
}
function localDate(date) {
return (date.getFullYear() +
'-' +
String(date.getMonth() + 1).padStart(2, '0') +
'-' +
String(date.getDate()).padStart(2, '0'));
}
function localTime(date, step) {
return (String(date.getHours()).padStart(2, '0') +
':' +
String(date.getMinutes()).padStart(2, '0') +
(step % 60 ? ':' + String(date.getSeconds()).padStart(2, '0') : ''));
}
function UTCDate(date) {
return (date.getUTCFullYear() +
'-' +
String(date.getUTCMonth() + 1).padStart(2, '0') +
'-' +
String(date.getUTCDate()).padStart(2, '0'));
}
function UTCTime(date, step) {
return (String(date.getUTCHours()).padStart(2, '0') +
':' +
String(date.getUTCMinutes()).padStart(2, '0') +
(step % 60 ? ':' + String(date.getUTCSeconds()).padStart(2, '0') : ''));
}
/*
function dateToUTC(date: Date) {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
);
}
*/

View File

@@ -0,0 +1,232 @@
import type { TaintedFields, SuperFormValidated, SuperValidated } from '../superValidate.js';
import type { ActionResult, BeforeNavigate, Page, SubmitFunction, Transport } from '@sveltejs/kit';
import { type Readable, type Writable, type Updater } from 'svelte/store';
import { type FormPathType, type FormPath, type FormPathLeaves } from '../stringPath.js';
import { enhance as kitEnhance } from '$app/forms';
import type { ValidationErrors } from '../superValidate.js';
import type { IsAny, MaybePromise } from '../utils.js';
import type { ClientValidationAdapter, ValidationAdapter } from '../adapters/adapters.js';
import type { InputConstraints } from '../jsonSchema/constraints.js';
import { type ProxyOptions } from './proxies.js';
export type SuperFormEvents<T extends Record<string, unknown>, M> = Pick<FormOptions<T, M>, 'onError' | 'onResult' | 'onSubmit' | 'onUpdate' | 'onUpdated'>;
export type SuperFormEventList<T extends Record<string, unknown>, M> = {
[Property in keyof SuperFormEvents<T, M>]-?: NonNullable<SuperFormEvents<T, M>[Property]>[];
};
type FilterType<T, Check> = {
[K in keyof NonNullable<T> as NonNullable<NonNullable<T>[K]> extends Check ? never : K]: NonNullable<T>[K];
};
/**
* Helper type for making ActionResult data strongly typed in onUpdate.
* @example const action : FormResult<ActionData> = result.data;
*/
export type FormResult<T extends Record<string, unknown> | null | undefined> = FilterType<T, SuperValidated<Record<string, unknown>, any, Record<string, unknown>>>;
export type TaintOption = boolean | 'untaint' | 'untaint-all' | 'untaint-form';
type ValidatorsOption<T extends Record<string, unknown>> = ValidationAdapter<Partial<T>, Record<string, unknown>> | false | 'clear';
export type FormOptions<T extends Record<string, unknown> = Record<string, unknown>, M = any, In extends Record<string, unknown> = T> = Partial<{
id: string;
/**
* If `false`, the form won't react to page state updates, except for page invalidations.
* If `'never'`, not even page invalidations will affect the form.
* @default true
*/
applyAction: boolean | 'never';
invalidateAll: boolean | 'force' | 'pessimistic';
resetForm: boolean | (() => boolean);
scrollToError: 'auto' | 'smooth' | 'off' | boolean | ScrollIntoViewOptions;
autoFocusOnError: boolean | 'detect';
errorSelector: string;
selectErrorText: boolean;
stickyNavbar: string;
taintedMessage: string | boolean | null | ((nav: BeforeNavigate) => MaybePromise<boolean>);
/**
* Enable single page application (SPA) mode.
* **The string and failStatus options are deprecated** and will be removed in the next major release.
* @see https://superforms.rocks/concepts/spa
*/
SPA: true | /** @deprecated */ string | /** @deprecated */ {
failStatus?: number;
};
onSubmit: (input: Parameters<SubmitFunction>[0] & {
/**
* If dataType: 'json' is set, send this data instead of $form when posting,
* and client-side validation for $form passes.
* @param data An object that can be serialized with devalue.
*/
jsonData: (data: Record<string, unknown>) => void;
/**
* Override client validation temporarily for this form submission.
*/
validators: (validators: Exclude<ValidatorsOption<T>, 'clear'>) => void;
/**
* Use a custom fetch or XMLHttpRequest implementation for this form submission. It must return an ActionResult in the response body.
* If the request is using a XMLHttpRequest, the promise must be resolved when the request has been completed, not before.
*/
customRequest: (validators: (input: Parameters<SubmitFunction>[0]) => Promise<Response | XMLHttpRequest | ActionResult>) => void;
}) => MaybePromise<unknown | void>;
onResult: (event: {
result: ActionResult;
/**
* @deprecated Use formElement instead
*/
formEl: HTMLFormElement;
formElement: HTMLFormElement;
cancel: () => void;
}) => MaybePromise<unknown | void>;
onUpdate: (event: {
form: SuperValidated<T, M, In>;
/**
* @deprecated Use formElement instead
*/
formEl: HTMLFormElement;
formElement: HTMLFormElement;
cancel: () => void;
result: Required<Extract<ActionResult, {
type: 'success' | 'failure';
}>>;
}) => MaybePromise<unknown | void>;
onUpdated: (event: {
form: Readonly<SuperValidated<T, M, In>>;
}) => MaybePromise<unknown | void>;
onError: 'apply' | ((event: {
result: {
type: 'error';
status?: number;
error: App.Error | Error | {
message: string;
};
};
}) => MaybePromise<unknown | void>);
onChange: (event: ChangeEvent<T>) => void;
dataType: 'form' | 'json';
jsonChunkSize: number;
validators: ClientValidationAdapter<Partial<T>, Record<string, unknown>> | ValidatorsOption<T>;
validationMethod: 'auto' | 'oninput' | 'onblur' | 'onsubmit' | 'submit-only';
customValidity: boolean;
clearOnSubmit: 'errors' | 'message' | 'errors-and-message' | 'none';
delayMs: number;
timeoutMs: number;
multipleSubmits: 'prevent' | 'allow' | 'abort';
syncFlashMessage?: boolean;
/**
* @deprecated SvelteKit has moved to $app/state instead of $app/stores, making it hard to support both. Use the flash library directly (setFlash or redirect) instead of integrating it with Superforms.
*/
flashMessage: {
module: {
getFlash(page: Readable<Page>): Writable<App.PageData['flash']>;
updateFlash(page: Readable<Page>, update?: () => Promise<void>): Promise<boolean>;
};
onError?: (event: {
result: {
type: 'error';
status?: number;
error: App.Error | Error | {
message: string;
};
};
flashMessage: Writable<App.PageData['flash']>;
}) => MaybePromise<unknown | void>;
cookiePath?: string;
cookieName?: string;
};
warnings: {
duplicateId?: boolean;
};
transport: IsAny<Transport> extends true ? never : Transport;
/**
* Version 1 compatibilty mode if true.
* Sets resetForm = false and taintedMessage = true.
* Add define: { SUPERFORMS_LEGACY: true } to vite.config.ts to enable globally.
*/
legacy: boolean;
}>;
export type SuperFormSnapshot<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = T> = SuperFormValidated<T, M, In> & {
tainted: TaintedFields<T> | undefined;
};
export type SuperFormData<T extends Record<string, unknown>> = {
subscribe: Readable<T>['subscribe'];
set(this: void, value: T, options?: {
taint?: TaintOption;
}): void;
update(this: void, updater: Updater<T>, options?: {
taint?: TaintOption;
}): void;
};
export type SuperFormErrors<T extends Record<string, unknown>> = {
subscribe: Writable<ValidationErrors<T>>['subscribe'];
set(this: void, value: ValidationErrors<T>, options?: {
force?: boolean;
}): void;
update(this: void, updater: Updater<ValidationErrors<T>>, options?: {
force?: boolean;
}): void;
clear: () => void;
};
type ResetOptions<T extends Record<string, unknown>> = {
keepMessage?: boolean;
data?: Partial<T>;
newState?: Partial<T>;
id?: string;
};
type Capture<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = [T] extends [T] ? () => SuperFormSnapshot<T, M> : never;
type Restore<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = (snapshot: SuperFormSnapshot<T, M>) => void;
export type SuperForm<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = {
form: SuperFormData<T>;
formId: Writable<string>;
errors: SuperFormErrors<T>;
constraints: Writable<InputConstraints<T>>;
message: Writable<M | undefined>;
tainted: Writable<TaintedFields<T> | undefined>;
submitting: Readable<boolean>;
delayed: Readable<boolean>;
timeout: Readable<boolean>;
/**
* @deprecated posted is inconsistent between server and client validation, and SPA mode. Will be removed in v3. Use a status message or return your own data in the form action to handle form post status.
*/
posted: Readable<boolean>;
allErrors: Readable<{
path: string;
messages: string[];
}[]>;
options: T extends T ? FormOptions<T, M> : never;
enhance: (el: HTMLFormElement, events?: SuperFormEvents<T, M>) => ReturnType<typeof kitEnhance>;
isTainted: (path?: FormPath<T> | Record<string, unknown> | boolean | undefined) => boolean;
reset: (options?: ResetOptions<T>) => void;
submit: (submitter?: HTMLElement | Event | EventTarget | null) => void;
capture: Capture<T, M>;
restore: T extends T ? Restore<T, M> : never;
validate: <Out extends Partial<T> = T, Path extends FormPathLeaves<T> = FormPathLeaves<T>, In extends Record<string, unknown> = Record<string, unknown>>(path: Path, opts?: ValidateOptions<FormPathType<T, Path>, Out, In>) => Promise<string[] | undefined>;
validateForm: <Out extends Partial<T> = T, In extends Record<string, unknown> = Record<string, unknown>>(opts?: {
update?: boolean;
schema?: ValidationAdapter<Out, In>;
focusOnError?: boolean;
}) => Promise<SuperFormValidated<T, M, In>>;
};
export type ValidateOptions<Value, Out extends Record<string, unknown>, In extends Record<string, unknown>> = Partial<{
value: Value;
update: boolean | 'errors' | 'value';
taint: TaintOption;
errors: string | string[];
schema: ValidationAdapter<Out, In>;
}>;
export type ChangeEvent<T extends Record<string, unknown>> = {
path: FormPath<T>;
paths: FormPath<T>[];
formElement: HTMLFormElement;
target: Element;
set: <Path extends FormPath<T>>(path: Path, value: FormPathType<T, Path>, options?: ProxyOptions) => void;
get: <Path extends FormPath<T>>(path: Path) => FormPathType<T, Path>;
} | {
target: undefined;
paths: FormPath<T>[];
set: <Path extends FormPath<T>>(path: Path, value: FormPathType<T, Path>, options?: ProxyOptions) => void;
get: <Path extends FormPath<T>>(path: Path) => FormPathType<T, Path>;
};
/**
* Initializes a SvelteKit form, for convenient handling of values, errors and sumbitting data.
* @param {SuperValidated} form Usually data.form from PageData or defaults, but can also be an object with default values, but then constraints won't be available.
* @param {FormOptions} formOptions Configuration for the form.
* @returns {SuperForm} A SuperForm object that can be used in a Svelte component.
* @DCI-context
*/
export declare function superForm<T extends Record<string, unknown> = Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = T>(form: SuperValidated<T, M, In> | T, formOptions?: FormOptions<T, M, In>): SuperForm<T, M>;
export {};

File diff suppressed because it is too large Load Diff