Extract combo editor and table grid
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
9455b31438
commit
e512eef232
5 changed files with 360 additions and 217 deletions
|
|
@ -140,15 +140,17 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
||||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||||
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
||||||
|
const combosEditor = await readFile("website/src/features/toolboxes/modules/CombosEditor.jsx", "utf8");
|
||||||
const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8");
|
const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8");
|
||||||
const combosList = await readFile("website/src/features/toolboxes/modules/CombosList.jsx", "utf8");
|
const combosList = await readFile("website/src/features/toolboxes/modules/CombosList.jsx", "utf8");
|
||||||
const combosBundle = `${combosModule}\n${combosParts}\n${combosList}`;
|
const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}`;
|
||||||
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
|
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
|
||||||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||||
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
|
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
|
||||||
|
const tableGrid = await readFile("website/src/features/toolboxes/modules/TableGrid.jsx", "utf8");
|
||||||
const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8");
|
const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8");
|
||||||
const tableToolbar = await readFile("website/src/features/toolboxes/modules/TableToolbar.jsx", "utf8");
|
const tableToolbar = await readFile("website/src/features/toolboxes/modules/TableToolbar.jsx", "utf8");
|
||||||
const tableBundle = `${tableModule}\n${tableGridParts}\n${tableToolbar}`;
|
const tableBundle = `${tableModule}\n${tableGrid}\n${tableGridParts}\n${tableToolbar}`;
|
||||||
const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8");
|
const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8");
|
||||||
const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8");
|
const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8");
|
||||||
const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8");
|
const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8");
|
||||||
|
|
@ -258,9 +260,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(combosUtils, /grab/);
|
assert.match(combosUtils, /grab/);
|
||||||
assert.match(combosUtils, /protect/);
|
assert.match(combosUtils, /protect/);
|
||||||
assert.match(combosUtils, /shoot/);
|
assert.match(combosUtils, /shoot/);
|
||||||
assert.match(combosModule, /holdMs/);
|
assert.match(combosBundle, /holdMs/);
|
||||||
assert.match(combosModule, /toggleSelectedInputHold/);
|
assert.match(combosModule, /toggleSelectedInputHold/);
|
||||||
assert.match(combosModule, /combo-hold-field/);
|
assert.match(combosEditor, /combo-hold-field/);
|
||||||
assert.match(combosParts, /useInlineEdit/);
|
assert.match(combosParts, /useInlineEdit/);
|
||||||
assert.match(combosModule, /useGroupedReorder/);
|
assert.match(combosModule, /useGroupedReorder/);
|
||||||
assert.match(combosModule, /reorderFeatures/);
|
assert.match(combosModule, /reorderFeatures/);
|
||||||
|
|
@ -316,6 +318,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(tableUtils, /parseTsvBlock/);
|
assert.match(tableUtils, /parseTsvBlock/);
|
||||||
assert.match(tableGridParts, /is-move-target/);
|
assert.match(tableGridParts, /is-move-target/);
|
||||||
assert.match(tableUtils, /isMultiCellSelection/);
|
assert.match(tableUtils, /isMultiCellSelection/);
|
||||||
|
assert.match(tableGrid, /export function TableGrid/);
|
||||||
assert.match(tableModule, /handleCellKeyDown/);
|
assert.match(tableModule, /handleCellKeyDown/);
|
||||||
assert.match(tableModule, /clearSelection/);
|
assert.match(tableModule, /clearSelection/);
|
||||||
assert.match(tableModule, /event\.key === "Delete"/);
|
assert.match(tableModule, /event\.key === "Delete"/);
|
||||||
|
|
|
||||||
194
website/src/features/toolboxes/modules/CombosEditor.jsx
Normal file
194
website/src/features/toolboxes/modules/CombosEditor.jsx
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
// Rôle : rend le formulaire de création et édition des combos.
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
|
||||||
|
import {
|
||||||
|
DEVICE_OPTIONS,
|
||||||
|
createInput,
|
||||||
|
getHoldSeconds
|
||||||
|
} from "./combosUtils.js";
|
||||||
|
|
||||||
|
export function CombosEditor({
|
||||||
|
data,
|
||||||
|
textContent,
|
||||||
|
comboCategory,
|
||||||
|
comboName,
|
||||||
|
customKey,
|
||||||
|
keyboardLayout,
|
||||||
|
paletteColumns,
|
||||||
|
draftInputs,
|
||||||
|
cleanDraftInputs,
|
||||||
|
activeStepIndex,
|
||||||
|
selectedInput,
|
||||||
|
selectedDraftInput,
|
||||||
|
simultaneousMode,
|
||||||
|
editingComboId,
|
||||||
|
onDeviceChange,
|
||||||
|
onKeyboardLayoutChange,
|
||||||
|
onComboCategoryChange,
|
||||||
|
onComboNameChange,
|
||||||
|
onCustomKeyChange,
|
||||||
|
onCustomKeyAdd,
|
||||||
|
onComboSave,
|
||||||
|
onDraftInputAdd,
|
||||||
|
onPaletteInputDrag,
|
||||||
|
onDraftInputDrag,
|
||||||
|
onInputDrop,
|
||||||
|
onNewStepDrop,
|
||||||
|
onStepSelect,
|
||||||
|
onInputSelect,
|
||||||
|
onSelectedInputHoldToggle,
|
||||||
|
onSelectedInputHoldChange,
|
||||||
|
onSimultaneousModeToggle,
|
||||||
|
onLastInputRemove,
|
||||||
|
onActiveStepRemove,
|
||||||
|
onComboEditCancel
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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) => onDeviceChange(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={() => onKeyboardLayoutChange(layout.value)}
|
||||||
|
aria-pressed={keyboardLayout === layout.value}
|
||||||
|
>
|
||||||
|
{layout.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="combos-add-form" onSubmit={onComboSave}>
|
||||||
|
<div className="combos-form-grid">
|
||||||
|
<label>
|
||||||
|
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
|
||||||
|
<input value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{textContent.comboNameLabel || "Nom"}</span>
|
||||||
|
<input value={comboName} onChange={(event) => onComboNameChange(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={() => onDraftInputAdd(kind, value)}
|
||||||
|
onDragStart={(event) => onPaletteInputDrag(event, kind, value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.device === "keyboardMouse" && (
|
||||||
|
<div className="inline-form combos-custom-key-form">
|
||||||
|
<input value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
|
||||||
|
<button type="button" onClick={onCustomKeyAdd}>{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={onStepSelect}
|
||||||
|
onSelectInput={onInputSelect}
|
||||||
|
onDropInput={onInputDrop}
|
||||||
|
onDropNewStep={onNewStepDrop}
|
||||||
|
onDragInput={onDraftInputDrag}
|
||||||
|
/>
|
||||||
|
<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={onSelectedInputHoldToggle}
|
||||||
|
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) => onSelectedInputHoldChange(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={onSimultaneousModeToggle}
|
||||||
|
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={onLastInputRemove} 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={onActiveStepRemove} 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={onComboEditCancel}>
|
||||||
|
{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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
|
||||||
import {
|
import {
|
||||||
applyGroupedReorderOperation,
|
applyGroupedReorderOperation,
|
||||||
getGroupedEntries,
|
getGroupedEntries,
|
||||||
useGroupedReorder
|
useGroupedReorder
|
||||||
} from "../../../hooks/useGroupedReorder.js";
|
} from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
|
import { CombosEditor } from "./CombosEditor.jsx";
|
||||||
import { CombosList } from "./CombosList.jsx";
|
import { CombosList } from "./CombosList.jsx";
|
||||||
import {
|
import {
|
||||||
DEVICE_OPTIONS,
|
|
||||||
createCombo,
|
createCombo,
|
||||||
createInput,
|
createInput,
|
||||||
getComboCategory,
|
getComboCategory,
|
||||||
getHoldSeconds,
|
|
||||||
getPaletteColumns,
|
getPaletteColumns,
|
||||||
getPaletteGroups,
|
getPaletteGroups,
|
||||||
normalizeCustomKey,
|
normalizeCustomKey,
|
||||||
|
|
@ -326,155 +323,45 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
|
||||||
return (
|
return (
|
||||||
<div className="combos-module">
|
<div className="combos-module">
|
||||||
{(editing || editingComboId) && (
|
{(editing || editingComboId) && (
|
||||||
<div className="module-add-panel combos-editor">
|
<CombosEditor
|
||||||
<div className="combos-device-row">
|
data={data}
|
||||||
<label>
|
textContent={textContent}
|
||||||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
comboCategory={comboCategory}
|
||||||
<select value={data.device} onChange={(event) => updateDevice(event.target.value)}>
|
comboName={comboName}
|
||||||
{DEVICE_OPTIONS.map((device) => (
|
customKey={customKey}
|
||||||
<option key={device.value} value={device.value}>{device.label}</option>
|
keyboardLayout={keyboardLayout}
|
||||||
))}
|
paletteColumns={paletteColumns}
|
||||||
</select>
|
draftInputs={draftInputs}
|
||||||
</label>
|
cleanDraftInputs={cleanDraftInputs}
|
||||||
{data.device === "keyboardMouse" && (
|
activeStepIndex={activeStepIndex}
|
||||||
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
|
selectedInput={selectedInput}
|
||||||
{[
|
selectedDraftInput={selectedDraftInput}
|
||||||
{ value: "azerty", label: "AZERTY" },
|
simultaneousMode={simultaneousMode}
|
||||||
{ value: "qwerty", label: "QWERTY" }
|
editingComboId={editingComboId}
|
||||||
].map((layout) => (
|
onDeviceChange={updateDevice}
|
||||||
<button
|
onKeyboardLayoutChange={setKeyboardLayout}
|
||||||
key={layout.value}
|
onComboCategoryChange={setComboCategory}
|
||||||
type="button"
|
onComboNameChange={setComboName}
|
||||||
className={keyboardLayout === layout.value ? "active" : ""}
|
onCustomKeyChange={setCustomKey}
|
||||||
onClick={() => setKeyboardLayout(layout.value)}
|
onCustomKeyAdd={addCustomKey}
|
||||||
aria-pressed={keyboardLayout === layout.value}
|
onComboSave={saveCombo}
|
||||||
>
|
onDraftInputAdd={addDraftInput}
|
||||||
{layout.label}
|
onPaletteInputDrag={dragPaletteInput}
|
||||||
</button>
|
onDraftInputDrag={dragDraftInput}
|
||||||
))}
|
onInputDrop={dropInputOnStep}
|
||||||
</div>
|
onNewStepDrop={dropInputAsNewStep}
|
||||||
)}
|
onStepSelect={selectDraftStep}
|
||||||
</div>
|
onInputSelect={(stepIndex, inputIndex) => {
|
||||||
|
setActiveStepIndex(stepIndex);
|
||||||
<form className="combos-add-form" onSubmit={saveCombo}>
|
setSelectedInput({ stepIndex, inputIndex });
|
||||||
<div className="combos-form-grid">
|
}}
|
||||||
<label>
|
onSelectedInputHoldToggle={toggleSelectedInputHold}
|
||||||
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
|
onSelectedInputHoldChange={updateSelectedInputHold}
|
||||||
<input value={comboCategory} onChange={(event) => setComboCategory(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
|
onSimultaneousModeToggle={() => setSimultaneousMode((enabled) => !enabled)}
|
||||||
</label>
|
onLastInputRemove={removeLastInput}
|
||||||
<label>
|
onActiveStepRemove={removeActiveStep}
|
||||||
<span>{textContent.comboNameLabel || "Nom"}</span>
|
onComboEditCancel={cancelComboEdit}
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
{textExchangeOpen && (
|
||||||
<div className="module-text-exchange-panel">
|
<div className="module-text-exchange-panel">
|
||||||
|
|
|
||||||
92
website/src/features/toolboxes/modules/TableGrid.jsx
Normal file
92
website/src/features/toolboxes/modules/TableGrid.jsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Rôle : rend la grille du tableau et connecte les cellules aux handlers du module.
|
||||||
|
import { columnIndexToName, parseCellAddress } from "./tableFormulaEngine.js";
|
||||||
|
import { HeaderLabelInput, TableRow } from "./TableGridParts.jsx";
|
||||||
|
|
||||||
|
export function TableGrid({
|
||||||
|
data,
|
||||||
|
textContent,
|
||||||
|
evaluatedCells,
|
||||||
|
editingCell,
|
||||||
|
highlightedCell,
|
||||||
|
selection,
|
||||||
|
movePreview,
|
||||||
|
columnWidths,
|
||||||
|
rowHeaderWidth,
|
||||||
|
draggingSelectionRef,
|
||||||
|
onColumnLabelChange,
|
||||||
|
onRowLabelChange,
|
||||||
|
onCellChange,
|
||||||
|
onFormulaSelectionStart,
|
||||||
|
onFormulaSelectionExtend,
|
||||||
|
onStartMoveSelection,
|
||||||
|
onMoveSelection,
|
||||||
|
onSelectCell,
|
||||||
|
onExtendSelection,
|
||||||
|
onPasteSelection,
|
||||||
|
onCellKeyDown,
|
||||||
|
onStartEditing,
|
||||||
|
onEditingCellClear,
|
||||||
|
registerInput
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="table-grid-wrap legacy-scrollbar">
|
||||||
|
<div
|
||||||
|
className="table-grid"
|
||||||
|
style={{
|
||||||
|
"--table-columns": `${rowHeaderWidth}px ${columnWidths.map((width) => `minmax(${width}px, 1fr)`).join(" ")}`,
|
||||||
|
"--table-min-width": `${rowHeaderWidth + columnWidths.reduce((total, width) => total + width, 0)}px`
|
||||||
|
}}
|
||||||
|
role="grid"
|
||||||
|
aria-label={textContent.gridLabel || "Tableau"}
|
||||||
|
>
|
||||||
|
<span className="table-corner" aria-hidden="true" />
|
||||||
|
{Array.from({ length: data.columns }, (_, columnIndex) => (
|
||||||
|
<HeaderLabelInput
|
||||||
|
className="table-header-cell table-header-input"
|
||||||
|
key={columnIndex}
|
||||||
|
value={data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex)}
|
||||||
|
fallback={columnIndexToName(columnIndex)}
|
||||||
|
onSave={(value) => onColumnLabelChange(columnIndex, value)}
|
||||||
|
aria-label={`${textContent.columnLabel || "Intitulé de colonne"} ${columnIndexToName(columnIndex)}`}
|
||||||
|
title={`${columnIndexToName(columnIndex)} - ${textContent.columnFormulaTitle || "Référence conservée dans les formules"}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{Array.from({ length: data.rows }, (_, rowIndex) => (
|
||||||
|
<TableRow
|
||||||
|
key={rowIndex}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
columns={data.columns}
|
||||||
|
cells={data.cells}
|
||||||
|
evaluatedCells={evaluatedCells}
|
||||||
|
editingCell={editingCell}
|
||||||
|
highlightedCell={highlightedCell}
|
||||||
|
textContent={textContent}
|
||||||
|
rowLabel={data.rowLabels?.[rowIndex] || ""}
|
||||||
|
selection={selection}
|
||||||
|
movePreview={movePreview}
|
||||||
|
onRowLabelChange={onRowLabelChange}
|
||||||
|
onFocus={(cell) => {
|
||||||
|
const parsed = parseCellAddress(cell);
|
||||||
|
if (parsed && !selection) onSelectCell(parsed.rowIndex, parsed.columnIndex);
|
||||||
|
}}
|
||||||
|
onBlur={(cell) => {
|
||||||
|
if (editingCell === cell) onEditingCellClear();
|
||||||
|
}}
|
||||||
|
onCellChange={onCellChange}
|
||||||
|
onFormulaSelectionStart={onFormulaSelectionStart}
|
||||||
|
onFormulaSelectionExtend={onFormulaSelectionExtend}
|
||||||
|
onStartMoveSelection={onStartMoveSelection}
|
||||||
|
onMoveSelection={onMoveSelection}
|
||||||
|
onSelectCell={onSelectCell}
|
||||||
|
onExtendSelection={onExtendSelection}
|
||||||
|
onPasteSelection={onPasteSelection}
|
||||||
|
draggingSelectionRef={draggingSelectionRef}
|
||||||
|
onCellKeyDown={onCellKeyDown}
|
||||||
|
onStartEditing={onStartEditing}
|
||||||
|
registerInput={registerInput}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import { cellAddress, columnIndexToName, evaluateTable, parseCellAddress } from "./tableFormulaEngine.js";
|
import { cellAddress, columnIndexToName, evaluateTable, parseCellAddress } from "./tableFormulaEngine.js";
|
||||||
import { HeaderLabelInput, TableRow } from "./TableGridParts.jsx";
|
import { TableGrid } from "./TableGrid.jsx";
|
||||||
import { TableToolbar } from "./TableToolbar.jsx";
|
import { TableToolbar } from "./TableToolbar.jsx";
|
||||||
import {
|
import {
|
||||||
DEFAULT_COLUMNS,
|
DEFAULT_COLUMNS,
|
||||||
|
|
@ -410,65 +410,32 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen })
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={copyTsv} />
|
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={copyTsv} />
|
||||||
<div className="table-grid-wrap legacy-scrollbar">
|
<TableGrid
|
||||||
<div
|
data={data}
|
||||||
className="table-grid"
|
textContent={textContent}
|
||||||
style={{
|
evaluatedCells={evaluatedCells}
|
||||||
"--table-columns": `${rowHeaderWidth}px ${columnWidths.map((width) => `minmax(${width}px, 1fr)`).join(" ")}`,
|
editingCell={editingCell}
|
||||||
"--table-min-width": `${rowHeaderWidth + columnWidths.reduce((total, width) => total + width, 0)}px`
|
highlightedCell={highlightedCell}
|
||||||
}}
|
selection={selection}
|
||||||
role="grid"
|
movePreview={movePreview}
|
||||||
aria-label={textContent.gridLabel || "Tableau"}
|
columnWidths={columnWidths}
|
||||||
>
|
rowHeaderWidth={rowHeaderWidth}
|
||||||
<span className="table-corner" aria-hidden="true" />
|
draggingSelectionRef={draggingSelectionRef}
|
||||||
{Array.from({ length: data.columns }, (_, columnIndex) => (
|
onColumnLabelChange={setColumnLabel}
|
||||||
<HeaderLabelInput
|
onRowLabelChange={setRowLabel}
|
||||||
className="table-header-cell table-header-input"
|
onCellChange={setCellValue}
|
||||||
key={columnIndex}
|
onFormulaSelectionStart={startFormulaSelection}
|
||||||
value={data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex)}
|
onFormulaSelectionExtend={extendFormulaSelection}
|
||||||
fallback={columnIndexToName(columnIndex)}
|
onStartMoveSelection={startMovingSelection}
|
||||||
onSave={(value) => setColumnLabel(columnIndex, value)}
|
onMoveSelection={updateMovePreview}
|
||||||
aria-label={`${textContent.columnLabel || "Intitulé de colonne"} ${columnIndexToName(columnIndex)}`}
|
onSelectCell={selectCell}
|
||||||
title={`${columnIndexToName(columnIndex)} - ${textContent.columnFormulaTitle || "Référence conservée dans les formules"}`}
|
onExtendSelection={extendSelection}
|
||||||
/>
|
onPasteSelection={pasteSelection}
|
||||||
))}
|
onCellKeyDown={handleCellKeyDown}
|
||||||
{Array.from({ length: data.rows }, (_, rowIndex) => (
|
onStartEditing={startEditing}
|
||||||
<TableRow
|
onEditingCellClear={() => setEditingCell("")}
|
||||||
key={rowIndex}
|
registerInput={registerInput}
|
||||||
rowIndex={rowIndex}
|
/>
|
||||||
columns={data.columns}
|
|
||||||
cells={data.cells}
|
|
||||||
evaluatedCells={evaluatedCells}
|
|
||||||
editingCell={editingCell}
|
|
||||||
highlightedCell={highlightedCell}
|
|
||||||
textContent={textContent}
|
|
||||||
rowLabel={data.rowLabels?.[rowIndex] || ""}
|
|
||||||
selection={selection}
|
|
||||||
movePreview={movePreview}
|
|
||||||
onRowLabelChange={setRowLabel}
|
|
||||||
onFocus={(cell) => {
|
|
||||||
const parsed = parseCellAddress(cell);
|
|
||||||
if (parsed && !selection) selectCell(parsed.rowIndex, parsed.columnIndex);
|
|
||||||
}}
|
|
||||||
onBlur={(cell) => {
|
|
||||||
if (editingCell === cell) setEditingCell("");
|
|
||||||
}}
|
|
||||||
onCellChange={setCellValue}
|
|
||||||
onFormulaSelectionStart={startFormulaSelection}
|
|
||||||
onFormulaSelectionExtend={extendFormulaSelection}
|
|
||||||
onStartMoveSelection={startMovingSelection}
|
|
||||||
onMoveSelection={updateMovePreview}
|
|
||||||
onSelectCell={selectCell}
|
|
||||||
onExtendSelection={extendSelection}
|
|
||||||
onPasteSelection={pasteSelection}
|
|
||||||
draggingSelectionRef={draggingSelectionRef}
|
|
||||||
onCellKeyDown={handleCellKeyDown}
|
|
||||||
onStartEditing={startEditing}
|
|
||||||
registerInput={registerInput}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue