210 lines
9.5 KiB
JavaScript
210 lines
9.5 KiB
JavaScript
// Rôle : rend l'éditeur des caractéristiques d'un équipement avec catégories réordonnables.
|
|
import { useState } from "react";
|
|
import { Icon } from "../../../components/Icon.jsx";
|
|
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
|
import {
|
|
TRAIT_ICONS,
|
|
createLocalId,
|
|
formatValue,
|
|
getCharacteristicCategories,
|
|
setItemCategory
|
|
} from "./equipmentPlannerOptions.js";
|
|
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
|
|
|
|
export function CharacteristicsEditor({ equipment, textContent, editing, onUpdate }) {
|
|
const [draft, setDraft] = useState({ icon: "sword", category: "", name: "", value: "" });
|
|
const categories = getCharacteristicCategories(equipment);
|
|
const {
|
|
itemReorder,
|
|
groupReorder,
|
|
getItemProps,
|
|
getGroupProps,
|
|
isItemDragging,
|
|
isItemDropTarget,
|
|
isGroupDragging,
|
|
isGroupDropTarget,
|
|
getDropPlacement
|
|
} = useGroupedReorder({
|
|
namespace: `equipment-characteristics-${equipment.id}`,
|
|
items: equipment.characteristics,
|
|
getItemId: (trait) => trait.id,
|
|
getItemGroup: (trait) => trait.category,
|
|
getEffectiveGroup: (trait) => trait.category,
|
|
reorderFeatures: {
|
|
item: { groupChange: true },
|
|
group: { reorder: true, itemDrop: true }
|
|
},
|
|
onItemMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
|
operation,
|
|
itemsKey: "characteristics",
|
|
groupOrderKey: "categoryOrder",
|
|
collapsedGroupsKey: "collapsedCategories",
|
|
getItemGroup: (trait) => trait.category,
|
|
setItemGroup: setItemCategory
|
|
})),
|
|
onGroupMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
|
operation,
|
|
itemsKey: "characteristics",
|
|
groupOrderKey: "categoryOrder",
|
|
collapsedGroupsKey: "collapsedCategories",
|
|
getItemGroup: (trait) => trait.category,
|
|
setItemGroup: setItemCategory
|
|
}))
|
|
});
|
|
const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, getDropPlacement };
|
|
|
|
function addTrait(event) {
|
|
event.preventDefault();
|
|
const cleanName = draft.name.trim();
|
|
if (!cleanName) return;
|
|
onUpdate((current) => ({
|
|
...current,
|
|
characteristics: [...current.characteristics, { id: createLocalId("trait"), icon: draft.icon, category: draft.category.trim(), name: cleanName, value: draft.value.trim() }],
|
|
categoryOrder: draft.category.trim() && !current.categoryOrder.includes(draft.category.trim()) ? [...current.categoryOrder, draft.category.trim()] : current.categoryOrder
|
|
}));
|
|
setDraft({ icon: draft.icon, category: draft.category, name: "", value: "" });
|
|
}
|
|
|
|
function updateTrait(traitId, updater) {
|
|
onUpdate((current) => ({ ...current, characteristics: current.characteristics.map((trait) => trait.id === traitId ? updater(trait) : trait).filter((trait) => trait.name) }));
|
|
}
|
|
|
|
return (
|
|
<section className="equipment-planner-panel">
|
|
<h4>{textContent.characteristicsTitle || "Caractéristiques"}</h4>
|
|
{editing && (
|
|
<form className="inline-form equipment-planner-trait-form" onSubmit={addTrait}>
|
|
<IconChoiceDropdown
|
|
label={textContent.traitIconLabel || "Icône"}
|
|
value={draft.icon}
|
|
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
|
onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
|
|
/>
|
|
<input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
|
|
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
|
|
<input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
|
|
<button>{textContent.addTraitButton || "Ajouter"}</button>
|
|
</form>
|
|
)}
|
|
<TraitCategoryList
|
|
traits={equipment.characteristics}
|
|
categories={categories}
|
|
collapsedCategories={equipment.collapsedCategories}
|
|
reorder={reorder}
|
|
textContent={textContent}
|
|
onToggleCategory={(category) => onUpdate((current) => ({
|
|
...current,
|
|
collapsedCategories: current.collapsedCategories.includes(category)
|
|
? current.collapsedCategories.filter((item) => item !== category)
|
|
: [...current.collapsedCategories, category]
|
|
}))}
|
|
onUpdateTrait={updateTrait}
|
|
editing={editing}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function TraitCategoryList({ traits, categories, collapsedCategories, reorder, textContent, onToggleCategory, onUpdateTrait, editing }) {
|
|
const ungrouped = traits.filter((trait) => !trait.category);
|
|
|
|
function renderTrait(trait) {
|
|
const traitClassName = [
|
|
"equipment-planner-trait",
|
|
reorder.isItemDragging(trait.id) ? "is-dragging" : "",
|
|
reorder.isItemDropTarget(trait.id) ? "is-drop-target" : "",
|
|
reorder.getDropPlacement("item", trait.id) === "after" ? "drop-after" : ""
|
|
].filter(Boolean).join(" ");
|
|
return (
|
|
<EquipmentTraitRow
|
|
key={trait.id}
|
|
trait={trait}
|
|
className={traitClassName}
|
|
itemProps={reorder.getItemProps({ itemId: trait.id, groupId: trait.category })}
|
|
editing={editing}
|
|
textContent={textContent}
|
|
onStartDrag={(event) => reorder.itemReorder.startDrag(event, trait.id)}
|
|
onUpdateTrait={onUpdateTrait}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="equipment-planner-trait-list">
|
|
{ungrouped.length ? <ul>{ungrouped.map(renderTrait)}</ul> : null}
|
|
{categories.map((category) => {
|
|
const categoryTraits = traits.filter((trait) => trait.category === category);
|
|
const collapsed = collapsedCategories.includes(category);
|
|
const className = [
|
|
"equipment-planner-trait-category",
|
|
collapsed ? "is-collapsed" : "",
|
|
reorder.isGroupDragging(category) ? "is-dragging" : "",
|
|
reorder.isGroupDropTarget(category) ? "is-drop-target" : ""
|
|
].filter(Boolean).join(" ");
|
|
return (
|
|
<section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
|
|
<div className="equipment-planner-trait-category-header">
|
|
<button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
|
|
<Icon name="drag" />
|
|
</button>
|
|
<strong>{category}</strong>
|
|
<span>{categoryTraits.length}</span>
|
|
<button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
|
|
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
|
|
</button>
|
|
</div>
|
|
{!collapsed && <ul>{categoryTraits.map(renderTrait)}</ul>}
|
|
</section>
|
|
);
|
|
})}
|
|
{!traits.length && <p className="equipment-planner-muted">{textContent.emptyCharacteristics || "Aucune caractéristique."}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EquipmentTraitRow({ trait, className, itemProps, editing, textContent, onStartDrag, onUpdateTrait }) {
|
|
const categoryEdit = useInlineEdit({
|
|
value: trait.category || "",
|
|
onCommit: (category) => onUpdateTrait(trait.id, (current) => ({ ...current, category }))
|
|
});
|
|
const nameEdit = useInlineEdit({
|
|
value: trait.name,
|
|
transform: (draft) => String(draft || "").trim() || trait.name,
|
|
onCommit: (name) => onUpdateTrait(trait.id, (current) => ({ ...current, name }))
|
|
});
|
|
const valueEdit = useInlineEdit({
|
|
value: formatValue(trait.value),
|
|
onCommit: (value) => onUpdateTrait(trait.id, (current) => ({ ...current, value }))
|
|
});
|
|
|
|
return (
|
|
<li className={className} {...itemProps}>
|
|
{editing ? (
|
|
<>
|
|
<button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
|
|
<Icon name="drag" />
|
|
</button>
|
|
<IconChoiceDropdown
|
|
label={textContent.traitIconLabel || "Icône"}
|
|
value={trait.icon || "sword"}
|
|
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
|
onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
|
|
/>
|
|
<input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
|
|
<input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
|
|
<input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
|
|
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
|
|
<Icon name="trash" />
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className="equipment-planner-trait-readonly">
|
|
<Icon name={trait.icon || "sword"} />
|
|
<span>{trait.name}</span>
|
|
{trait.value !== "" && <b>{formatValue(trait.value)}</b>}
|
|
</div>
|
|
)}
|
|
</li>
|
|
);
|
|
}
|