You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
6.0 KiB
TypeScript
248 lines
6.0 KiB
TypeScript
import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns";
|
|
import {
|
|
Component,
|
|
Show,
|
|
mergeProps,
|
|
splitProps,
|
|
FlowComponent,
|
|
createSignal,
|
|
createEffect,
|
|
} from "solid-js";
|
|
import { validateInput } from "~/hooks/validation";
|
|
import AsteriskIcon from "~icons/ph/asterisk-bold";
|
|
import MaximizeIcon from "~icons/carbon/maximize";
|
|
import MinimizeIcon from "~icons/carbon/minimize";
|
|
import autosize from "autosize";
|
|
import type { JSX } from "solid-js";
|
|
import {
|
|
createNativeInputValue,
|
|
createOptionalNumberInputHandler,
|
|
} from "~/util";
|
|
|
|
export const TextInput: Component<
|
|
{
|
|
label: string;
|
|
placeholder?: string;
|
|
labelMinWidth?: string;
|
|
suffix?: string;
|
|
size?: string;
|
|
vertical?: boolean;
|
|
} & JSX.InputHTMLAttributes<HTMLInputElement>
|
|
> = (p) => {
|
|
p = mergeProps(
|
|
{
|
|
placeholder: p.label,
|
|
labelMinWidth: "95px",
|
|
size: "sm",
|
|
vertical: false,
|
|
},
|
|
p
|
|
);
|
|
const [props, rest] = splitProps(p, [
|
|
"name",
|
|
"size",
|
|
"class",
|
|
"label",
|
|
"placeholder",
|
|
"suffix",
|
|
"vertical",
|
|
"labelMinWidth",
|
|
]);
|
|
const sizes: Record<string, string> = {
|
|
xs: "input-xs",
|
|
sm: "input-sm",
|
|
lg: "input-lg",
|
|
};
|
|
|
|
const [validate, vState] = validateInput({ value: () => rest.value });
|
|
|
|
return (
|
|
<div class="shrink form-control">
|
|
<label
|
|
classList={{
|
|
"input-group": true,
|
|
"input-group-vertical": props.vertical,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
"min-width":
|
|
!props.vertical && props.labelMinWidth
|
|
? props.labelMinWidth
|
|
: undefined,
|
|
}}
|
|
classList={{
|
|
"gap-1": true,
|
|
"h-8": props.vertical,
|
|
"font-bold": rest.required,
|
|
"bg-slate-200/70": vState().valid,
|
|
"bg-red-100": !vState().valid,
|
|
}}
|
|
>
|
|
{props.label}
|
|
<Show when={rest.required}>
|
|
<AsteriskIcon class="text-primary text-[0.5rem]" />
|
|
</Show>
|
|
</span>
|
|
<input
|
|
name={props.name || props.label}
|
|
use:validate
|
|
classList={{
|
|
"peer flex-1 input invalid:input-error input-bordered": true,
|
|
"w-0": !props.vertical,
|
|
[(props.size && sizes[props.size]) || ""]: true,
|
|
[props.class || ""]: true,
|
|
}}
|
|
type="text"
|
|
placeholder={props.placeholder}
|
|
{...rest}
|
|
/>
|
|
<Show when={props.suffix}>
|
|
<span
|
|
classList={{
|
|
"bg-slate-200/70": vState().valid,
|
|
"bg-red-100": !vState().valid,
|
|
}}
|
|
>
|
|
{props.suffix}
|
|
</span>
|
|
</Show>
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const Checkbox: FlowComponent<
|
|
JSX.InputHTMLAttributes<HTMLInputElement>
|
|
> = (p) => {
|
|
const [props, rest] = splitProps(p, ["children"]);
|
|
|
|
return (
|
|
<div class="form-control">
|
|
<label class="label py-0 cursor-pointer">
|
|
<span class="label-text">{props.children}</span>
|
|
<input type="checkbox" class="checkbox" {...rest} />
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const TextArea: Component<
|
|
{
|
|
label: string;
|
|
labelSuffixJsx?: JSX.Element;
|
|
value?: string;
|
|
placeholder?: string;
|
|
} & JSX.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
> = (p) => {
|
|
p = mergeProps({ rows: 3, placeholder: p.label }, p);
|
|
const [props, rest] = splitProps(p, ["label", "labelSuffixJsx", "value"]);
|
|
const [autosizeEnabled, setAutosize] = createSignal(false);
|
|
let textareaEl: HTMLTextAreaElement = undefined!;
|
|
createEffect(function () {
|
|
if (autosizeEnabled()) {
|
|
autosize(textareaEl);
|
|
} else {
|
|
autosize.destroy(textareaEl);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div class="form-control relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setAutosize(!autosizeEnabled())}
|
|
class="absolute right-3 top-2 hover:text-accent transition-all hover:scale-110"
|
|
>
|
|
<Show when={!autosizeEnabled()} fallback={<MinimizeIcon />}>
|
|
<MaximizeIcon />
|
|
</Show>
|
|
</button>
|
|
<label class="input-group input-group-vertical">
|
|
<span class="h-8 bg-slate-200/70 flex gap-2 justify-between pr-14">
|
|
{props.label}
|
|
{props.labelSuffixJsx}
|
|
</span>
|
|
<textarea
|
|
autocomplete="off"
|
|
ref={textareaEl}
|
|
classList={{
|
|
"textarea h-auto py-2 textarea-bordered leading-normal": true,
|
|
"min-h-[150px]": autosizeEnabled(),
|
|
}}
|
|
{...rest}
|
|
>
|
|
{props.value || ""}
|
|
</textarea>
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const UnixDateInput: Component<
|
|
{
|
|
required?: boolean;
|
|
label: string;
|
|
value?: number;
|
|
onInput: (v: number) => void;
|
|
} & Parameters<typeof TextInput>[0]
|
|
> = (p) => {
|
|
const [props, rest] = splitProps(p, [
|
|
"required",
|
|
"label",
|
|
"value",
|
|
"onInput",
|
|
]);
|
|
return (
|
|
<TextInput
|
|
required={props.required}
|
|
type="date"
|
|
label={props.label}
|
|
max="9999-12-31"
|
|
value={
|
|
props.value != null
|
|
? formatISO9075(fromUnixTime(props.value), {
|
|
representation: "date",
|
|
})
|
|
: (null as any)
|
|
}
|
|
onInput={(evt) => {
|
|
if (!evt.currentTarget.valueAsDate) {
|
|
return;
|
|
}
|
|
|
|
if (evt.currentTarget.valueAsDate.getFullYear() > 9999) {
|
|
return;
|
|
}
|
|
|
|
props.onInput(getUnixTime(evt.currentTarget.valueAsDate));
|
|
}}
|
|
{...rest}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const NumberInput: Component<
|
|
{ value?: number; onInput: (v: number | undefined) => void } & Omit<
|
|
Parameters<typeof TextInput>[0],
|
|
"onInput"
|
|
>
|
|
> = (p) => {
|
|
let el: HTMLInputElement = undefined!;
|
|
const [props, rest] = splitProps(p, ["value", "onInput"]);
|
|
const value = createNativeInputValue(
|
|
() => el,
|
|
() => props.value
|
|
);
|
|
|
|
return (
|
|
<TextInput
|
|
ref={el}
|
|
value={value()}
|
|
maxLength={9}
|
|
onInput={createOptionalNumberInputHandler(props.onInput)}
|
|
{...rest}
|
|
/>
|
|
);
|
|
};
|