kelt-ui-kit-react 2.1.8 → 2.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/damier/damierCell/damierCell.interface.d.ts +3 -0
- package/dist/damier/damierEditor/DamierEditor.d.ts +33 -0
- package/dist/damier/damierEditor/damierEditorCell/DamierEditorCell.d.ts +16 -0
- package/dist/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.d.ts +10 -0
- package/dist/damier/damierEditor/damierEditorTree/DamierEditorTree.d.ts +13 -0
- package/dist/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.d.ts +11 -0
- package/dist/damier/damierEditor/damierEditorTree/damierTreeNode.interface.d.ts +5 -0
- package/dist/damier/damierEditor/maybePromise.interface.d.ts +1 -0
- package/dist/damier/damierPage/damierPage.interface.d.ts +8 -0
- package/dist/damier/utils/buildDamierTree.d.ts +3 -0
- package/dist/damier/utils/createDefaultDamierPage.d.ts +2 -0
- package/dist/damier/utils/damierDefaultSize.d.ts +1 -0
- package/dist/damier/utils/findDamierTreePath.d.ts +2 -0
- package/dist/damier/utils/getFirstFreePosition.d.ts +2 -0
- package/dist/damier/utils/moveDamierCell.d.ts +2 -0
- package/dist/form/form.interface.d.ts +2 -0
- package/dist/form/getFieldError.d.ts +2 -0
- package/dist/index.js +881 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/damier/Damier.tsx +1 -1
- package/src/damier/Damier.view.tsx +46 -0
- package/src/damier/damierCell/DamierCell.tsx +1 -1
- package/src/damier/damierCell/damierCell.interface.tsx +3 -0
- package/src/damier/damierEditor/DamierEditor.tsx +346 -0
- package/src/damier/damierEditor/damierEditor.css +255 -0
- package/src/damier/damierEditor/damierEditorCell/DamierEditorCell.tsx +91 -0
- package/src/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.tsx +71 -0
- package/src/damier/damierEditor/damierEditorTree/DamierEditorTree.tsx +134 -0
- package/src/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.tsx +78 -0
- package/src/damier/damierEditor/damierEditorTree/damierTreeNode.interface.ts +6 -0
- package/src/damier/damierEditor/maybePromise.interface.ts +1 -0
- package/src/damier/damierPage/damierPage.interface.tsx +9 -0
- package/src/damier/utils/buildDamierTree.ts +31 -0
- package/src/damier/utils/createDefaultDamierPage.ts +10 -0
- package/src/damier/utils/damierDefaultSize.ts +1 -0
- package/src/damier/utils/findDamierTreePath.ts +14 -0
- package/src/damier/utils/getFirstFreePosition.ts +12 -0
- package/src/damier/utils/moveDamierCell.ts +13 -0
- package/src/dataTable/DataTable.tsx +2 -2
- package/src/docs/catalog.ts +1 -1
- package/src/form/Form.tsx +68 -8
- package/src/form/form.css +9 -0
- package/src/form/form.interface.tsx +2 -0
- package/src/form/getFieldError.ts +14 -0
- package/src/select/select.css +11 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DragEvent } from "react";
|
|
2
|
+
import { Icon } from "../../../icon/Icon";
|
|
3
|
+
import { DamierCellInterface } from "../../damierCell/damierCell.interface";
|
|
4
|
+
|
|
5
|
+
interface DamierEditorCellProps {
|
|
6
|
+
position: number;
|
|
7
|
+
cell?: DamierCellInterface;
|
|
8
|
+
dragging?: boolean;
|
|
9
|
+
dragOver?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onDragStart: (position: number) => void;
|
|
12
|
+
onDragEnd: () => void;
|
|
13
|
+
onDragOver: (position: number) => void;
|
|
14
|
+
onDrop: (from: number, to: number) => void;
|
|
15
|
+
onClick: (position: number, cell?: DamierCellInterface) => void;
|
|
16
|
+
onDelete?: (cell: DamierCellInterface) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Emplacement du damier en édition : cible de dépôt, et source de glisser si une case l'occupe
|
|
20
|
+
export const DamierEditorCell = ({
|
|
21
|
+
position,
|
|
22
|
+
cell,
|
|
23
|
+
dragging = false,
|
|
24
|
+
dragOver = false,
|
|
25
|
+
disabled = false,
|
|
26
|
+
onDragStart,
|
|
27
|
+
onDragEnd,
|
|
28
|
+
onDragOver,
|
|
29
|
+
onDrop,
|
|
30
|
+
onClick,
|
|
31
|
+
onDelete,
|
|
32
|
+
}: DamierEditorCellProps) => {
|
|
33
|
+
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
|
|
34
|
+
e.dataTransfer.setData("text/plain", String(position));
|
|
35
|
+
e.dataTransfer.effectAllowed = "move";
|
|
36
|
+
onDragStart(position);
|
|
37
|
+
};
|
|
38
|
+
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
39
|
+
if (disabled) return;
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.dataTransfer.dropEffect = "move";
|
|
42
|
+
onDragOver(position);
|
|
43
|
+
};
|
|
44
|
+
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
const from = Number(e.dataTransfer.getData("text/plain"));
|
|
47
|
+
if (from) onDrop(from, position);
|
|
48
|
+
};
|
|
49
|
+
const classNames = [
|
|
50
|
+
"damier-editor-cell",
|
|
51
|
+
cell ? "" : "damier-editor-cell--empty",
|
|
52
|
+
dragging ? "damier-editor-cell--dragging" : "",
|
|
53
|
+
dragOver ? "damier-editor-cell--over" : "",
|
|
54
|
+
].join(" ");
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={classNames}
|
|
59
|
+
draggable={!!cell && !disabled}
|
|
60
|
+
title={cell ? cell.label : "Ajouter une case"}
|
|
61
|
+
onDragStart={handleDragStart}
|
|
62
|
+
onDragEnd={onDragEnd}
|
|
63
|
+
onDragOver={handleDragOver}
|
|
64
|
+
onDrop={handleDrop}
|
|
65
|
+
onClick={() => !disabled && onClick(position, cell)}
|
|
66
|
+
>
|
|
67
|
+
{cell ? (
|
|
68
|
+
<>
|
|
69
|
+
<div className="damier-editor-cell-content">
|
|
70
|
+
{cell.child ?? <span>{cell.label}</span>}
|
|
71
|
+
</div>
|
|
72
|
+
{onDelete && !disabled && (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="damier-editor-cell-delete"
|
|
76
|
+
aria-label="Supprimer la case"
|
|
77
|
+
onClick={(e) => {
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
onDelete(cell);
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Icon classIcon="bi-x-lg" />
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</>
|
|
86
|
+
) : (
|
|
87
|
+
<Icon classIcon="bi-plus-lg damier-editor-cell-add" />
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Button } from "../../../button/Button";
|
|
3
|
+
import { TypeInputEnum } from "../../../form/form.enum";
|
|
4
|
+
import { Input } from "../../../form/input/Input";
|
|
5
|
+
import { DamierPageInterface } from "../../damierPage/damierPage.interface";
|
|
6
|
+
|
|
7
|
+
interface DamierEditorToolbarProps {
|
|
8
|
+
page: DamierPageInterface;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
canAddCell: boolean;
|
|
11
|
+
onChangeDimension: (cols: number, rows: number) => void;
|
|
12
|
+
onAddCell: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Dimension de la page courante (colonnes x lignes) et ajout rapide d'une case
|
|
16
|
+
export const DamierEditorToolbar = ({
|
|
17
|
+
page,
|
|
18
|
+
disabled = false,
|
|
19
|
+
canAddCell,
|
|
20
|
+
onChangeDimension,
|
|
21
|
+
onAddCell,
|
|
22
|
+
}: DamierEditorToolbarProps) => {
|
|
23
|
+
const [cols, setCols] = useState(page.cols);
|
|
24
|
+
const [rows, setRows] = useState(page.rows);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setCols(page.cols);
|
|
27
|
+
setRows(page.rows);
|
|
28
|
+
}, [page.id, page.cols, page.rows]);
|
|
29
|
+
const changed = cols !== page.cols || rows !== page.rows;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="damier-editor-toolbar">
|
|
33
|
+
<label className="damier-editor-dimension">
|
|
34
|
+
Colonnes
|
|
35
|
+
<Input
|
|
36
|
+
id="damier-editor-cols"
|
|
37
|
+
name="damier-editor-cols"
|
|
38
|
+
type={TypeInputEnum.NUMBER}
|
|
39
|
+
value={cols}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
disabledDebounce
|
|
42
|
+
onChange={(e) => setCols(Number(e.target.value))}
|
|
43
|
+
/>
|
|
44
|
+
</label>
|
|
45
|
+
<label className="damier-editor-dimension">
|
|
46
|
+
Lignes
|
|
47
|
+
<Input
|
|
48
|
+
id="damier-editor-rows"
|
|
49
|
+
name="damier-editor-rows"
|
|
50
|
+
type={TypeInputEnum.NUMBER}
|
|
51
|
+
value={rows}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
disabledDebounce
|
|
54
|
+
onChange={(e) => setRows(Number(e.target.value))}
|
|
55
|
+
/>
|
|
56
|
+
</label>
|
|
57
|
+
<Button
|
|
58
|
+
title="Appliquer la dimension"
|
|
59
|
+
classIcon="bi-aspect-ratio"
|
|
60
|
+
disabled={disabled || !changed}
|
|
61
|
+
onClick={() => onChangeDimension(cols, rows)}
|
|
62
|
+
/>
|
|
63
|
+
<Button
|
|
64
|
+
title="Ajouter une case"
|
|
65
|
+
classIcon="bi-plus-square"
|
|
66
|
+
disabled={disabled || !canAddCell}
|
|
67
|
+
onClick={onAddCell}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { Button } from "../../../button/Button";
|
|
3
|
+
import { Input } from "../../../form/input/Input";
|
|
4
|
+
import { DamierPageInterface } from "../../damierPage/damierPage.interface";
|
|
5
|
+
import { buildDamierTree } from "../../utils/buildDamierTree";
|
|
6
|
+
import { findDamierTreePath } from "../../utils/findDamierTreePath";
|
|
7
|
+
import { DamierEditorTreeNode } from "./damierEditorTreeNode/DamierEditorTreeNode";
|
|
8
|
+
|
|
9
|
+
interface DamierEditorTreeProps {
|
|
10
|
+
pages: DamierPageInterface[];
|
|
11
|
+
currentPageId: number;
|
|
12
|
+
rootPageId?: number;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onSelect: (page: DamierPageInterface) => void;
|
|
15
|
+
/** Sans callback, le champ d'ajout de page est masqué */
|
|
16
|
+
onAdd?: (title: string) => void;
|
|
17
|
+
onDelete?: (page: DamierPageInterface) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Arborescence des pages (à gauche du damier) + recherche, ajout et suppression de page
|
|
21
|
+
export const DamierEditorTree = ({
|
|
22
|
+
pages,
|
|
23
|
+
currentPageId,
|
|
24
|
+
rootPageId,
|
|
25
|
+
disabled = false,
|
|
26
|
+
onSelect,
|
|
27
|
+
onAdd,
|
|
28
|
+
onDelete,
|
|
29
|
+
}: DamierEditorTreeProps) => {
|
|
30
|
+
const [title, setTitle] = useState("");
|
|
31
|
+
const [search, setSearch] = useState("");
|
|
32
|
+
const tree = useMemo(
|
|
33
|
+
() => buildDamierTree(pages, rootPageId),
|
|
34
|
+
[pages, rootPageId],
|
|
35
|
+
);
|
|
36
|
+
const activePath = useMemo(
|
|
37
|
+
() => findDamierTreePath(tree, currentPageId),
|
|
38
|
+
[tree, currentPageId],
|
|
39
|
+
);
|
|
40
|
+
const term = search.trim().toLocaleLowerCase();
|
|
41
|
+
const results = term
|
|
42
|
+
? pages.filter((page) => page.title.toLocaleLowerCase().includes(term))
|
|
43
|
+
: [];
|
|
44
|
+
const currentPage = pages.find((page) => page.id === currentPageId);
|
|
45
|
+
const add = () => {
|
|
46
|
+
if (!title.trim() || !onAdd) return;
|
|
47
|
+
onAdd(title.trim());
|
|
48
|
+
setTitle("");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<nav className="damier-tree" aria-label="Pages du damier">
|
|
53
|
+
<div className="damier-tree-actions">
|
|
54
|
+
{onAdd && (
|
|
55
|
+
<>
|
|
56
|
+
<Input
|
|
57
|
+
id="damier-editor-page-title"
|
|
58
|
+
name="damier-editor-page-title"
|
|
59
|
+
placeholder="Nouvelle page"
|
|
60
|
+
value={title}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
disabledDebounce
|
|
63
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
64
|
+
handleKeyDown={(e) => e.key === "Enter" && add()}
|
|
65
|
+
/>
|
|
66
|
+
<Button
|
|
67
|
+
title=""
|
|
68
|
+
classIcon="bi-plus-lg"
|
|
69
|
+
disabled={disabled || !title.trim()}
|
|
70
|
+
onClick={add}
|
|
71
|
+
/>
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
{onDelete && currentPage && pages.length > 1 && (
|
|
75
|
+
<Button
|
|
76
|
+
title=""
|
|
77
|
+
classIcon="bi-trash"
|
|
78
|
+
className="damier-editor-danger"
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
onClick={() => onDelete(currentPage)}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
<Input
|
|
85
|
+
id="damier-editor-page-search"
|
|
86
|
+
name="damier-editor-page-search"
|
|
87
|
+
placeholder="Rechercher une page"
|
|
88
|
+
value={search}
|
|
89
|
+
disabledDebounce
|
|
90
|
+
showClearButton
|
|
91
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
<div className="damier-tree-list">
|
|
94
|
+
{term ? (
|
|
95
|
+
<ul>
|
|
96
|
+
{results.map((page) => (
|
|
97
|
+
<li key={page.id}>
|
|
98
|
+
<div
|
|
99
|
+
className={`damier-tree-item ${
|
|
100
|
+
page.id === currentPageId ? "damier-tree-item--active" : ""
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="damier-tree-label"
|
|
106
|
+
title={page.title}
|
|
107
|
+
onClick={() => onSelect(page)}
|
|
108
|
+
>
|
|
109
|
+
{page.title}
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</li>
|
|
113
|
+
))}
|
|
114
|
+
{!results.length && (
|
|
115
|
+
<li className="damier-tree-empty">Aucune page</li>
|
|
116
|
+
)}
|
|
117
|
+
</ul>
|
|
118
|
+
) : (
|
|
119
|
+
<ul role="tree">
|
|
120
|
+
{tree.map((node) => (
|
|
121
|
+
<DamierEditorTreeNode
|
|
122
|
+
key={node.page.id}
|
|
123
|
+
node={node}
|
|
124
|
+
currentPageId={currentPageId}
|
|
125
|
+
activePath={activePath}
|
|
126
|
+
onSelect={onSelect}
|
|
127
|
+
/>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</nav>
|
|
133
|
+
);
|
|
134
|
+
};
|
package/src/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Icon } from "../../../../icon/Icon";
|
|
3
|
+
import { DamierPageInterface } from "../../../damierPage/damierPage.interface";
|
|
4
|
+
import { DamierTreeNodeInterface } from "../damierTreeNode.interface";
|
|
5
|
+
|
|
6
|
+
interface DamierEditorTreeNodeProps {
|
|
7
|
+
node: DamierTreeNodeInterface;
|
|
8
|
+
currentPageId: number;
|
|
9
|
+
activePath: number[];
|
|
10
|
+
depth?: number;
|
|
11
|
+
onSelect: (page: DamierPageInterface) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DamierEditorTreeNode = ({
|
|
15
|
+
node,
|
|
16
|
+
currentPageId,
|
|
17
|
+
activePath,
|
|
18
|
+
depth = 0,
|
|
19
|
+
onSelect,
|
|
20
|
+
}: DamierEditorTreeNodeProps) => {
|
|
21
|
+
const inActivePath = activePath.includes(node.page.id);
|
|
22
|
+
const [expanded, setExpanded] = useState(depth === 0 || inActivePath);
|
|
23
|
+
// On déplie automatiquement le chemin vers la page sélectionnée
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (inActivePath) setExpanded(true);
|
|
26
|
+
}, [inActivePath]);
|
|
27
|
+
const hasChildren = node.children.length > 0;
|
|
28
|
+
const active = node.page.id === currentPageId;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<li
|
|
32
|
+
role="treeitem"
|
|
33
|
+
aria-expanded={hasChildren ? expanded : undefined}
|
|
34
|
+
aria-selected={active}
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
className={`damier-tree-item ${active ? "damier-tree-item--active" : ""}`}
|
|
38
|
+
style={{ paddingLeft: `${depth * 0.9}rem` }}
|
|
39
|
+
>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className="damier-tree-toggle"
|
|
43
|
+
aria-label={expanded ? "Replier" : "Déplier"}
|
|
44
|
+
disabled={!hasChildren}
|
|
45
|
+
onClick={() => setExpanded(!expanded)}
|
|
46
|
+
>
|
|
47
|
+
{hasChildren && (
|
|
48
|
+
<Icon
|
|
49
|
+
classIcon={expanded ? "bi-chevron-down" : "bi-chevron-right"}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className="damier-tree-label"
|
|
56
|
+
title={node.page.title}
|
|
57
|
+
onClick={() => onSelect(node.page)}
|
|
58
|
+
>
|
|
59
|
+
{node.page.title}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
{hasChildren && expanded && (
|
|
63
|
+
<ul role="group">
|
|
64
|
+
{node.children.map((child) => (
|
|
65
|
+
<DamierEditorTreeNode
|
|
66
|
+
key={child.page.id}
|
|
67
|
+
node={child}
|
|
68
|
+
currentPageId={currentPageId}
|
|
69
|
+
activePath={activePath}
|
|
70
|
+
depth={depth + 1}
|
|
71
|
+
onSelect={onSelect}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
)}
|
|
76
|
+
</li>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DamierTreeNodeInterface } from "../damierEditor/damierEditorTree/damierTreeNode.interface";
|
|
2
|
+
import { DamierPageInterface } from "../damierPage/damierPage.interface";
|
|
3
|
+
|
|
4
|
+
// Arborescence des pages à partir des liens des cases (ordre des positions).
|
|
5
|
+
// Chaque page n'apparaît qu'une fois (premier chemin trouvé, pas de cycle) ;
|
|
6
|
+
// les pages non atteignables depuis la racine deviennent des racines à leur tour.
|
|
7
|
+
export const buildDamierTree = (
|
|
8
|
+
pages: DamierPageInterface[],
|
|
9
|
+
rootPageId?: number,
|
|
10
|
+
): DamierTreeNodeInterface[] => {
|
|
11
|
+
const byId = new Map(pages.map((page) => [page.id, page]));
|
|
12
|
+
const visited = new Set<number>();
|
|
13
|
+
const build = (page: DamierPageInterface): DamierTreeNodeInterface => {
|
|
14
|
+
visited.add(page.id);
|
|
15
|
+
const children: DamierPageInterface[] = [];
|
|
16
|
+
[...page.cells]
|
|
17
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
18
|
+
.forEach((cell) => {
|
|
19
|
+
const child = cell.linkPageId ? byId.get(cell.linkPageId) : undefined;
|
|
20
|
+
if (child && !visited.has(child.id)) {
|
|
21
|
+
visited.add(child.id);
|
|
22
|
+
children.push(child);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return { page, children: children.map(build) };
|
|
26
|
+
};
|
|
27
|
+
const root =
|
|
28
|
+
(rootPageId !== undefined ? byId.get(rootPageId) : undefined) ?? pages[0];
|
|
29
|
+
const ordered = root ? [root, ...pages.filter((p) => p !== root)] : pages;
|
|
30
|
+
return ordered.flatMap((page) => (visited.has(page.id) ? [] : [build(page)]));
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DamierPageInterface } from "../damierPage/damierPage.interface";
|
|
2
|
+
import { DAMIER_DEFAULT_SIZE } from "./damierDefaultSize";
|
|
3
|
+
|
|
4
|
+
// Page initiale utilisée quand aucun damier n'est fourni au composant
|
|
5
|
+
export const createDefaultDamierPage = (
|
|
6
|
+
cols = DAMIER_DEFAULT_SIZE,
|
|
7
|
+
rows = DAMIER_DEFAULT_SIZE,
|
|
8
|
+
id = -1,
|
|
9
|
+
title = "Sommaire",
|
|
10
|
+
): DamierPageInterface => ({ id, title, cols, rows, cells: [] });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DAMIER_DEFAULT_SIZE = 9;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DamierTreeNodeInterface } from "../damierEditor/damierEditorTree/damierTreeNode.interface";
|
|
2
|
+
|
|
3
|
+
// Ids des pages ancêtres (incluse) menant à `pageId`, vide si absente de l'arbre
|
|
4
|
+
export const findDamierTreePath = (
|
|
5
|
+
nodes: DamierTreeNodeInterface[],
|
|
6
|
+
pageId: number,
|
|
7
|
+
): number[] => {
|
|
8
|
+
for (const node of nodes) {
|
|
9
|
+
if (node.page.id === pageId) return [node.page.id];
|
|
10
|
+
const path = findDamierTreePath(node.children, pageId);
|
|
11
|
+
if (path.length) return [node.page.id, ...path];
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DamierPageInterface } from "../damierPage/damierPage.interface";
|
|
2
|
+
|
|
3
|
+
// Première position libre (base 1) de la page, undefined si la page est pleine
|
|
4
|
+
export const getFirstFreePosition = (
|
|
5
|
+
page: DamierPageInterface,
|
|
6
|
+
): number | undefined => {
|
|
7
|
+
const taken = new Set(page.cells.map((cell) => cell.position));
|
|
8
|
+
for (let position = 1; position <= page.cols * page.rows; position++) {
|
|
9
|
+
if (!taken.has(position)) return position;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DamierCellInterface } from "../damierCell/damierCell.interface";
|
|
2
|
+
|
|
3
|
+
// Déplace la case de `from` vers `to` ; si `to` est occupée, les deux cases sont échangées.
|
|
4
|
+
export const moveDamierCell = (
|
|
5
|
+
cells: DamierCellInterface[],
|
|
6
|
+
from: number,
|
|
7
|
+
to: number,
|
|
8
|
+
): DamierCellInterface[] =>
|
|
9
|
+
cells.map((cell) => {
|
|
10
|
+
if (cell.position === from) return { ...cell, position: to };
|
|
11
|
+
if (cell.position === to) return { ...cell, position: from };
|
|
12
|
+
return cell;
|
|
13
|
+
});
|
|
@@ -521,9 +521,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
521
521
|
{name && <h2 className="mb-2">{name}</h2>}
|
|
522
522
|
{element}
|
|
523
523
|
<DataTableFilters currentFilters={tableState.filters} setCurrentFilters={tableState.setFilters} />
|
|
524
|
-
{showMobileCardView && columns.some((column) => column.filterable
|
|
524
|
+
{showMobileCardView && columns.some((column) => column.filterable) && (
|
|
525
525
|
<div className="data-table-mobile-controls">
|
|
526
|
-
{columns.filter((column) => column.filterable
|
|
526
|
+
{columns.filter((column) => column.filterable).map((column, idx) => (
|
|
527
527
|
<div key={column.id}>
|
|
528
528
|
<table><thead><tr><DataTableColumn column={column} idx={idx}
|
|
529
529
|
isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
|
package/src/docs/catalog.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const catalog: Record<string, { category: Category; description: string;
|
|
|
23
23
|
"/expands": { category: "Affichage", icon: "arrows-expand", description: "Deplier des sections et organiser des contenus denses." },
|
|
24
24
|
"/grid": { category: "Affichage", icon: "grid-3x3", description: "Construire une mise en page avec des colonnes responsives." },
|
|
25
25
|
"/card": { category: "Affichage", icon: "card-heading", description: "Regrouper contenu, informations et actions dans une carte." },
|
|
26
|
-
"/damier": { category: "Affichage", icon: "grid", description: "Presenter des elements dans une grille de cellules." },
|
|
26
|
+
"/damier": { category: "Affichage", icon: "grid", description: "Presenter des elements dans une grille de cellules, et l'editer (pages, cases, glisser-deposer)." },
|
|
27
27
|
"/carousel": { category: "Affichage", icon: "images", description: "Parcourir une collection de contenus." },
|
|
28
28
|
"/badge": { category: "Affichage", icon: "patch-check", description: "Mettre en valeur un statut ou une information courte." },
|
|
29
29
|
"/data-table": { category: "Affichage", icon: "table", description: "Afficher des donnees, selections, colonnes et actions." },
|
package/src/form/Form.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import { Row } from "../grid/row/Row";
|
|
|
23
23
|
import { GridBreakpoints, GridResponsive } from "../grid/grid.interface";
|
|
24
24
|
import { TypeInputEnum } from "./form.enum";
|
|
25
25
|
import { AddressSearch } from "../addressSearch/AddressSearch";
|
|
26
|
+
import { getFieldError } from "./getFieldError";
|
|
26
27
|
|
|
27
28
|
const euroFormatter = new Intl.NumberFormat("fr-FR", {
|
|
28
29
|
style: "currency",
|
|
@@ -107,8 +108,34 @@ export const DynamicForm = forwardRef(
|
|
|
107
108
|
return initialValues;
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
const [errors, setErrors] = useState<Record<string, string | undefined>>(
|
|
112
|
+
{}
|
|
113
|
+
);
|
|
114
|
+
|
|
110
115
|
const inputRefs = useRef<{ [key: string]: HTMLInputElement | null }>({});
|
|
111
116
|
|
|
117
|
+
/** Revalide le champ pendant la saisie uniquement s'il est déjà en erreur */
|
|
118
|
+
const revalidateField = useCallback(
|
|
119
|
+
(field: FormInterface, value: unknown) => {
|
|
120
|
+
setErrors((prevErrors) =>
|
|
121
|
+
prevErrors[field.name]
|
|
122
|
+
? { ...prevErrors, [field.name]: getFieldError(field, value) }
|
|
123
|
+
: prevErrors
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
[]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const validateField = useCallback(
|
|
130
|
+
(field: FormInterface, value: unknown) => {
|
|
131
|
+
setErrors((prevErrors) => ({
|
|
132
|
+
...prevErrors,
|
|
133
|
+
[field.name]: getFieldError(field, value),
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
136
|
+
[]
|
|
137
|
+
);
|
|
138
|
+
|
|
112
139
|
/** Gestion des <input> */
|
|
113
140
|
const handleChangeInput = useCallback(
|
|
114
141
|
(
|
|
@@ -116,11 +143,15 @@ export const DynamicForm = forwardRef(
|
|
|
116
143
|
forminterface: FormInterface
|
|
117
144
|
) => {
|
|
118
145
|
const { name, type, checked, value } = event.target;
|
|
119
|
-
const newValue =
|
|
146
|
+
const newValue =
|
|
147
|
+
type === "checkbox"
|
|
148
|
+
? checked
|
|
149
|
+
: (forminterface.formatOnChange?.(value) ?? value);
|
|
120
150
|
|
|
121
151
|
if (forminterface.onChange) {
|
|
122
152
|
forminterface.onChange(newValue, name);
|
|
123
153
|
}
|
|
154
|
+
revalidateField(forminterface, newValue);
|
|
124
155
|
|
|
125
156
|
setValues((prevValues) => {
|
|
126
157
|
const updatedValues = { ...prevValues, [name]: newValue };
|
|
@@ -130,13 +161,14 @@ export const DynamicForm = forwardRef(
|
|
|
130
161
|
return updatedValues;
|
|
131
162
|
});
|
|
132
163
|
},
|
|
133
|
-
[onChange]
|
|
164
|
+
[onChange, revalidateField]
|
|
134
165
|
);
|
|
135
166
|
const handleChangeTextArea = useCallback(
|
|
136
167
|
(name: string, value: string, forminterface: FormInterface) => {
|
|
137
168
|
if (forminterface.onChange) {
|
|
138
169
|
forminterface.onChange(value, name);
|
|
139
170
|
}
|
|
171
|
+
revalidateField(forminterface, value);
|
|
140
172
|
|
|
141
173
|
setValues((prevValues) => {
|
|
142
174
|
const updatedValues = { ...prevValues, [name]: value };
|
|
@@ -146,7 +178,7 @@ export const DynamicForm = forwardRef(
|
|
|
146
178
|
return updatedValues;
|
|
147
179
|
});
|
|
148
180
|
},
|
|
149
|
-
[onChange]
|
|
181
|
+
[onChange, revalidateField]
|
|
150
182
|
);
|
|
151
183
|
/** Gestion des <select> */
|
|
152
184
|
const handleChangeSelect = useCallback(
|
|
@@ -171,14 +203,31 @@ export const DynamicForm = forwardRef(
|
|
|
171
203
|
const handleSubmit = useCallback(
|
|
172
204
|
(e: React.FormEvent<HTMLFormElement>) => {
|
|
173
205
|
e.preventDefault();
|
|
206
|
+
const submitErrors = formFields.reduce<
|
|
207
|
+
Record<string, string | undefined>
|
|
208
|
+
>((fieldErrors, field) => {
|
|
209
|
+
fieldErrors[field.name] = getFieldError(field, values[field.name]);
|
|
210
|
+
return fieldErrors;
|
|
211
|
+
}, {});
|
|
212
|
+
setErrors(submitErrors);
|
|
213
|
+
if (Object.values(submitErrors).some(Boolean)) {
|
|
214
|
+
const firstErrorField = formFields.find(
|
|
215
|
+
(field) => submitErrors[field.name]
|
|
216
|
+
);
|
|
217
|
+
if (firstErrorField) {
|
|
218
|
+
inputRefs.current[firstErrorField.name]?.focus();
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
174
222
|
if (onSubmit) {
|
|
175
223
|
onSubmit(values);
|
|
176
224
|
}
|
|
177
225
|
},
|
|
178
|
-
[onSubmit, values]
|
|
226
|
+
[onSubmit, values, formFields]
|
|
179
227
|
);
|
|
180
228
|
|
|
181
229
|
const resetForm = () => {
|
|
230
|
+
setErrors({});
|
|
182
231
|
setValues(() => {
|
|
183
232
|
const resetValues: FormValuesInterface<T> =
|
|
184
233
|
{} as FormValuesInterface<T>;
|
|
@@ -320,7 +369,8 @@ export const DynamicForm = forwardRef(
|
|
|
320
369
|
>
|
|
321
370
|
<div
|
|
322
371
|
className={`d-flex flex-column ${v.className ?? ""
|
|
323
|
-
} form-group form-group-${v.type}
|
|
372
|
+
} form-group form-group-${v.type} ${errors[v.name] ? "form-group--error" : ""
|
|
373
|
+
}`}
|
|
324
374
|
>
|
|
325
375
|
{v.label && <label>{v.label}</label>}
|
|
326
376
|
{v.icon && <Icon classIcon={v.icon} />}
|
|
@@ -351,12 +401,16 @@ export const DynamicForm = forwardRef(
|
|
|
351
401
|
required={required}
|
|
352
402
|
allowManualEntry
|
|
353
403
|
manualEntryLabel={v.addressSearch?.manualEntryLabel}
|
|
354
|
-
onChange={(value) =>
|
|
355
|
-
|
|
356
|
-
|
|
404
|
+
onChange={(value) => {
|
|
405
|
+
revalidateField(v, value);
|
|
406
|
+
updateValues({ [v.name]: value } as FormValuesInterface<T>);
|
|
407
|
+
}}
|
|
357
408
|
onSelect={(address) => {
|
|
358
409
|
const mappedValues =
|
|
359
410
|
v.addressSearch?.mapAddress?.(address) ?? {};
|
|
411
|
+
if (v.name in mappedValues) {
|
|
412
|
+
revalidateField(v, mappedValues[v.name]);
|
|
413
|
+
}
|
|
360
414
|
updateValues(mappedValues as FormValuesInterface<T>);
|
|
361
415
|
v.addressSearch?.onSelect?.(address);
|
|
362
416
|
}}
|
|
@@ -431,9 +485,15 @@ export const DynamicForm = forwardRef(
|
|
|
431
485
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
432
486
|
handleChangeInput(e, v)
|
|
433
487
|
}
|
|
488
|
+
onBlur={(e: React.FocusEvent<HTMLInputElement>) =>
|
|
489
|
+
validateField(v, e.target.value)
|
|
490
|
+
}
|
|
434
491
|
onInvalid={v.onInvalid ? v.onInvalid : undefined}
|
|
435
492
|
/>
|
|
436
493
|
)}
|
|
494
|
+
{mode !== "read" && errors[v.name] && (
|
|
495
|
+
<small className="form-error">{errors[v.name]}</small>
|
|
496
|
+
)}
|
|
437
497
|
</div>
|
|
438
498
|
</FormFieldContainer>
|
|
439
499
|
);
|