sveltekit-superforms
Version:
Making SvelteKit forms a pleasure to use!
155 lines (154 loc) • 5.48 kB
JavaScript
import { isElementInViewport, scrollToAndCenter } from './elements.js';
import { onDestroy, tick } from 'svelte';
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 });
});
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.
}
}
};