2023-01-07 16:01:53 +01:00
|
|
|
/* @refresh reload */
|
2023-04-08 19:24:10 +02:00
|
|
|
import { type Component, createSignal, JSX, onMount, Setter, Show } from "solid-js";
|
2023-01-07 16:01:53 +01:00
|
|
|
import Row from "./row";
|
2023-04-08 19:24:10 +02:00
|
|
|
import { Icon } from "solid-heroicons";
|
|
|
|
import { magnifyingGlass, xMark } from "solid-heroicons/solid";
|
|
|
|
import { getElementById } from "../utils/dom";
|
2023-01-07 16:01:53 +01:00
|
|
|
|
|
|
|
function setupEventListener(id: string, setIsHover: Setter<boolean>): () => void {
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
|
|
function hover(hover: boolean): void {
|
|
|
|
if (isMounted) {
|
|
|
|
setIsHover(hover);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-08 19:24:10 +02:00
|
|
|
const el = getElementById(id);
|
2023-01-07 16:01:53 +01:00
|
|
|
el?.addEventListener("pointerenter", () => hover(true));
|
|
|
|
el?.addEventListener("pointerleave", () => hover(false));
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
el?.removeEventListener("pointerenter", () => hover(true));
|
|
|
|
el?.removeEventListener("pointerleave", () => hover(false));
|
|
|
|
isMounted = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets isText to 'true' or 'false' using the setIsText function.
|
|
|
|
* if the value of the input element is not empty and it's different from the current value
|
|
|
|
*/
|
|
|
|
function setSetIsText(id: string | undefined, isText: boolean, setIsText: Setter<boolean>): void {
|
|
|
|
if (id) {
|
2023-04-08 19:24:10 +02:00
|
|
|
const el = getElementById<HTMLInputElement | HTMLTextAreaElement>(id);
|
2023-01-07 16:01:53 +01:00
|
|
|
if (el && el.value !== "" !== isText) {
|
|
|
|
setIsText(el.value !== "");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-24 20:44:32 +02:00
|
|
|
interface Input<T extends HTMLElement> extends InputProps<T> {
|
2023-01-07 16:01:53 +01:00
|
|
|
leading?: JSX.Element,
|
|
|
|
trailing?: JSX.Element,
|
2023-01-10 22:50:29 +01:00
|
|
|
onChange?: () => void,
|
2023-02-22 22:54:53 +01:00
|
|
|
inputClass?: string,
|
2023-01-07 16:01:53 +01:00
|
|
|
}
|
|
|
|
|
2023-04-08 19:24:10 +02:00
|
|
|
export const Input: Component<Input<HTMLInputElement>> = ( // TODO remove leading and trailing from component
|
2023-01-07 16:01:53 +01:00
|
|
|
{
|
|
|
|
className,
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
type = "text",
|
|
|
|
title,
|
|
|
|
placeholder,
|
|
|
|
required = false,
|
|
|
|
onChange,
|
|
|
|
leading,
|
2023-02-22 22:54:53 +01:00
|
|
|
trailing,
|
2023-07-24 20:44:32 +02:00
|
|
|
inputClass,
|
|
|
|
ref
|
2023-01-07 16:01:53 +01:00
|
|
|
}): JSX.Element => {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is 'true' if the input element is in focus
|
|
|
|
*/
|
|
|
|
const [isFocused, setIsFocused] = createSignal(false);
|
|
|
|
/**
|
|
|
|
* Is 'true' if the user is hovering over the input element
|
|
|
|
*/
|
|
|
|
const [isHover, setIsHover] = createSignal(false);
|
|
|
|
/**
|
|
|
|
* Is 'true' if the input element contains any characters
|
|
|
|
*/
|
|
|
|
const [isText, setIsText] = createSignal(false);
|
|
|
|
|
2023-04-08 19:24:10 +02:00
|
|
|
onMount(() => {
|
2023-01-07 16:01:53 +01:00
|
|
|
if (id && title) {
|
|
|
|
setupEventListener(id, setIsHover);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
2023-02-22 22:54:53 +01:00
|
|
|
<Row className={ `relative ${ className }` }>
|
2023-01-07 16:01:53 +01:00
|
|
|
{ leading }
|
2023-01-10 22:50:29 +01:00
|
|
|
<HoverTitle title={ title } isActive={ isFocused() || isHover() || isText() } htmlFor={ id } />
|
2023-01-07 16:01:53 +01:00
|
|
|
<input
|
|
|
|
class={ `bg-default-bg focus:border-cyan-500 outline-none border-2 border-gray-500
|
2023-02-22 22:54:53 +01:00
|
|
|
hover:border-t-cyan-400 ${ inputClass }` }
|
2023-01-07 16:01:53 +01:00
|
|
|
id={ id }
|
2023-07-24 20:44:32 +02:00
|
|
|
ref={ ref }
|
2023-01-07 16:01:53 +01:00
|
|
|
onFocus={ () => setIsFocused(true) }
|
|
|
|
onBlur={ () => setIsFocused(false) }
|
|
|
|
name={ name ?? undefined }
|
|
|
|
type={ type }
|
|
|
|
placeholder={ placeholder ?? undefined }
|
|
|
|
required={ required }
|
2023-01-10 22:50:29 +01:00
|
|
|
onInput={ () => {
|
|
|
|
setSetIsText(id, isText(), setIsText);
|
|
|
|
if (onChange) {
|
|
|
|
onChange();
|
|
|
|
}
|
|
|
|
} } />
|
2023-01-07 16:01:53 +01:00
|
|
|
{ trailing }
|
|
|
|
</Row>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-08 19:24:10 +02:00
|
|
|
const HoverTitle: Component<{ title?: string, isActive?: boolean, htmlFor?: string }> = (
|
2023-01-07 16:01:53 +01:00
|
|
|
{
|
|
|
|
title,
|
|
|
|
isActive = false,
|
|
|
|
htmlFor
|
2023-04-08 19:24:10 +02:00
|
|
|
}) => (
|
|
|
|
<label class={ `absolute pointer-events-none
|
2023-01-07 16:01:53 +01:00
|
|
|
${ isActive ? "-top-2 left-3 default-bg text-sm" : "left-2 top-1" }
|
|
|
|
transition-all duration-150 text-gray-600 dark:text-gray-400` }
|
2023-04-08 19:24:10 +02:00
|
|
|
for={ htmlFor }>
|
|
|
|
<div class={ "z-50 relative" }>{ title }</div>
|
|
|
|
<div class={ "w-full h-2 default-bg absolute bottom-1/3 z-10" } />
|
|
|
|
</label>
|
|
|
|
);
|
|
|
|
|
|
|
|
interface SearchProps extends InputProps<HTMLInputElement> {
|
|
|
|
typingDefault?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export const Search: Component<SearchProps> = (
|
|
|
|
{
|
|
|
|
typingDefault = false,
|
|
|
|
id = "search",
|
2023-07-24 20:44:32 +02:00
|
|
|
className,
|
|
|
|
ref
|
2023-04-08 19:24:10 +02:00
|
|
|
}) => {
|
|
|
|
|
|
|
|
const [typing, setTyping] = createSignal(typingDefault);
|
|
|
|
|
|
|
|
function getInputElement() {
|
|
|
|
return getElementById<HTMLInputElement>(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
function clearSearch(): void {
|
|
|
|
const el = getInputElement();
|
|
|
|
if (el) {
|
|
|
|
el.value = "";
|
|
|
|
setTyping(false);
|
|
|
|
history.replaceState(null, "", location.pathname);
|
|
|
|
el.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onChange(): void {
|
|
|
|
const el = getInputElement();
|
|
|
|
if (el && (el.value !== "") !== typing()) {
|
|
|
|
setTyping(el.value !== "");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Input inputClass={ `rounded-xl pl-7 h-10 w-full pr-8` } className={ `w-full ${ className }` }
|
|
|
|
id={ id }
|
2023-07-24 20:44:32 +02:00
|
|
|
ref={ ref }
|
2023-04-08 19:24:10 +02:00
|
|
|
placeholder={ "¬A & B -> C" }
|
|
|
|
type={ "text" }
|
|
|
|
onChange={ onChange }
|
|
|
|
leading={ <Icon path={ magnifyingGlass } aria-label={ "Magnifying glass" }
|
|
|
|
class={ "pl-2 absolute" } /> }
|
|
|
|
trailing={ <Show when={ typing() } keyed>
|
|
|
|
<button class={ "absolute right-2" }
|
|
|
|
title={ "Clear" }
|
|
|
|
type={ "reset" }
|
|
|
|
onClick={ clearSearch }>
|
|
|
|
<Icon path={ xMark } aria-label={ "The letter X" } />
|
|
|
|
</button>
|
|
|
|
</Show> }
|
|
|
|
/>
|
2023-01-07 16:01:53 +01:00
|
|
|
);
|
|
|
|
}
|