200 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|