425 lines
24 KiB
TypeScript
425 lines
24 KiB
TypeScript
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses, text, div } from "#shared/dom.util";
|
|
import { parseURL } from 'ufo';
|
|
import render from "#shared/markdown.util";
|
|
import { contextmenu, popper } from "#shared/floating.util";
|
|
import { Canvas } from "#shared/canvas.util";
|
|
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
|
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
|
import { clamp, unifySlug } from "#shared/general.util";
|
|
import { Tree } from "./tree";
|
|
|
|
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
|
export type Prose = { class: string } | { custom: CustomProse };
|
|
export const a: Prose = {
|
|
custom(properties, children) {
|
|
const href = decodeURIComponent(properties.href) as string;
|
|
const { hash, pathname } = parseURL(href);
|
|
const router = useRouter();
|
|
|
|
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
|
|
|
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
|
|
|
|
let rendered = false;
|
|
|
|
const el = dom('a', { class: 'text-accent-blue inline-flex items-center', attributes: { href: nav.href }, listeners: {
|
|
'click': (e) => {
|
|
e.preventDefault();
|
|
router.push(link);
|
|
}
|
|
}}, [
|
|
dom('span', {}, [
|
|
...(children ?? []),
|
|
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
|
|
])
|
|
]);
|
|
|
|
if(!!overview)
|
|
{
|
|
popper(el, {
|
|
arrow: true,
|
|
delay: 150,
|
|
offset: 12,
|
|
cover: "height",
|
|
placement: 'bottom-start',
|
|
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]',
|
|
content: [loading("large")],
|
|
viewport: document.getElementById('mainContainer') ?? undefined,
|
|
onShow(content: HTMLDivElement) {
|
|
if(!rendered)
|
|
{
|
|
Content.getContent(overview.id).then((_content) => {
|
|
if(_content?.type === 'markdown')
|
|
{
|
|
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'py-4 px-6' }), content.children[0]!);
|
|
}
|
|
if(_content?.type === 'canvas')
|
|
{
|
|
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
|
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
|
|
canvas.mount();
|
|
}
|
|
});
|
|
rendered = true;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
return el;
|
|
}
|
|
}
|
|
export const fakeA: Prose = {
|
|
custom(properties, children) {
|
|
const href = properties.href as string;
|
|
const { hash, pathname } = parseURL(href);
|
|
const router = useRouter();
|
|
|
|
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
|
|
|
const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [
|
|
dom('span', {}, [
|
|
...(children ?? []),
|
|
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
|
|
])
|
|
]);
|
|
|
|
if(!!overview)
|
|
{
|
|
const magicKeys = useMagicKeys();
|
|
popper(el, {
|
|
arrow: true,
|
|
delay: 150,
|
|
offset: 12,
|
|
cover: "height",
|
|
placement: 'bottom-start',
|
|
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
|
|
content: [loading("large")],
|
|
onShow(content: HTMLDivElement) {
|
|
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
|
|
return false;
|
|
|
|
content.replaceChild(loading("large"), content.children[0]!);
|
|
Content.getContent(overview.id).then((_content) => {
|
|
if(_content?.type === 'markdown')
|
|
{
|
|
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]!);
|
|
}
|
|
if(_content?.type === 'canvas')
|
|
{
|
|
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
|
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
|
|
canvas.mount();
|
|
}
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
return el;
|
|
}
|
|
}
|
|
export const callout: Prose = {
|
|
custom(properties, children) {
|
|
const calloutIconByType: Record<string, string> = {
|
|
note: 'radix-icons:pencil-1',
|
|
abstract: 'radix-icons:file-text',
|
|
info: 'radix-icons:info-circled',
|
|
todo: 'radix-icons:check-circled',
|
|
tip: 'radix-icons:star',
|
|
success: 'radix-icons:check',
|
|
question: 'radix-icons:question-mark-circled',
|
|
warning: 'radix-icons:exclamation-triangle',
|
|
failure: 'radix-icons:cross-circled',
|
|
danger: 'radix-icons:circle-backslash',
|
|
bug: 'solar:bug-linear',
|
|
example: 'radix-icons:list-bullet',
|
|
quote: 'radix-icons:quote',
|
|
};
|
|
const defaultCalloutIcon = 'radix-icons:info-circled';
|
|
|
|
const { type, title, fold }: {
|
|
type: string;
|
|
title?: string;
|
|
fold?: boolean;
|
|
} = properties;
|
|
|
|
let open = fold;
|
|
const container = dom('div', { class: 'callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
|
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
|
|
container.setAttribute('data-state', open ? 'open' : 'closed');
|
|
open = !open;
|
|
}}},
|
|
[icon(calloutIconByType[type] ?? defaultCalloutIcon, { inline: true, width: 24, height: 24, class: 'w-6 h-6 stroke-2 float-start me-2 flex-shrink-0' }), !!title ? dom('span', { class: 'block font-bold text-start', text: title }) : undefined, fold !== undefined ? icon('radix-icons:caret-right', { height: 24, width: 24, class: 'transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6' }) : undefined
|
|
]),
|
|
dom('div', { class: {'overflow-hidden': true, 'group-data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] group-data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] group-data-[state=closed]:h-0': fold !== undefined } }, [
|
|
dom('div', { class: 'px-2' }, children),
|
|
])
|
|
]);
|
|
|
|
return container;
|
|
},
|
|
}
|
|
export const tag: Prose = {
|
|
class: "before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30",
|
|
}
|
|
export const blockquote: Prose = {
|
|
class: 'empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30',
|
|
}
|
|
export const h1: Prose = {
|
|
class: 'text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2',
|
|
}
|
|
export const h2: Prose = {
|
|
class: 'text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2',
|
|
}
|
|
export const h3: Prose = {
|
|
class: 'text-2xl font-bold mt-2 mb-4',
|
|
}
|
|
export const h4: Prose = {
|
|
class: 'text-xl font-semibold my-2',
|
|
}
|
|
export const h5: Prose = {
|
|
class: 'text-lg font-semibold my-1',
|
|
}
|
|
export const hr: Prose = {
|
|
class: 'border-b border-light-35 dark:border-dark-35 m-4',
|
|
}
|
|
export const li: Prose = {
|
|
class: 'before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4',
|
|
}
|
|
export const small: Prose = {
|
|
class: 'text-light-60 dark:text-dark-60 text-sm italic',
|
|
}
|
|
export const table: Prose = {
|
|
class: 'mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35',
|
|
}
|
|
export const td: Prose = {
|
|
class: 'border border-light-35 dark:border-dark-35 py-1 px-2',
|
|
}
|
|
export const th: Prose = {
|
|
class: 'border border-light-35 dark:border-dark-35 px-4 first:pt-0',
|
|
}
|
|
|
|
export default function(tag: string, prose: Prose, children?: NodeChildren, properties?: any): Node
|
|
{
|
|
if('class' in prose)
|
|
{
|
|
return dom(tag as keyof HTMLElementTagNameMap, { class: [properties?.class, prose.class] }, children ?? []);
|
|
}
|
|
else
|
|
{
|
|
return prose.custom(properties, children ?? []);
|
|
}
|
|
}
|
|
|
|
export function link(properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>, children?: NodeChildren)
|
|
{
|
|
const router = useRouter();
|
|
const nav = link ? router.resolve(link) : undefined;
|
|
return dom('a', { ...properties, class: [properties?.class, properties?.active && router.currentRoute.value.fullPath === nav?.fullPath ? properties.active : undefined], attributes: { href: nav?.href, 'data-active': properties?.active ? mergeClasses(properties?.active) : undefined }, listeners: link ? {
|
|
click: function(e)
|
|
{
|
|
e.preventDefault();
|
|
router.push(link);
|
|
}
|
|
} : undefined }, children);
|
|
}
|
|
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
|
|
{
|
|
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
|
|
}
|
|
export function button(content: Node, onClick?: () => void, cls?: Class)
|
|
{
|
|
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
|
}
|
|
export type Option<T> = { text: string, value: T | Option<T>[] } | undefined;
|
|
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T> | undefined> };
|
|
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
|
{
|
|
let close: Function | undefined;
|
|
|
|
let disabled = settings?.disabled ?? false;
|
|
const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? '');
|
|
const optionElements = options.map(e => {
|
|
if(e === undefined)
|
|
return;
|
|
|
|
return dom('div', { listeners: { click: () => {
|
|
textValue.textContent = e.text;
|
|
settings?.change && settings?.change(e.value);
|
|
close && close();
|
|
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
|
|
});
|
|
const select = dom('div', { listeners: { click: () => {
|
|
if(disabled)
|
|
return;
|
|
|
|
const box = select.getBoundingClientRect();
|
|
close = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` } }).close;
|
|
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
|
|
|
Object.defineProperty(select, 'disabled', {
|
|
get: () => disabled,
|
|
set: (v) => {
|
|
disabled = !!v;
|
|
select.toggleAttribute('data-disabled', disabled);
|
|
},
|
|
})
|
|
return select;
|
|
}
|
|
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
|
{
|
|
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
|
let selected = true, tree: StoredOption<T>[] = [];
|
|
|
|
const show = () => {
|
|
if(disabled || (context && context.container.parentElement))
|
|
return;
|
|
|
|
const box = container.getBoundingClientRect();
|
|
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: hide });
|
|
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
|
};
|
|
const hide = () => {
|
|
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
|
tree = [];
|
|
|
|
context && context.container.parentElement && context.close();
|
|
};
|
|
const progress = (option: StoredOption<T>) => {
|
|
if(!context || !context.container.parentElement || option.container === undefined)
|
|
return;
|
|
|
|
const redrawn = render(option.item)?.container;
|
|
if(redrawn)
|
|
{
|
|
context.container.replaceChildren(redrawn);
|
|
tree.push(option);
|
|
}
|
|
};
|
|
const back = () => {
|
|
tree.pop();
|
|
const last = tree.slice(-1)[0];
|
|
|
|
last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
|
|
};
|
|
const render = (option: Option<T>): StoredOption<T> | undefined => {
|
|
if(option === undefined)
|
|
return;
|
|
|
|
if(Array.isArray(option.value))
|
|
{
|
|
const children = option.value.map(render);
|
|
|
|
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored) }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
|
|
return stored;
|
|
}
|
|
else
|
|
{
|
|
return { item: option, dom: dom('div', { listeners: { click: () => {
|
|
select.value = option.text;
|
|
settings?.change && settings?.change(option.value as T);
|
|
selected = true;
|
|
hide();
|
|
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(option.text) ]) };
|
|
}
|
|
}
|
|
const filter = (value: string, option?: StoredOption<T>): HTMLElement[] => {
|
|
if(option && option.children !== undefined)
|
|
{
|
|
return option.children.flatMap(e => filter(value, e));
|
|
}
|
|
else if(option && option.item)
|
|
{
|
|
return option.item.text.toLowerCase().normalize().includes(value) ? [ option.dom ] : [];
|
|
}
|
|
else
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
let disabled = settings?.disabled ?? false;
|
|
const optionElements = options.map(render);
|
|
const select = dom('input', { listeners: { focus: show, input: () => {
|
|
context && select.value ? context.container.replaceChildren(...optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e))) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
|
|
selected = false;
|
|
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
|
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
|
settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });
|
|
|
|
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);
|
|
|
|
Object.defineProperty(container, 'disabled', {
|
|
get: () => disabled,
|
|
set: (v) => {
|
|
disabled = !!v;
|
|
container.toggleAttribute('data-disabled', disabled);
|
|
select.toggleAttribute('disabled', disabled);
|
|
},
|
|
})
|
|
return container;
|
|
}
|
|
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
|
|
{
|
|
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
|
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
|
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
|
input: () => settings?.input && settings.input(input.value),
|
|
change: () => settings?.change && settings.change(input.value),
|
|
focus: () => settings?.focus,
|
|
blur: () => settings?.blur,
|
|
}})
|
|
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
|
|
|
return input;
|
|
}
|
|
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: () => void, blur?: () => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
|
|
{
|
|
let storedValue = settings?.defaultValue ?? 0;
|
|
const validateAndChange = (value: number) => {
|
|
if(isNaN(value))
|
|
field.value = '';
|
|
else
|
|
{
|
|
value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
|
|
field.value = value.toString(10);
|
|
if(storedValue !== value)
|
|
{
|
|
storedValue = value;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
|
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
|
keydown: (e: KeyboardEvent) => {
|
|
switch(e.key)
|
|
{
|
|
case "ArrowUp":
|
|
validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
|
break;
|
|
case "ArrowDown":
|
|
validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
|
break;
|
|
case "PageUp":
|
|
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
|
|
break;
|
|
case "PageDown":
|
|
settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
},
|
|
change: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.change && settings.change(storedValue),
|
|
focus: () => settings?.focus && settings.focus(),
|
|
blur: () => settings?.blur && settings.blur(),
|
|
}});
|
|
if(settings?.defaultValue) field.value = storedValue.toString(10);
|
|
|
|
return field;
|
|
} |