Refactor large toolbox modules
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-25 08:20:56 +02:00
parent c2e37ad3f2
commit 6dd64f87c3
19 changed files with 2042 additions and 1659 deletions

View file

@ -1,308 +1,30 @@
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
import { useMemo, useState } from "react";
import {
applyGroupedReorderOperation,
getGroupedEntries,
useGroupedReorder
} from "../../../hooks/useGroupedReorder.js";
import { getGroupedEntries } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { CombosEditor } from "./CombosEditor.jsx";
import { CombosList } from "./CombosList.jsx";
import {
createCombo,
createInput,
getComboCategory,
getPaletteColumns,
getPaletteGroups,
normalizeCustomKey,
parseHoldSeconds,
setComboItemCategory
getComboCategory
} from "./combosUtils.js";
import { useCombosEditorState } from "./useCombosEditorState.js";
import { useCombosReorder } from "./useCombosReorder.js";
export function CombosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
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);
}
const editorState = useCombosEditorState({ data, context, textContent, save });
const reorder = useCombosReorder({ data, moduleId, save });
function updateCombo(comboId, updater) {
save({ ...data, combos: data.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length) });
@ -310,7 +32,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
function deleteCombo(comboId) {
updateCombo(comboId, () => ({ name: "", inputs: [] }));
if (editingComboId === comboId) cancelComboEdit();
if (editorState.editingComboId === comboId) editorState.cancelComboEdit();
}
function toggleCategoryCollapsed(category) {
@ -322,45 +44,42 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
return (
<div className="combos-module">
{(editing || editingComboId) && (
{(editing || editorState.editingComboId) && (
<CombosEditor
data={data}
textContent={textContent}
comboCategory={comboCategory}
comboName={comboName}
customKey={customKey}
keyboardLayout={keyboardLayout}
paletteColumns={paletteColumns}
draftInputs={draftInputs}
cleanDraftInputs={cleanDraftInputs}
activeStepIndex={activeStepIndex}
selectedInput={selectedInput}
selectedDraftInput={selectedDraftInput}
simultaneousMode={simultaneousMode}
editingComboId={editingComboId}
onDeviceChange={updateDevice}
onKeyboardLayoutChange={setKeyboardLayout}
onComboCategoryChange={setComboCategory}
onComboNameChange={setComboName}
onCustomKeyChange={setCustomKey}
onCustomKeyAdd={addCustomKey}
onComboSave={saveCombo}
onDraftInputAdd={addDraftInput}
onPaletteInputDrag={dragPaletteInput}
onDraftInputDrag={dragDraftInput}
onInputDrop={dropInputOnStep}
onNewStepDrop={dropInputAsNewStep}
onStepSelect={selectDraftStep}
onInputSelect={(stepIndex, inputIndex) => {
setActiveStepIndex(stepIndex);
setSelectedInput({ stepIndex, inputIndex });
}}
onSelectedInputHoldToggle={toggleSelectedInputHold}
onSelectedInputHoldChange={updateSelectedInputHold}
onSimultaneousModeToggle={() => setSimultaneousMode((enabled) => !enabled)}
onLastInputRemove={removeLastInput}
onActiveStepRemove={removeActiveStep}
onComboEditCancel={cancelComboEdit}
comboCategory={editorState.comboCategory}
comboName={editorState.comboName}
customKey={editorState.customKey}
keyboardLayout={editorState.keyboardLayout}
paletteColumns={editorState.paletteColumns}
draftInputs={editorState.draftInputs}
cleanDraftInputs={editorState.cleanDraftInputs}
activeStepIndex={editorState.activeStepIndex}
selectedInput={editorState.selectedInput}
selectedDraftInput={editorState.selectedDraftInput}
simultaneousMode={editorState.simultaneousMode}
editingComboId={editorState.editingComboId}
onDeviceChange={editorState.updateDevice}
onKeyboardLayoutChange={editorState.setKeyboardLayout}
onComboCategoryChange={editorState.setComboCategory}
onComboNameChange={editorState.setComboName}
onCustomKeyChange={editorState.setCustomKey}
onCustomKeyAdd={editorState.addCustomKey}
onComboSave={editorState.saveCombo}
onDraftInputAdd={editorState.addDraftInput}
onPaletteInputDrag={editorState.dragPaletteInput}
onDraftInputDrag={editorState.dragDraftInput}
onInputDrop={editorState.dropInputOnStep}
onNewStepDrop={editorState.dropInputAsNewStep}
onStepSelect={editorState.selectDraftStep}
onInputSelect={editorState.selectDraftInput}
onSelectedInputHoldToggle={editorState.toggleSelectedInputHold}
onSelectedInputHoldChange={editorState.updateSelectedInputHold}
onSimultaneousModeToggle={editorState.toggleSimultaneousMode}
onLastInputRemove={editorState.removeLastInput}
onActiveStepRemove={editorState.removeActiveStep}
onComboEditCancel={editorState.cancelComboEdit}
/>
)}
{textExchangeOpen && (
@ -378,21 +97,10 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
data={data}
textContent={textContent}
reorder={{
itemReorder,
groupReorder,
getItemProps,
getGroupProps,
getGroupBoundaryProps,
isItemDragging,
isItemDropTarget,
isGroupDragging,
isGroupDropTarget,
isBoundaryDropTarget,
getDropPlacement,
shouldShowGroupBoundaries
...reorder
}}
onComboUpdate={updateCombo}
onComboEdit={startComboEdit}
onComboEdit={editorState.startComboEdit}
onComboDelete={deleteCombo}
onCategoryToggle={toggleCategoryCollapsed}
/>