Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/input/InputCheckboxCards.tsx
T

200 lines
6.2 KiB
TypeScript

import clsx from "clsx";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import HintTooltip from "../tooltips/HintTooltip";
import { Colors } from "~/application/enums/shared/Colors";
import { CheckboxCard } from "~/ui/components/CheckboxCard";
type CheckboxCardValue = string | number;
function compareCheckboxCardValue(a: CheckboxCardValue, b: CheckboxCardValue) {
const aKey = `${typeof a}:${String(a)}`;
const bKey = `${typeof b}:${String(b)}`;
return aKey.localeCompare(bKey);
}
function hasSameCheckboxCardValues(a: CheckboxCardValue[] | undefined, b: CheckboxCardValue[] | undefined) {
if (a === b) {
return true;
}
if (!a || !b || a.length !== b.length) {
return false;
}
const sortedA = [...a].sort(compareCheckboxCardValue);
const sortedB = [...b].sort(compareCheckboxCardValue);
return sortedA.every((value, index) => Object.is(value, sortedB[index]));
}
export default function InputCheckboxCards({
title,
name,
options,
value,
onChange,
selectAndClearAll = false,
disabled,
required,
help,
hint,
columns,
display,
}: {
title?: string;
name?: string;
options: { name: string; value: CheckboxCardValue; color?: Colors; disabled?: boolean }[];
value?: CheckboxCardValue[];
onChange?: (value: CheckboxCardValue[]) => void;
selectAndClearAll?: boolean;
disabled?: boolean;
required?: boolean;
help?: string;
hint?: React.ReactNode;
columns?: number;
display?: "name" | "value" | "nameAndValue";
}) {
const { t } = useTranslation();
const [selected, setSelected] = useState<CheckboxCardValue[]>(() => (value ? [...value] : []));
const [displayType, setDisplayType] = useState<"name" | "value" | "nameAndValue">("name");
useEffect(() => {
if (display) {
setDisplayType(display);
} else {
const hasNames = options.some((item) => item.name);
if (!hasNames) {
setDisplayType("value");
} else {
const namesAreDifferent = options.some((item) => item.name !== item.value.toString());
if (namesAreDifferent) {
setDisplayType("nameAndValue");
} else {
setDisplayType("name");
}
}
}
}, [display, options]);
useEffect(() => {
if (value && !hasSameCheckboxCardValues(selected, value)) {
setSelected([...value]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useEffect(() => {
if (onChange && !hasSameCheckboxCardValues(selected, value)) {
onChange(selected);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected]);
function isChecked(value: CheckboxCardValue) {
return selected.includes(value);
}
return (
<div>
{/* selected: {JSON.stringify(selected)} */}
<div className="flex justify-between space-x-2">
<div>
{title && (
<label htmlFor={name} className="mb-1 flex justify-between space-x-2 text-xs font-medium">
<div className="flex items-center space-x-1">
<div className="truncate">
{title}
{required && <span className="ml-1 text-red-600">*</span>}
</div>
{help && <HintTooltip text={help} />}
</div>
{hint}
</label>
)}
</div>
{selectAndClearAll && (
<div className="flex items-center space-x-2">
{selected.length !== options.length && (
<button
type="button"
className="text-muted-foreground text-xs font-medium"
onClick={() => {
setSelected(options.map((item) => item.value.toString()));
}}
>
{t("shared.selectAll")}
</button>
)}
{selected.length > 0 && (
<button
type="button"
className="text-muted-foreground text-xs font-medium"
onClick={() => {
setSelected([]);
}}
>
{t("shared.clear")}
</button>
)}
</div>
)}
</div>
<ul
className={clsx(
"grid w-full grid-cols-1 gap-3",
columns === undefined && "md:grid-cols-3",
columns === 1 && "md:grid-cols-1",
columns === 2 && "md:grid-cols-2",
columns === 3 && "md:grid-cols-3",
columns === 4 && "md:grid-cols-4",
columns === 5 && "md:grid-cols-5",
columns === 6 && "md:grid-cols-6",
columns === 7 && "md:grid-cols-7",
columns === 8 && "md:grid-cols-8",
columns === 9 && "md:grid-cols-9",
columns === 10 && "md:grid-cols-10",
columns === 11 && "md:grid-cols-11",
columns === 12 && "md:grid-cols-12"
)}
>
{options.map((item) => {
const itemDisabled = disabled || item.disabled;
return (
<li key={item.value}>
<CheckboxCard
id={`${item.name}-${item.value}`}
name={name}
value={String(item.value)}
aria-label={item.name || String(item.value)}
checked={isChecked(item.value)}
disabled={itemDisabled}
onCheckedChange={() => {
if (selected.includes(item.value)) {
setSelected(selected.filter((v) => v !== item.value));
} else {
setSelected([...selected, item.value]);
}
}}
className="w-full"
>
<div className="block w-full min-w-0">
<div className="flex w-full min-w-0 justify-between truncate">
<div className="min-w-0 truncate">
<div className={clsx("block w-full truncate text-sm", !itemDisabled ? "font-medium" : "")}>{item.name}</div>
</div>
</div>
{["nameAndValue", "value"].includes(displayType) && <div className="w-full truncate text-sm">{item.value}</div>}
</div>
</CheckboxCard>
</li>
);
})}
</ul>
</div>
);
}