Refactor large toolbox modules
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
This commit is contained in:
parent
c2e37ad3f2
commit
6dd64f87c3
19 changed files with 2042 additions and 1659 deletions
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue