From 1a35dcae7bf56ba43a46c53ea365bf12d1c1979f Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 3 Aug 2026 12:53:34 +0200 Subject: [PATCH] Add hold and special inputs to combos --- docs/STORAGE_SCHEMA.md | 8 +- tests/helpers/data-validation.mjs | 2 + tests/static-styles.test.mjs | 8 + tests/static-toolboxes.test.mjs | 7 + tests/toolbox-modules.test.mjs | 12 +- website/public/data/site.json | 2 + website/public/static/icons/fist.svg | 2 + website/public/static/icons/foot.svg | 36 +++ website/public/static/icons/grab.svg | 3 + website/public/static/icons/gun.svg | 57 +++++ website/public/static/icons/shield.svg | 4 + .../toolboxes/modules/CombosModule.jsx | 186 ++++++++++++-- .../toolboxes/storage/toolboxStorage.js | 10 +- website/src/styles/_icons.scss | 25 ++ website/src/styles/_toolboxes.scss | 239 +++++++++++++++++- 15 files changed, 575 insertions(+), 26 deletions(-) create mode 100644 website/public/static/icons/fist.svg create mode 100644 website/public/static/icons/foot.svg create mode 100644 website/public/static/icons/grab.svg create mode 100644 website/public/static/icons/gun.svg create mode 100644 website/public/static/icons/shield.svg diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index ebfb398..8055eae 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -442,8 +442,8 @@ Type : `combos` "device": "playstation", "inputs": [ [ - { "kind": "direction", "value": "down" }, - { "kind": "button", "value": "triangle" } + { "kind": "direction", "value": "down", "holdMs": 2000 }, + { "kind": "button", "value": "triangle", "hold": true } ] ] }, @@ -470,6 +470,8 @@ Notes : - `inputs` est une liste d'étapes successives. - chaque étape contient une ou plusieurs entrées simultanées. - `kind` vaut `button`, `direction`, `key` ou `mouse`. +- `hold` est facultatif et indique qu'une entrée doit être maintenue sans durée précise. +- `holdMs` est facultatif et indique qu'une entrée doit être maintenue pendant cette durée en millisecondes ; l'interface limite la saisie à des secondes entières de `1` à `99`. - `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. - `collapsedCategories` stocke les noms des catégories repliées. - les catégories restent locales à l'outil ; l'utilisateur peut ajouter plusieurs outils Combos pour séparer des usages. @@ -478,6 +480,8 @@ Stockage compact : - `device` est omis si la valeur vaut `playstation`. - `category` est omise si vide. +- `hold` est omis si absent ou faux. +- `holdMs` est omis si absent ou nul. - `categoryOrder` est omis si aucune catégorie n'est utilisée. - `collapsedCategories` est omis si aucune catégorie n'est repliée. - `combos` est omis si aucun combo valide n'existe. diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index fe41c04..f24060d 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -189,6 +189,8 @@ export function validateSiteContent(site) { "toolboxes.modules.combos.simultaneousModeTitle", "toolboxes.modules.combos.successiveModeLabel", "toolboxes.modules.combos.simultaneousModeLabel", + "toolboxes.modules.combos.holdLabel", + "toolboxes.modules.combos.holdToggleLabel", "toolboxes.modules.combos.removeStepTitle", "toolboxes.modules.combos.removeLastInputTitle", "toolboxes.modules.combos.addComboButton", diff --git a/tests/static-styles.test.mjs b/tests/static-styles.test.mjs index dbb363d..c0a368e 100644 --- a/tests/static-styles.test.mjs +++ b/tests/static-styles.test.mjs @@ -75,6 +75,14 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styleIcons, /ui-icon-remove-column/); assert.match(styleIcons, /ui-icon-table/); assert.match(styleIcons, /ui-icon-map/); + assert.match(styleIcons, /ui-icon-grab/); + assert.match(styleIcons, /ui-icon-shield/); + assert.match(styleIcons, /ui-icon-gun/); + assert.match(styleToolboxes, /\.combo-hold-controls/); + assert.match(styleToolboxes, /\.combo-input-direction/); + assert.match(styleToolboxes, /\.combo-device-n64\.combo-value-stick-up/); + assert.match(styleToolboxes, /\.combo-device-n64\.combo-value-up/); + assert.match(styleToolboxes, /\.combo-value-grab \.ui-icon/); assert.match(styleGames, /\.game-home-card/); assert.match(styleGames, /\.game-layout/); assert.match(styleGames, /\.game-grid/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 36c84f5..5a3cd5a 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -159,6 +159,13 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(combosModule, /ComboSequence/); assert.match(combosModule, /keyboardLayout/); assert.match(combosModule, /simultaneousMode/); + assert.match(combosModule, /SPECIAL_INPUT_GROUP/); + assert.match(combosModule, /grab/); + assert.match(combosModule, /protect/); + assert.match(combosModule, /shoot/); + assert.match(combosModule, /holdMs/); + assert.match(combosModule, /toggleSelectedInputHold/); + assert.match(combosModule, /combo-hold-field/); assert.match(combosModule, /useInlineEdit/); assert.match(combosModule, /useGroupedReorder/); assert.match(combosModule, /reorderFeatures/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index 0ef8920..ece4441 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -257,7 +257,7 @@ test("combos storage normalizes devices, steps and compact export ids", () => { id: "rootCombo", name: "Root", device: "switch", - inputs: [[{ kind: "button", value: "triangle" }]] + inputs: [[{ kind: "button", value: "triangle", hold: true }]] }, { id: "combo1", @@ -267,7 +267,8 @@ test("combos storage normalizes devices, steps and compact export ids", () => { inputs: [ [ { kind: "direction", value: "down" }, - { kind: "button", value: "cross" }, + { kind: "button", value: "cross", hold: false, holdMs: 2000.4 }, + { kind: "button", value: "circle", holdMs: 100 }, { kind: "bad", value: "ignored" } ], [], @@ -281,6 +282,7 @@ test("combos storage normalizes devices, steps and compact export ids", () => { assert.equal(normalized.device, "playstation"); assert.equal(normalized.combos.length, 2); assert.equal(normalized.combos[0].device, "switch"); + assert.equal(normalized.combos[0].inputs[0][0].hold, true); assert.equal(normalized.combos[1].category.length, 80); assert.equal(normalized.combos[1].name.length, 80); assert.equal("note" in normalized.combos[1], false); @@ -289,7 +291,8 @@ test("combos storage normalizes devices, steps and compact export ids", () => { assert.deepEqual(normalized.combos[1].inputs, [ [ { kind: "direction", value: "down" }, - { kind: "button", value: "cross" } + { kind: "button", value: "cross", hold: true, holdMs: 2000 }, + { kind: "button", value: "circle" } ], [{ kind: "key", value: "space" }] ]); @@ -300,9 +303,12 @@ test("combos storage normalizes devices, steps and compact export ids", () => { assert.deepEqual(compact.collapsedCategories, [longText.slice(0, 80)]); assert.equal(compact.combos[0].id, "rootCombo"); assert.equal(compact.combos[0].device, "switch"); + assert.equal(compact.combos[0].inputs[0][0].hold, true); assert.equal(compact.combos[1].device, "n64"); assert.equal(compact.combos[1].id, "combo1"); assert.equal(compact.combos[1].category.length, 80); + assert.equal(compact.combos[1].inputs[0][1].holdMs, 2000); + assert.equal(compact.combos[1].inputs[0][1].hold, true); const exported = createToolboxExportPayload( { id: "toolbox1", name: "Combos", modules: [{ id: "module1", type: "combos" }], updatedAt: "2026-01-01T00:00:00.000Z" }, diff --git a/website/public/data/site.json b/website/public/data/site.json index 14736f4..fe13a4e 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -348,6 +348,8 @@ "simultaneousModeTitle": "Ajouter dans l'étape active", "successiveModeLabel": "A › B", "simultaneousModeLabel": "A + B", + "holdLabel": "Maintien (s)", + "holdToggleLabel": "Hold", "removeStepTitle": "Supprimer l'étape", "removeLastInputTitle": "Retirer la dernière touche", "addComboButton": "Ajouter le combo", diff --git a/website/public/static/icons/fist.svg b/website/public/static/icons/fist.svg new file mode 100644 index 0000000..28bd64b --- /dev/null +++ b/website/public/static/icons/fist.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/website/public/static/icons/foot.svg b/website/public/static/icons/foot.svg new file mode 100644 index 0000000..550d100 --- /dev/null +++ b/website/public/static/icons/foot.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/grab.svg b/website/public/static/icons/grab.svg new file mode 100644 index 0000000..0e87f34 --- /dev/null +++ b/website/public/static/icons/grab.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/public/static/icons/gun.svg b/website/public/static/icons/gun.svg new file mode 100644 index 0000000..e6dba0f --- /dev/null +++ b/website/public/static/icons/gun.svg @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/website/public/static/icons/shield.svg b/website/public/static/icons/shield.svg new file mode 100644 index 0000000..2ce4c2b --- /dev/null +++ b/website/public/static/icons/shield.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/src/features/toolboxes/modules/CombosModule.jsx b/website/src/features/toolboxes/modules/CombosModule.jsx index 017e8a3..33225c5 100644 --- a/website/src/features/toolboxes/modules/CombosModule.jsx +++ b/website/src/features/toolboxes/modules/CombosModule.jsx @@ -16,26 +16,32 @@ const DEVICE_OPTIONS = [ { value: "keyboardMouse", label: "Clavier/Souris" } ]; +const SPECIAL_INPUT_GROUP = { title: "Spéciale", column: "actions", inputs: [["button", "fist"], ["button", "foot"], ["button", "grab"], ["button", "protect"], ["button", "shoot"]] }; + const DEVICE_GROUPS = { playstation: [ { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, { title: "Actions", column: "actions", inputs: [["button", "triangle"], ["button", "circle"], ["button", "cross"], ["button", "square"], ["button", "l1"], ["button", "r1"], ["button", "l2"], ["button", "r2"]] }, + SPECIAL_INPUT_GROUP, { title: "Système", inputs: [["button", "options"], ["button", "share"], ["button", "l3"], ["button", "r3"]] } ], xbox: [ { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "lb"], ["button", "rb"], ["button", "lt"], ["button", "rt"]] }, + SPECIAL_INPUT_GROUP, { title: "Système", inputs: [["button", "menu"], ["button", "view"], ["button", "ls"], ["button", "rs"]] } ], switch: [ { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "l"], ["button", "r"], ["button", "zl"], ["button", "zr"]] }, + SPECIAL_INPUT_GROUP, { title: "Système", inputs: [["button", "plus"], ["button", "minus"], ["button", "ls"], ["button", "rs"]] } ], n64: [ { title: "Stick", kind: "directions", column: "movement", inputs: [["direction", "stick-up-left"], ["direction", "stick-up"], ["direction", "stick-up-right"], ["direction", "stick-left"], null, ["direction", "stick-right"], ["direction", "stick-down-left"], ["direction", "stick-down"], ["direction", "stick-down-right"]] }, { title: "D-Pad", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "z"], ["button", "start"], ["button", "l"], ["button", "r"]] }, + SPECIAL_INPUT_GROUP, { title: "C-buttons", kind: "directions", inputs: [["button", "c-up"], ["button", "c-down"], ["button", "c-left"], ["button", "c-right"]] } ] }; @@ -45,12 +51,14 @@ const KEYBOARD_GROUPS = { { title: "ZQSD", kind: "directions", column: "movement", inputs: [null, ["key", "z"], null, ["key", "q"], ["key", "s"], ["key", "d"], null, null, null] }, { title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] }, { title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "a"], ["key", "r"], ["key", "f"]] }, + SPECIAL_INPUT_GROUP, { title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] } ], qwerty: [ { title: "WASD", kind: "directions", column: "movement", inputs: [null, ["key", "w"], null, ["key", "a"], ["key", "s"], ["key", "d"], null, null, null] }, { title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] }, { title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "q"], ["key", "r"], ["key", "f"]] }, + SPECIAL_INPUT_GROUP, { title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] } ] }; @@ -84,9 +92,22 @@ const INPUT_LABELS = { "c-down": "C↓", "c-left": "C←", "c-right": "C→", + fist: "Poing", + foot: "Pied", + grab: "Grab", + protect: "Protect", + shoot: "Shoot", space: "Espace" }; +const INPUT_ICONS = { + fist: "fist", + foot: "foot", + grab: "grab", + protect: "shield", + shoot: "gun" +}; + const MOUSE_LABELS = { left: "Clic G", right: "Clic D", @@ -103,11 +124,33 @@ function getInputLabel(input) { } function inputKey(input) { - return `${input.kind}:${input.value}`; + return `${input.kind}:${input.value}:${input.hold ? "hold" : ""}:${input.holdMs || 0}`; } -function createInput(kind, value) { - return { kind, value }; +function createInput(kind, value, holdMs = 0) { + const input = { kind, value }; + if (holdMs > 0) input.holdMs = holdMs; + return input; +} + +function getHoldSeconds(input) { + return Math.round((Number(input?.holdMs) || 0) / 1000); +} + +function formatHoldLabel(input) { + const seconds = getHoldSeconds(input); + if (!seconds) return input?.hold ? "hold" : ""; + return `${seconds}s`; +} + +function parseHoldSeconds(value) { + const seconds = Number.parseInt(String(value || "").replace(/[^\d]/g, ""), 10); + if (!Number.isInteger(seconds) || seconds <= 0) return 0; + return Math.min(99, seconds); +} + +function isDirectionInput(input) { + return input?.kind === "direction"; } function createCombo(context, id, name, category, inputs, fallbackName, device) { @@ -153,9 +196,13 @@ function setComboItemCategory(combo, category) { function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) { const label = getInputLabel(input); + const iconName = INPUT_ICONS[input.value]; + const holdLabel = !palette ? formatHoldLabel(input) : ""; const classNames = [ "combo-input-token", `combo-input-${input.kind}`, + iconName ? "combo-input-icon" : "", + holdLabel ? "has-hold" : "", input.kind === "key" && label.length === 1 ? "combo-input-short" : "", `combo-device-${device}`, `combo-value-${String(input.value || "").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}` @@ -164,19 +211,20 @@ function ComboInputToken({ input, device, palette = false, onClick, onDragStart if (palette) { return ( ); } return ( - - {label} + + {iconName ? : label} + {holdLabel && {holdLabel}} ); } -function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInput, onDragInput }) { +function ComboStep({ step, stepIndex, device, active = false, selectedInputIndex = -1, onClick, onSelectInput, onDropInput, onDragInput }) { const className = [ "combo-step-inputs", step.length > 1 ? "is-simultaneous" : "", @@ -184,9 +232,17 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp active ? "is-active" : "" ].filter(Boolean).join(" "); const content = step.length ? step.map((input, inputIndex) => ( - + {inputIndex > 0 && } - onDragInput(event, stepIndex, inputIndex, input) : null} /> + { + event.stopPropagation(); + onSelectInput(stepIndex, inputIndex); + } : null} + onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null} + /> )) : Étape vide; @@ -214,7 +270,7 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp return {content}; } -function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSelectStep, onDropInput, onDropNewStep, onDragInput }) { +function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, selectedInput, onSelectStep, onSelectInput, onDropInput, onDropNewStep, onDragInput }) { const visibleInputs = inputs.map((step, index) => ({ step, index })).filter(({ step }) => step.length || onSelectStep); if (!visibleInputs.length) return {emptyLabel}; return ( @@ -230,12 +286,26 @@ function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSel onDropNewStep(event); }} > - {visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => ( + {visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => { + const nextStep = visibleInputs[visibleIndex + 1]?.step || []; + const hideSeparator = step.every(isDirectionInput) && nextStep.every(isDirectionInput); + return ( - onSelectStep(stepIndex) : null} onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} onDragInput={onDragInput} /> - {visibleIndex < visibleInputs.length - 1 && } + onSelectStep(stepIndex) : null} + onSelectInput={onSelectInput} + onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} + onDragInput={onDragInput} + /> + {visibleIndex < visibleInputs.length - 1 && !hideSeparator && } - ))} + ); + })} ); } @@ -265,6 +335,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { 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(""); @@ -272,6 +343,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { 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, @@ -344,6 +418,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { 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; }); } @@ -359,6 +434,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { const filledSteps = steps.filter((step) => step.length); const nextSteps = [...filledSteps, [input]]; setActiveStepIndex(nextSteps.length - 1); + setSelectedInput({ stepIndex: nextSteps.length - 1, inputIndex: 0 }); return nextSteps; }); } @@ -417,6 +493,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { 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); }); } @@ -428,6 +505,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { if (!input) return steps; const safeSteps = nextSteps.filter((step) => step.length); setActiveStepIndex(safeSteps.length); + setSelectedInput({ stepIndex: safeSteps.length, inputIndex: 0 }); return [...safeSteps, [input]]; }); } @@ -437,6 +515,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { const nextSteps = steps.filter((_, index) => index !== activeStepIndex); const safeSteps = nextSteps.length ? nextSteps : [[]]; setActiveStepIndex(Math.min(activeStepIndex, safeSteps.length - 1)); + setSelectedInput(null); return safeSteps; }); } @@ -455,10 +534,46 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { 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); @@ -485,6 +600,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { setComboName(combo.name); setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]); setActiveStepIndex(0); + setSelectedInput(null); } function cancelComboEdit() { @@ -493,6 +609,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { setComboCategory(""); setDraftInputs([[]]); setActiveStepIndex(0); + setSelectedInput(null); setSimultaneousMode(false); } @@ -708,8 +825,47 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) { )}
- + { + setActiveStepIndex(stepIndex); + setSelectedInput({ stepIndex, inputIndex }); + }} + onDropInput={dropInputOnStep} + onDropNewStep={dropInputAsNewStep} + onDragInput={dragDraftInput} + />
+