Skip to content

Instantly share code, notes, and snippets.

@nestarz
Last active April 25, 2023 12:30
Show Gist options
  • Save nestarz/00d675775afd040a4c768c1c45bc04ee to your computer and use it in GitHub Desktop.
Save nestarz/00d675775afd040a4c768c1c45bc04ee to your computer and use it in GitHub Desktop.
import { h } from "preact";
import { createContext, ComponentChildren } from "preact";
import { useContext, useRef } from "preact/hooks";
import { Signal, signal, useSignalEffect } from "@preact/signals";
interface ComboboxProps {
children: ComponentChildren;
value: Signal<string>;
onChange: (value: string | null) => void;
nullable?: boolean;
freeSolo?: boolean;
}
interface ComboboxInputProps {
onChange: (event: Event) => void;
displayValue?: (value: string | null) => string;
}
interface ComboboxOptionsProps {
children: ComponentChildren;
}
interface ComboboxOptionProps {
children: ComponentChildren;
value: string;
}
const ComboboxContext = createContext<{
value: undefined | Signal<string>;
onChange: (value: string | null) => void;
nullable?: boolean;
freeSolo?: boolean;
}>({
value: signal(""),
nullable: false,
freeSolo: false,
onChange: () => {},
});
export const Combobox = ({
value,
onChange,
nullable,
freeSolo,
...props
}: ComboboxProps) => {
return (
<ComboboxContext.Provider value={{ value, nullable, freeSolo, onChange }}>
<div role="combobox" {...props} />
</ComboboxContext.Provider>
);
};
export const ComboboxInput = ({
onChange,
displayValue,
...props
}: ComboboxInputProps) => {
const ref = useRef<HTMLInputElement>(null);
const { value, nullable, ...c } = useContext(ComboboxContext);
const safeDisplayValue = (value) =>
(displayValue?.(value.value) ?? value.value) || "";
const onBlur = (e) => {
if (value?.valueOf) e.target.value = safeDisplayValue(value);
};
const handleChange = (e) => {
const currValue = e.target.value;
onChange(e);
if ((c.freeSolo || nullable) && currValue === "") c.onChange(null);
else if (c.freeSolo) c.onChange(currValue);
};
useSignalEffect(() => {
if (value?.valueOf)
if (ref.current) ref.current.value = safeDisplayValue(value);
});
return (
<input
ref={ref}
type="text"
onChange={handleChange}
onBlur={onBlur}
autoComplete="off"
{...props}
/>
);
};
export const ComboboxOptions = ({ ...props }: ComboboxOptionsProps) => {
return <ul role="listbox" tabIndex="0" {...props} />;
};
export const ComboboxOption = ({
value,
component: C = "button",
...props
}: ComboboxOptionProps) => {
const { onChange } = useContext(ComboboxContext);
const handleChange = (e: Event) => {
onChange(value);
e.target?.blur();
document.activeElement.blur();
};
return <C type="button" role="option" tabIndex="0" onClick={handleChange} {...props} />;
};
import { h, hydrate } from "preact";
export { h, hydrate };
export const slugify = (text) =>
text
?.toString()
?.normalize("NFD")
?.replace(/[\u0300-\u036f]/g, "")
?.toLowerCase()
?.trim()
?.replace(/\s+/g, "-")
?.replace(/[^\w-]+/g, "")
?.replace(/--+/g, "-");
export default ({
className,
children,
component: C = "div",
InputProps,
...props
}) => {
const on = (evt) => evt.preventDefault();
return (
<C
{...props}
className={className}
onDragOver={on}
onDragEnter={on}
onDrop={(e) => {
e.preventDefault();
const fileInput = e.target?.childNodes?.[0];
if (fileInput) fileInput.files = e.dataTransfer.files;
}}
>
{children}
<input type="file" {...InputProps} />
</C>
);
};
export const Obfuscate = ({ children, ...props }) => (
<span {...props} dir="rtl" style={{ "unicode-bidi": "bidi-override" }}>
{String(children)
.split("")
.reverse()
.flatMap((char: string) => [
<em style={{ display: "none" }}>{Math.random().toString(36).substring(7)}</em>,
<span>{char}</span>,
])}
</span>
);
export const atobClick = (node, str) => {
node.addEventListener("click", (ev: Event) => {
ev.preventDefault();
window.location.href = atob(str);
});
};
const slugify = (text) =>
text
?.toString()
?.normalize("NFD")
?.replace(/[\u0300-\u036f]/g, "")
?.toLowerCase()
?.trim()
?.replace(/\s+/g, "-")
?.replace(/[^\w-]+/g, "")
?.replace(/--+/g, "-");
const getFilesFromFormData = (formData: FormData): File[] => {
return [...formData.entries()]
.map(([, v]) => v)
.filter((file) => file instanceof File && file.name);
};
const getSafeName = (string: string) => {
const defaultP = [null, string, ""];
const [, raw, extension] = /^(.+)(\.[^.]+)$/.exec(string) ?? defaultP;
const basename = slugify(raw);
const name = `${basename}${extension}`;
return { name, basename, extension };
};
interface UseUpload {
getPrefix: () => string | undefined;
onProgress?: (p: number, all: number) => unknown;
onEnded?: () => unknown;
uploadFn: (key: string, file: File) => Promise<unknown>;
}
export default ({ getPrefix, onProgress, uploadFn, onEnded }: UseUpload) => {
const handleFileSubmit = (fn) => (e) => {
e.preventDefault();
return fn(getFilesFromFormData(new FormData(e.target)));
};
const upload = async (files: File[]) => {
let i = 0;
onProgress?.(i, files.length);
const res = [];
for (const file of files) {
i += 1;
const relpath = file.webkitRelativePath?.slice(0, -file.name.length - 1);
const path = relpath?.split("/").map((v) => slugify(v)) ?? [];
const { name } = getSafeName(file.name);
const key = [getPrefix?.() ?? "", ...path, name]
.filter((v) => v)
.join("/")
.replaceAll("//", "/");
res.push(await uploadFn(key, file));
onProgress?.(i, files.length);
}
onEnded?.(res);
return res;
};
return { handleFileSubmit, upload };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment