607 lines
25 KiB
JavaScript
607 lines
25 KiB
JavaScript
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
||
import { useMemo, useState } from "react";
|
||
import { Icon } from "../../../components/Icon.jsx";
|
||
import {
|
||
applyGroupedReorderOperation,
|
||
getGroupedEntries,
|
||
useGroupedReorder
|
||
} from "../../../hooks/useGroupedReorder.js";
|
||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||
import { ComboInputToken, ComboSequence, InlineTextInput } from "./CombosParts.jsx";
|
||
import {
|
||
DEVICE_OPTIONS,
|
||
createCombo,
|
||
createInput,
|
||
getComboCategory,
|
||
getHoldSeconds,
|
||
getPaletteColumns,
|
||
getPaletteGroups,
|
||
normalizeCustomKey,
|
||
parseHoldSeconds,
|
||
setComboItemCategory
|
||
} from "./combosUtils.js";
|
||
|
||
export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
|
||
const textContent = context.moduleText?.combos || {};
|
||
const [comboCategory, setComboCategory] = useState("");
|
||
const [comboName, setComboName] = useState("");
|
||
const [draftInputs, setDraftInputs] = useState([[]]);
|
||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||
const [selectedInput, setSelectedInput] = useState(null);
|
||
const [simultaneousMode, setSimultaneousMode] = useState(false);
|
||
const [keyboardLayout, setKeyboardLayout] = useState("azerty");
|
||
const [customKey, setCustomKey] = useState("");
|
||
const [editingComboId, setEditingComboId] = useState("");
|
||
const paletteGroups = useMemo(() => getPaletteGroups(data.device, keyboardLayout), [data.device, keyboardLayout]);
|
||
const paletteColumns = useMemo(() => getPaletteColumns(paletteGroups), [paletteGroups]);
|
||
const cleanDraftInputs = draftInputs.map((step) => step.filter(Boolean)).filter((step) => step.length);
|
||
const selectedDraftInput = Number.isInteger(selectedInput?.stepIndex) && Number.isInteger(selectedInput?.inputIndex)
|
||
? draftInputs[selectedInput.stepIndex]?.[selectedInput.inputIndex] || null
|
||
: null;
|
||
const entries = getGroupedEntries(data.combos, {
|
||
groupOrder: data.categoryOrder,
|
||
getItemGroup: getComboCategory,
|
||
groupIdKey: "category",
|
||
groupItemKey: "combos"
|
||
});
|
||
const {
|
||
itemReorder,
|
||
groupReorder,
|
||
getItemProps,
|
||
getGroupProps,
|
||
getGroupBoundaryProps,
|
||
isItemDragging,
|
||
isItemDropTarget,
|
||
isGroupDragging,
|
||
isGroupDropTarget,
|
||
isBoundaryDropTarget,
|
||
getDropPlacement,
|
||
shouldShowGroupBoundaries
|
||
} = useGroupedReorder({
|
||
namespace: `combos-${moduleId}`,
|
||
items: data.combos,
|
||
getItemId: (combo) => combo.id,
|
||
getItemGroup: getComboCategory,
|
||
reorderFeatures: {
|
||
item: {
|
||
groupChange: true,
|
||
boundaryDrop: true,
|
||
ungroupedDrop: true
|
||
},
|
||
group: {
|
||
reorder: true,
|
||
boundaryDrop: true,
|
||
ungroupedDrop: true,
|
||
itemDrop: true
|
||
}
|
||
},
|
||
onItemMove: applyComboReorder,
|
||
onGroupMove: applyComboReorder,
|
||
hierarchy: { enabled: false, stickyParents: false }
|
||
});
|
||
|
||
function save(nextData) {
|
||
context.setModuleData(toolboxId, moduleId, nextData, "combos");
|
||
}
|
||
|
||
function updateDevice(device) {
|
||
const stampComboDevice = (combo) => combo.device ? combo : { ...combo, device: data.device };
|
||
save({
|
||
...data,
|
||
device,
|
||
combos: data.combos.map(stampComboDevice)
|
||
});
|
||
}
|
||
|
||
function applyComboReorder(operation) {
|
||
save(applyGroupedReorderOperation(data, {
|
||
operation,
|
||
itemsKey: "combos",
|
||
groupOrderKey: "categoryOrder",
|
||
collapsedGroupsKey: "collapsedCategories",
|
||
getItemGroup: getComboCategory,
|
||
setItemGroup: setComboItemCategory
|
||
}));
|
||
}
|
||
|
||
function addInputToStep(kind, value, stepIndex) {
|
||
const input = createInput(kind, value);
|
||
setDraftInputs((steps) => {
|
||
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
|
||
const targetIndex = Math.min(Math.max(0, stepIndex), nextSteps.length - 1);
|
||
nextSteps[targetIndex] = [...nextSteps[targetIndex], input];
|
||
setSelectedInput({ stepIndex: targetIndex, inputIndex: nextSteps[targetIndex].length - 1 });
|
||
return nextSteps;
|
||
});
|
||
}
|
||
|
||
function addDraftInput(kind, value) {
|
||
const input = createInput(kind, value);
|
||
if (simultaneousMode) {
|
||
addInputToStep(kind, value, activeStepIndex);
|
||
return;
|
||
}
|
||
|
||
setDraftInputs((steps) => {
|
||
const filledSteps = steps.filter((step) => step.length);
|
||
const nextSteps = [...filledSteps, [input]];
|
||
setActiveStepIndex(nextSteps.length - 1);
|
||
setSelectedInput({ stepIndex: nextSteps.length - 1, inputIndex: 0 });
|
||
return nextSteps;
|
||
});
|
||
}
|
||
|
||
function dragPaletteInput(event, kind, value) {
|
||
event.dataTransfer.effectAllowed = "copy";
|
||
event.dataTransfer.setData("application/x-sokkog-combo-input", JSON.stringify(createInput(kind, value)));
|
||
}
|
||
|
||
function dragDraftInput(event, stepIndex, inputIndex, input) {
|
||
event.dataTransfer.effectAllowed = "move";
|
||
event.dataTransfer.setData("application/x-sokkog-combo-input", JSON.stringify({
|
||
...input,
|
||
sourceStepIndex: stepIndex,
|
||
sourceInputIndex: inputIndex
|
||
}));
|
||
}
|
||
|
||
function dropInputOnStep(event, stepIndex) {
|
||
const rawInput = event.dataTransfer.getData("application/x-sokkog-combo-input");
|
||
if (!rawInput) return;
|
||
try {
|
||
const input = JSON.parse(rawInput);
|
||
if (!input?.kind || !input?.value) return;
|
||
if (Number.isInteger(input.sourceStepIndex) && Number.isInteger(input.sourceInputIndex)) {
|
||
moveDraftInput(input.sourceStepIndex, input.sourceInputIndex, stepIndex);
|
||
} else {
|
||
addInputToStep(input.kind, input.value, stepIndex);
|
||
}
|
||
setActiveStepIndex(stepIndex);
|
||
} catch {
|
||
// Ignore invalid drag payloads from outside the app.
|
||
}
|
||
}
|
||
|
||
function dropInputAsNewStep(event) {
|
||
const rawInput = event.dataTransfer.getData("application/x-sokkog-combo-input");
|
||
if (!rawInput) return;
|
||
try {
|
||
const input = JSON.parse(rawInput);
|
||
if (!input?.kind || !input?.value) return;
|
||
if (Number.isInteger(input.sourceStepIndex) && Number.isInteger(input.sourceInputIndex)) {
|
||
moveDraftInputToNewStep(input.sourceStepIndex, input.sourceInputIndex);
|
||
} else {
|
||
addDraftInput(input.kind, input.value);
|
||
}
|
||
} catch {
|
||
// Ignore invalid drag payloads from outside the app.
|
||
}
|
||
}
|
||
|
||
function moveDraftInput(sourceStepIndex, sourceInputIndex, targetStepIndex) {
|
||
setDraftInputs((steps) => {
|
||
if (sourceStepIndex === targetStepIndex) return steps;
|
||
const nextSteps = steps.map((step) => [...step]);
|
||
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
|
||
if (!input || !nextSteps[targetStepIndex]) return steps;
|
||
nextSteps[targetStepIndex].push(input);
|
||
setSelectedInput({ stepIndex: targetStepIndex, inputIndex: nextSteps[targetStepIndex].length - 1 });
|
||
return nextSteps.filter((step, index) => step.length || index === targetStepIndex);
|
||
});
|
||
}
|
||
|
||
function moveDraftInputToNewStep(sourceStepIndex, sourceInputIndex) {
|
||
setDraftInputs((steps) => {
|
||
const nextSteps = steps.map((step) => [...step]);
|
||
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
|
||
if (!input) return steps;
|
||
const safeSteps = nextSteps.filter((step) => step.length);
|
||
setActiveStepIndex(safeSteps.length);
|
||
setSelectedInput({ stepIndex: safeSteps.length, inputIndex: 0 });
|
||
return [...safeSteps, [input]];
|
||
});
|
||
}
|
||
|
||
function removeActiveStep() {
|
||
setDraftInputs((steps) => {
|
||
const nextSteps = steps.filter((_, index) => index !== activeStepIndex);
|
||
const safeSteps = nextSteps.length ? nextSteps : [[]];
|
||
setActiveStepIndex(Math.min(activeStepIndex, safeSteps.length - 1));
|
||
setSelectedInput(null);
|
||
return safeSteps;
|
||
});
|
||
}
|
||
|
||
function removeLastInput() {
|
||
setDraftInputs((steps) => {
|
||
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
|
||
let targetIndex = -1;
|
||
for (let index = nextSteps.length - 1; index >= 0; index -= 1) {
|
||
if (nextSteps[index].length) {
|
||
targetIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
if (targetIndex < 0) return [[]];
|
||
nextSteps[targetIndex].pop();
|
||
const safeSteps = nextSteps.filter((step) => step.length);
|
||
setActiveStepIndex(Math.max(0, safeSteps.length - 1));
|
||
setSelectedInput(null);
|
||
return safeSteps.length ? safeSteps : [[]];
|
||
});
|
||
}
|
||
|
||
function updateSelectedInputHold(value) {
|
||
if (!selectedDraftInput) return;
|
||
const holdMs = Math.round(parseHoldSeconds(value) * 1000);
|
||
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
|
||
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
|
||
const nextInput = { ...input };
|
||
if (holdMs > 0) {
|
||
nextInput.hold = true;
|
||
nextInput.holdMs = holdMs;
|
||
}
|
||
else delete nextInput.holdMs;
|
||
return nextInput;
|
||
})));
|
||
}
|
||
|
||
function toggleSelectedInputHold() {
|
||
if (!selectedDraftInput) return;
|
||
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
|
||
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
|
||
const nextInput = { ...input };
|
||
if (nextInput.hold || nextInput.holdMs) {
|
||
delete nextInput.hold;
|
||
delete nextInput.holdMs;
|
||
} else {
|
||
nextInput.hold = true;
|
||
}
|
||
return nextInput;
|
||
})));
|
||
}
|
||
|
||
function selectDraftStep(stepIndex) {
|
||
setActiveStepIndex(stepIndex);
|
||
setSelectedInput(null);
|
||
}
|
||
|
||
function addCustomKey(event) {
|
||
event.preventDefault();
|
||
const value = normalizeCustomKey(customKey);
|
||
if (!value || data.device !== "keyboardMouse") return;
|
||
addDraftInput("key", value.toLowerCase());
|
||
setCustomKey("");
|
||
}
|
||
|
||
function saveCombo(event) {
|
||
event.preventDefault();
|
||
if (!comboName.trim() && !cleanDraftInputs.length) return;
|
||
const combo = createCombo(context, editingComboId, comboName, comboCategory, cleanDraftInputs, textContent.defaultComboName || "Combo", data.device);
|
||
const nextCombos = data.combos.filter((item) => item.id !== editingComboId);
|
||
const nextCategoryOrder = combo.category && !data.categoryOrder.includes(combo.category)
|
||
? [...data.categoryOrder, combo.category]
|
||
: data.categoryOrder;
|
||
save({ ...data, combos: [...nextCombos, combo], categoryOrder: nextCategoryOrder });
|
||
cancelComboEdit();
|
||
}
|
||
|
||
function startComboEdit(combo) {
|
||
setComboCategory(getComboCategory(combo));
|
||
setEditingComboId(combo.id);
|
||
setComboName(combo.name);
|
||
setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]);
|
||
setActiveStepIndex(0);
|
||
setSelectedInput(null);
|
||
}
|
||
|
||
function cancelComboEdit() {
|
||
setEditingComboId("");
|
||
setComboName("");
|
||
setComboCategory("");
|
||
setDraftInputs([[]]);
|
||
setActiveStepIndex(0);
|
||
setSelectedInput(null);
|
||
setSimultaneousMode(false);
|
||
}
|
||
|
||
function updateCombo(comboId, updater) {
|
||
save({ ...data, combos: data.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length) });
|
||
}
|
||
|
||
function deleteCombo(comboId) {
|
||
updateCombo(comboId, () => ({ name: "", inputs: [] }));
|
||
if (editingComboId === comboId) cancelComboEdit();
|
||
}
|
||
|
||
function toggleCategoryCollapsed(category) {
|
||
const collapsedCategories = data.collapsedCategories.includes(category)
|
||
? data.collapsedCategories.filter((item) => item !== category)
|
||
: [...data.collapsedCategories, category];
|
||
save({ ...data, collapsedCategories });
|
||
}
|
||
|
||
function renderCombo(combo) {
|
||
const category = getComboCategory(combo);
|
||
const comboClassName = [
|
||
"combo-card",
|
||
"is-editing",
|
||
isItemDragging(combo.id) ? "is-dragging" : "",
|
||
isItemDropTarget(combo.id) ? "is-drop-target" : "",
|
||
getDropPlacement("item", combo.id) === "after" ? "drop-after" : ""
|
||
].filter(Boolean).join(" ");
|
||
return (
|
||
<article className={comboClassName} key={combo.id} {...getItemProps({ itemId: combo.id, groupId: category })}>
|
||
<button
|
||
className="task-planner-drag-handle combo-drag-handle"
|
||
type="button"
|
||
onPointerDown={(event) => itemReorder.startDrag(event, combo.id)}
|
||
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
|
||
title={textContent.reorderComboTitle || "Déplacer le combo"}
|
||
>
|
||
<Icon name="drag" />
|
||
</button>
|
||
<div className="combo-card-main">
|
||
<InlineTextInput
|
||
className="tool-split-entry-label combo-title-input"
|
||
value={combo.name}
|
||
onCommit={(name) => updateCombo(combo.id, (current) => ({ ...current, name: name || current.name }))}
|
||
ariaLabel={textContent.comboNameLabel || "Nom"}
|
||
/>
|
||
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
|
||
</div>
|
||
<div className="combo-card-actions">
|
||
<button type="button" className="checklist-delete-button" onClick={() => startComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
||
<Icon name="edit" />
|
||
</button>
|
||
<button type="button" className="checklist-delete-button danger" onClick={() => deleteCombo(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
||
<Icon name="trash" />
|
||
</button>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
function renderCategory(group, index, groupCount) {
|
||
const category = group.category;
|
||
const isCollapsed = data.collapsedCategories.includes(category);
|
||
const isLastCategory = index === groupCount - 1;
|
||
const categoryClassName = [
|
||
"combos-category checklist-section is-grouped",
|
||
isCollapsed ? "is-collapsed" : "",
|
||
isGroupDragging(category) ? "is-dragging" : "",
|
||
isGroupDropTarget(category) ? "is-drop-target" : "",
|
||
getDropPlacement("group", category) === "after" ? "drop-after" : ""
|
||
].filter(Boolean).join(" ");
|
||
const showBoundaryDropZones = shouldShowGroupBoundaries();
|
||
const beforeDropZone = showBoundaryDropZones ? (
|
||
<div
|
||
className={`combos-category-boundary-drop-zone ${isBoundaryDropTarget({ groupId: category, placement: "before" }) ? "is-drop-target" : ""}`}
|
||
key={`${category}:before-drop`}
|
||
{...getGroupBoundaryProps({ groupId: category, placement: "before" })}
|
||
/>
|
||
) : null;
|
||
const afterDropZone = showBoundaryDropZones ? (
|
||
<div
|
||
className={`combos-category-boundary-drop-zone ${isBoundaryDropTarget({ groupId: category, placement: "after" }) ? "is-drop-target" : ""}`}
|
||
key={`${category}:after-drop`}
|
||
{...getGroupBoundaryProps({ groupId: category, placement: "after" })}
|
||
/>
|
||
) : null;
|
||
return [
|
||
beforeDropZone,
|
||
<section className={categoryClassName} key={category} {...getGroupProps({ groupId: category })}>
|
||
<div className="checklist-section-header combos-category-header">
|
||
<div className="combos-category-title">
|
||
<button
|
||
className="task-planner-category-drag-handle"
|
||
type="button"
|
||
onPointerDown={(event) => groupReorder.startDrag(event, { groupId: category })}
|
||
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
|
||
title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}
|
||
>
|
||
<Icon name="drag" />
|
||
</button>
|
||
<h3>{category}</h3>
|
||
</div>
|
||
<div className="checklist-section-actions">
|
||
<span>{group.combos.length}</span>
|
||
<button
|
||
className="checklist-section-collapse-button"
|
||
type="button"
|
||
onClick={() => toggleCategoryCollapsed(category)}
|
||
aria-expanded={!isCollapsed}
|
||
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||
title={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||
>
|
||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{!isCollapsed && (
|
||
<div className="combos-list">
|
||
{group.combos.map((combo) => renderCombo(combo))}
|
||
</div>
|
||
)}
|
||
</section>,
|
||
showBoundaryDropZones && isLastCategory ? afterDropZone : null
|
||
];
|
||
}
|
||
|
||
const categoryCount = entries.filter((entry) => entry.type === "group").length;
|
||
let renderedCategoryIndex = -1;
|
||
|
||
function renderEntry(entry) {
|
||
if (entry.type !== "group") return renderCombo(entry.item);
|
||
renderedCategoryIndex += 1;
|
||
return renderCategory(entry.group, renderedCategoryIndex, categoryCount);
|
||
}
|
||
|
||
return (
|
||
<div className="combos-module">
|
||
{(editing || editingComboId) && (
|
||
<div className="module-add-panel combos-editor">
|
||
<div className="combos-device-row">
|
||
<label>
|
||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
||
<select value={data.device} onChange={(event) => updateDevice(event.target.value)}>
|
||
{DEVICE_OPTIONS.map((device) => (
|
||
<option key={device.value} value={device.value}>{device.label}</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
{data.device === "keyboardMouse" && (
|
||
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
|
||
{[
|
||
{ value: "azerty", label: "AZERTY" },
|
||
{ value: "qwerty", label: "QWERTY" }
|
||
].map((layout) => (
|
||
<button
|
||
key={layout.value}
|
||
type="button"
|
||
className={keyboardLayout === layout.value ? "active" : ""}
|
||
onClick={() => setKeyboardLayout(layout.value)}
|
||
aria-pressed={keyboardLayout === layout.value}
|
||
>
|
||
{layout.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<form className="combos-add-form" onSubmit={saveCombo}>
|
||
<div className="combos-form-grid">
|
||
<label>
|
||
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
|
||
<input value={comboCategory} onChange={(event) => setComboCategory(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
|
||
</label>
|
||
<label>
|
||
<span>{textContent.comboNameLabel || "Nom"}</span>
|
||
<input value={comboName} onChange={(event) => setComboName(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
|
||
</label>
|
||
</div>
|
||
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
|
||
{paletteColumns.map((columnGroups, columnIndex) => (
|
||
<div className="combos-palette-column" key={columnIndex}>
|
||
{columnGroups.map((group) => (
|
||
<div className={`combos-palette-group ${group.kind === "directions" ? "is-direction-grid" : ""}`} key={group.title}>
|
||
<span>{group.title}</span>
|
||
<div>
|
||
{group.inputs.map((input, inputIndex) => {
|
||
if (!input) return <span className="combo-palette-empty-cell" key={`empty-${inputIndex}`} aria-hidden="true" />;
|
||
const [kind, value] = input;
|
||
return (
|
||
<ComboInputToken
|
||
key={`${kind}-${value}`}
|
||
input={createInput(kind, value)}
|
||
device={data.device}
|
||
palette
|
||
onClick={() => addDraftInput(kind, value)}
|
||
onDragStart={(event) => dragPaletteInput(event, kind, value)}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{data.device === "keyboardMouse" && (
|
||
<div className="inline-form combos-custom-key-form">
|
||
<input value={customKey} onChange={(event) => setCustomKey(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
|
||
<button type="button" onClick={addCustomKey}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
|
||
</div>
|
||
)}
|
||
|
||
<div className="combo-draft-panel">
|
||
<ComboSequence
|
||
inputs={draftInputs}
|
||
device={data.device}
|
||
emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."}
|
||
activeStepIndex={activeStepIndex}
|
||
selectedInput={selectedInput}
|
||
onSelectStep={selectDraftStep}
|
||
onSelectInput={(stepIndex, inputIndex) => {
|
||
setActiveStepIndex(stepIndex);
|
||
setSelectedInput({ stepIndex, inputIndex });
|
||
}}
|
||
onDropInput={dropInputOnStep}
|
||
onDropNewStep={dropInputAsNewStep}
|
||
onDragInput={dragDraftInput}
|
||
/>
|
||
<div className="combo-draft-actions">
|
||
<label className="combo-hold-field">
|
||
<span className="combo-hold-controls">
|
||
<button
|
||
type="button"
|
||
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
|
||
onClick={toggleSelectedInputHold}
|
||
disabled={!selectedDraftInput}
|
||
aria-pressed={Boolean(selectedDraftInput?.hold || selectedDraftInput?.holdMs)}
|
||
>
|
||
{textContent.holdToggleLabel || "Hold"}
|
||
</button>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="99"
|
||
step="1"
|
||
inputMode="numeric"
|
||
value={selectedDraftInput ? getHoldSeconds(selectedDraftInput) || "" : ""}
|
||
onChange={(event) => updateSelectedInputHold(event.target.value)}
|
||
disabled={!selectedDraftInput}
|
||
placeholder="0"
|
||
aria-label={textContent.holdLabel || "Maintien en secondes"}
|
||
/>
|
||
</span>
|
||
</label>
|
||
<button
|
||
type="button"
|
||
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
|
||
onClick={() => setSimultaneousMode((enabled) => !enabled)}
|
||
aria-pressed={simultaneousMode}
|
||
title={textContent.simultaneousModeTitle || "Ajout simultané"}
|
||
aria-label={textContent.simultaneousModeTitle || "Ajout simultané"}
|
||
>
|
||
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
|
||
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
|
||
</button>
|
||
<button type="button" className="notepad-toolbar-button" onClick={removeLastInput} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
|
||
<Icon name="back" />
|
||
</button>
|
||
<button type="button" className="notepad-toolbar-button danger" onClick={removeActiveStep} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
|
||
<Icon name="trash" />
|
||
</button>
|
||
<div className="combo-save-actions">
|
||
{editingComboId && (
|
||
<button type="button" className="combo-cancel-button" onClick={cancelComboEdit}>
|
||
{textContent.cancelEditButton || "Annuler"}
|
||
</button>
|
||
)}
|
||
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
|
||
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
{editing && !editingComboId && (
|
||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{!data.combos.length && (
|
||
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
||
)}
|
||
|
||
<div className="combos-category-list">
|
||
{entries.map(renderEntry)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|