@quatrain/ux-taxonomy 1.0.1
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/README.md +41 -0
- package/dist/TaxonomyController.d.ts +184 -0
- package/dist/TaxonomyController.js +318 -0
- package/dist/ThematicBadgeGroup.d.ts +22 -0
- package/dist/ThematicBadgeGroup.js +29 -0
- package/dist/ThematicTree.d.ts +23 -0
- package/dist/ThematicTree.js +100 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/package.json +45 -0
- package/src/TaxonomyController.test.ts +151 -0
- package/src/TaxonomyController.ts +373 -0
- package/src/ThematicBadgeGroup.tsx +73 -0
- package/src/ThematicTree.tsx +230 -0
- package/src/index.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @quatrain/ux-taxonomy
|
|
2
|
+
|
|
3
|
+
Headless taxonomy controller and interactive React Mantine components for managing thematic trees, categories, and transversal tagging.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Framework-Agnostic Headless Controller**: `TaxonomyController` handles hierarchical nodes, single/multi-selection, expansions, and listener notifications with zero external framework dependencies.
|
|
8
|
+
- **ThematicTree Component**: Accessible, interactive Mantine-based tree view with live filtering, badge counts, and add-subthematic triggers.
|
|
9
|
+
- **ThematicBadgeGroup Component**: Interactive badge cluster for managing multi-thematic tag associations.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yarn add @quatrain/ux-taxonomy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { TaxonomyController, ThematicTree } from '@quatrain/ux-taxonomy'
|
|
21
|
+
|
|
22
|
+
const controller = new TaxonomyController({
|
|
23
|
+
initialNodes: [
|
|
24
|
+
{
|
|
25
|
+
id: 'soil-health',
|
|
26
|
+
label: 'Soil Health & Biology',
|
|
27
|
+
children: [{ id: 'mycorrhizae', label: 'Mycorrhizae' }]
|
|
28
|
+
},
|
|
29
|
+
{ id: 'cover-crops', label: 'Cover Crops' }
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export function MyThematicExplorer() {
|
|
34
|
+
return (
|
|
35
|
+
<ThematicTree
|
|
36
|
+
controller={controller}
|
|
37
|
+
onSelect={(node) => console.log('Selected:', node.id)}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface representing a node within a taxonomy or thematic hierarchy tree.
|
|
3
|
+
*/
|
|
4
|
+
export interface TaxonomyNode {
|
|
5
|
+
/** Unique slug identifier for the thematic node (e.g., 'soil-health') */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Human-readable label displayed in the UI */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Short description explaining the thematic domain */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Parent thematic identifier, or null for root-level categories */
|
|
12
|
+
parentId?: string | null;
|
|
13
|
+
/** Optional icon identifier from Tabler Icons or UI icon registry */
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** Accent color token or hex string */
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Number of curated documents associated with this thematic */
|
|
18
|
+
count?: number;
|
|
19
|
+
/** Child thematic nodes */
|
|
20
|
+
children?: TaxonomyNode[];
|
|
21
|
+
/** Arbitrary domain metadata */
|
|
22
|
+
metadata?: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options for initializing a TaxonomyController instance.
|
|
26
|
+
*/
|
|
27
|
+
export interface TaxonomyControllerOptions {
|
|
28
|
+
/** Initial tree or flat list of taxonomy nodes */
|
|
29
|
+
initialNodes?: TaxonomyNode[];
|
|
30
|
+
/** Initially selected thematic identifier */
|
|
31
|
+
selectedId?: string | null;
|
|
32
|
+
/** If true, allows multiple simultaneous thematic selections */
|
|
33
|
+
multiSelect?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type TaxonomyListener = (nodes: TaxonomyNode[], selectedIds: string[]) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Headless, framework-agnostic controller managing hierarchical thematics,
|
|
38
|
+
* category trees, transversal tag taxonomies, and active filter selections.
|
|
39
|
+
*/
|
|
40
|
+
export declare class TaxonomyController {
|
|
41
|
+
protected nodes: Map<string, TaxonomyNode>;
|
|
42
|
+
protected selectedIds: Set<string>;
|
|
43
|
+
protected expandedIds: Set<string>;
|
|
44
|
+
protected facetFilters: Map<string, Set<string>>;
|
|
45
|
+
protected listeners: Set<TaxonomyListener>;
|
|
46
|
+
protected multiSelect: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Instantiates a new TaxonomyController.
|
|
49
|
+
*
|
|
50
|
+
* @param options - Configuration options.
|
|
51
|
+
*/
|
|
52
|
+
constructor(options?: TaxonomyControllerOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Loads or replaces nodes in the taxonomy manager.
|
|
55
|
+
*
|
|
56
|
+
* @param nodes - Array of hierarchical or flat taxonomy nodes.
|
|
57
|
+
*/
|
|
58
|
+
loadNodes(nodes: TaxonomyNode[]): void;
|
|
59
|
+
/**
|
|
60
|
+
* Adds or updates a thematic node in the taxonomy.
|
|
61
|
+
*
|
|
62
|
+
* @param node - The thematic node to insert or update.
|
|
63
|
+
*/
|
|
64
|
+
addNode(node: TaxonomyNode): void;
|
|
65
|
+
/**
|
|
66
|
+
* Removes a node and recursively removes its sub-thematics.
|
|
67
|
+
*
|
|
68
|
+
* @param id - The identifier of the node to remove.
|
|
69
|
+
*/
|
|
70
|
+
removeNode(id: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Selects a thematic node. Clears previous selection unless multiSelect is enabled.
|
|
73
|
+
*
|
|
74
|
+
* @param id - The node identifier to select.
|
|
75
|
+
*/
|
|
76
|
+
select(id: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Deselects a thematic node.
|
|
79
|
+
*
|
|
80
|
+
* @param id - The node identifier to deselect.
|
|
81
|
+
*/
|
|
82
|
+
deselect(id: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Toggles the selection status of a thematic node.
|
|
85
|
+
*
|
|
86
|
+
* @param id - The node identifier to toggle.
|
|
87
|
+
*/
|
|
88
|
+
toggleSelect(id: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* Clears all active selections.
|
|
91
|
+
*/
|
|
92
|
+
clearSelection(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Retrieves the set of currently selected thematic node identifiers.
|
|
95
|
+
*
|
|
96
|
+
* @returns Array of selected IDs.
|
|
97
|
+
*/
|
|
98
|
+
getSelected(): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Checks whether a specific thematic node is currently selected.
|
|
101
|
+
*
|
|
102
|
+
* @param id - The thematic identifier to verify.
|
|
103
|
+
* @returns True if selected, false otherwise.
|
|
104
|
+
*/
|
|
105
|
+
isSelected(id: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Toggles the expansion collapse state of a parent thematic node.
|
|
108
|
+
*
|
|
109
|
+
* @param id - The thematic identifier to toggle.
|
|
110
|
+
*/
|
|
111
|
+
toggleExpand(id: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* Checks whether a thematic node is currently expanded.
|
|
114
|
+
*
|
|
115
|
+
* @param id - The thematic identifier to check.
|
|
116
|
+
* @returns True if expanded, false otherwise.
|
|
117
|
+
*/
|
|
118
|
+
isExpanded(id: string): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Reconstructs and returns the full hierarchical taxonomy tree.
|
|
121
|
+
*
|
|
122
|
+
* @returns Array of root-level TaxonomyNodes with nested children.
|
|
123
|
+
*/
|
|
124
|
+
getTree(): TaxonomyNode[];
|
|
125
|
+
/**
|
|
126
|
+
* Returns a flat array of all registered taxonomy nodes.
|
|
127
|
+
*
|
|
128
|
+
* @returns Array of TaxonomyNode objects.
|
|
129
|
+
*/
|
|
130
|
+
getFlatNodes(): TaxonomyNode[];
|
|
131
|
+
/**
|
|
132
|
+
* Finds a specific thematic node by identifier.
|
|
133
|
+
*
|
|
134
|
+
* @param id - The node identifier to locate.
|
|
135
|
+
* @returns The TaxonomyNode or undefined if not found.
|
|
136
|
+
*/
|
|
137
|
+
getNode(id: string): TaxonomyNode | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Updates the document counter for a thematic node.
|
|
140
|
+
*
|
|
141
|
+
* @param id - The thematic identifier.
|
|
142
|
+
* @param count - The updated count.
|
|
143
|
+
*/
|
|
144
|
+
updateCount(id: string, count: number): void;
|
|
145
|
+
/**
|
|
146
|
+
* Subscribes a listener callback to state changes.
|
|
147
|
+
*
|
|
148
|
+
* @param listener - Callback receiving updated nodes and active selection.
|
|
149
|
+
* @returns Unsubscribe function.
|
|
150
|
+
*/
|
|
151
|
+
subscribe(listener: TaxonomyListener): () => void;
|
|
152
|
+
/**
|
|
153
|
+
* Sets or updates active filter values for a specific multi-axial facet axis (e.g., 'soils', 'climates').
|
|
154
|
+
*
|
|
155
|
+
* @param axis - The facet axis identifier (e.g. 'soils', 'climates', 'itineraries').
|
|
156
|
+
* @param values - Array of selected facet values.
|
|
157
|
+
*/
|
|
158
|
+
setFacetFilter(axis: string, values: string[]): void;
|
|
159
|
+
/**
|
|
160
|
+
* Retrieves active filter values for a specific facet axis.
|
|
161
|
+
*
|
|
162
|
+
* @param axis - The facet axis identifier.
|
|
163
|
+
* @returns Array of active facet values.
|
|
164
|
+
*/
|
|
165
|
+
getFacetFilter(axis: string): string[];
|
|
166
|
+
/**
|
|
167
|
+
* Retrieves all active multi-axial facet filters.
|
|
168
|
+
*
|
|
169
|
+
* @returns Key-value map of axis name to selected filter values array.
|
|
170
|
+
*/
|
|
171
|
+
getAllFacetFilters(): Record<string, string[]>;
|
|
172
|
+
/**
|
|
173
|
+
* Clears all active multi-axial facet filters.
|
|
174
|
+
*/
|
|
175
|
+
clearFacetFilters(): void;
|
|
176
|
+
/**
|
|
177
|
+
* Tests whether an item's multi-axial facets match the currently active filters.
|
|
178
|
+
*
|
|
179
|
+
* @param itemFacets - Document facet metadata (e.g. { soils: ['argilo-calcaire'], climates: ['mediterraneen'] }).
|
|
180
|
+
* @returns True if the item satisfies all active facet criteria.
|
|
181
|
+
*/
|
|
182
|
+
matchesFilters(itemFacets: Record<string, any>): boolean;
|
|
183
|
+
protected notify(): void;
|
|
184
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaxonomyController = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Headless, framework-agnostic controller managing hierarchical thematics,
|
|
6
|
+
* category trees, transversal tag taxonomies, and active filter selections.
|
|
7
|
+
*/
|
|
8
|
+
class TaxonomyController {
|
|
9
|
+
nodes = new Map();
|
|
10
|
+
selectedIds = new Set();
|
|
11
|
+
expandedIds = new Set();
|
|
12
|
+
facetFilters = new Map();
|
|
13
|
+
listeners = new Set();
|
|
14
|
+
multiSelect;
|
|
15
|
+
/**
|
|
16
|
+
* Instantiates a new TaxonomyController.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Configuration options.
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.multiSelect = Boolean(options.multiSelect);
|
|
22
|
+
if (options.initialNodes && options.initialNodes.length > 0) {
|
|
23
|
+
this.loadNodes(options.initialNodes);
|
|
24
|
+
}
|
|
25
|
+
if (options.selectedId) {
|
|
26
|
+
this.selectedIds.add(options.selectedId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Loads or replaces nodes in the taxonomy manager.
|
|
31
|
+
*
|
|
32
|
+
* @param nodes - Array of hierarchical or flat taxonomy nodes.
|
|
33
|
+
*/
|
|
34
|
+
loadNodes(nodes) {
|
|
35
|
+
this.nodes.clear();
|
|
36
|
+
const flatten = (items, parentId = null) => {
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const node = {
|
|
39
|
+
...item,
|
|
40
|
+
parentId: item.parentId !== undefined ? item.parentId : parentId,
|
|
41
|
+
children: []
|
|
42
|
+
};
|
|
43
|
+
this.nodes.set(node.id, node);
|
|
44
|
+
if (item.children && item.children.length > 0) {
|
|
45
|
+
flatten(item.children, item.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
flatten(nodes);
|
|
50
|
+
this.notify();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Adds or updates a thematic node in the taxonomy.
|
|
54
|
+
*
|
|
55
|
+
* @param node - The thematic node to insert or update.
|
|
56
|
+
*/
|
|
57
|
+
addNode(node) {
|
|
58
|
+
this.nodes.set(node.id, {
|
|
59
|
+
...node,
|
|
60
|
+
children: []
|
|
61
|
+
});
|
|
62
|
+
this.notify();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Removes a node and recursively removes its sub-thematics.
|
|
66
|
+
*
|
|
67
|
+
* @param id - The identifier of the node to remove.
|
|
68
|
+
*/
|
|
69
|
+
removeNode(id) {
|
|
70
|
+
const childrenToRemove = [];
|
|
71
|
+
const findDescendants = (parentId) => {
|
|
72
|
+
for (const [nodeId, node] of this.nodes.entries()) {
|
|
73
|
+
if (node.parentId === parentId) {
|
|
74
|
+
childrenToRemove.push(nodeId);
|
|
75
|
+
findDescendants(nodeId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
findDescendants(id);
|
|
80
|
+
childrenToRemove.push(id);
|
|
81
|
+
for (const childId of childrenToRemove) {
|
|
82
|
+
this.nodes.delete(childId);
|
|
83
|
+
this.selectedIds.delete(childId);
|
|
84
|
+
this.expandedIds.delete(childId);
|
|
85
|
+
}
|
|
86
|
+
this.notify();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Selects a thematic node. Clears previous selection unless multiSelect is enabled.
|
|
90
|
+
*
|
|
91
|
+
* @param id - The node identifier to select.
|
|
92
|
+
*/
|
|
93
|
+
select(id) {
|
|
94
|
+
if (!this.multiSelect) {
|
|
95
|
+
this.selectedIds.clear();
|
|
96
|
+
}
|
|
97
|
+
this.selectedIds.add(id);
|
|
98
|
+
this.notify();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Deselects a thematic node.
|
|
102
|
+
*
|
|
103
|
+
* @param id - The node identifier to deselect.
|
|
104
|
+
*/
|
|
105
|
+
deselect(id) {
|
|
106
|
+
this.selectedIds.delete(id);
|
|
107
|
+
this.notify();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Toggles the selection status of a thematic node.
|
|
111
|
+
*
|
|
112
|
+
* @param id - The node identifier to toggle.
|
|
113
|
+
*/
|
|
114
|
+
toggleSelect(id) {
|
|
115
|
+
if (this.selectedIds.has(id)) {
|
|
116
|
+
this.deselect(id);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.select(id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Clears all active selections.
|
|
124
|
+
*/
|
|
125
|
+
clearSelection() {
|
|
126
|
+
this.selectedIds.clear();
|
|
127
|
+
this.notify();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Retrieves the set of currently selected thematic node identifiers.
|
|
131
|
+
*
|
|
132
|
+
* @returns Array of selected IDs.
|
|
133
|
+
*/
|
|
134
|
+
getSelected() {
|
|
135
|
+
return Array.from(this.selectedIds);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Checks whether a specific thematic node is currently selected.
|
|
139
|
+
*
|
|
140
|
+
* @param id - The thematic identifier to verify.
|
|
141
|
+
* @returns True if selected, false otherwise.
|
|
142
|
+
*/
|
|
143
|
+
isSelected(id) {
|
|
144
|
+
return this.selectedIds.has(id);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Toggles the expansion collapse state of a parent thematic node.
|
|
148
|
+
*
|
|
149
|
+
* @param id - The thematic identifier to toggle.
|
|
150
|
+
*/
|
|
151
|
+
toggleExpand(id) {
|
|
152
|
+
if (this.expandedIds.has(id)) {
|
|
153
|
+
this.expandedIds.delete(id);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.expandedIds.add(id);
|
|
157
|
+
}
|
|
158
|
+
this.notify();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Checks whether a thematic node is currently expanded.
|
|
162
|
+
*
|
|
163
|
+
* @param id - The thematic identifier to check.
|
|
164
|
+
* @returns True if expanded, false otherwise.
|
|
165
|
+
*/
|
|
166
|
+
isExpanded(id) {
|
|
167
|
+
return this.expandedIds.has(id);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reconstructs and returns the full hierarchical taxonomy tree.
|
|
171
|
+
*
|
|
172
|
+
* @returns Array of root-level TaxonomyNodes with nested children.
|
|
173
|
+
*/
|
|
174
|
+
getTree() {
|
|
175
|
+
const rootNodes = [];
|
|
176
|
+
const nodeMap = new Map();
|
|
177
|
+
for (const [id, node] of this.nodes.entries()) {
|
|
178
|
+
nodeMap.set(id, { ...node, children: [] });
|
|
179
|
+
}
|
|
180
|
+
for (const node of nodeMap.values()) {
|
|
181
|
+
if (node.parentId && nodeMap.has(node.parentId)) {
|
|
182
|
+
const parent = nodeMap.get(node.parentId);
|
|
183
|
+
parent.children = parent.children || [];
|
|
184
|
+
parent.children.push(node);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
rootNodes.push(node);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return rootNodes;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns a flat array of all registered taxonomy nodes.
|
|
194
|
+
*
|
|
195
|
+
* @returns Array of TaxonomyNode objects.
|
|
196
|
+
*/
|
|
197
|
+
getFlatNodes() {
|
|
198
|
+
return Array.from(this.nodes.values());
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Finds a specific thematic node by identifier.
|
|
202
|
+
*
|
|
203
|
+
* @param id - The node identifier to locate.
|
|
204
|
+
* @returns The TaxonomyNode or undefined if not found.
|
|
205
|
+
*/
|
|
206
|
+
getNode(id) {
|
|
207
|
+
return this.nodes.get(id);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Updates the document counter for a thematic node.
|
|
211
|
+
*
|
|
212
|
+
* @param id - The thematic identifier.
|
|
213
|
+
* @param count - The updated count.
|
|
214
|
+
*/
|
|
215
|
+
updateCount(id, count) {
|
|
216
|
+
const existing = this.nodes.get(id);
|
|
217
|
+
if (existing) {
|
|
218
|
+
existing.count = count;
|
|
219
|
+
this.notify();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Subscribes a listener callback to state changes.
|
|
224
|
+
*
|
|
225
|
+
* @param listener - Callback receiving updated nodes and active selection.
|
|
226
|
+
* @returns Unsubscribe function.
|
|
227
|
+
*/
|
|
228
|
+
subscribe(listener) {
|
|
229
|
+
this.listeners.add(listener);
|
|
230
|
+
listener(this.getTree(), this.getSelected());
|
|
231
|
+
return () => {
|
|
232
|
+
this.listeners.delete(listener);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Sets or updates active filter values for a specific multi-axial facet axis (e.g., 'soils', 'climates').
|
|
237
|
+
*
|
|
238
|
+
* @param axis - The facet axis identifier (e.g. 'soils', 'climates', 'itineraries').
|
|
239
|
+
* @param values - Array of selected facet values.
|
|
240
|
+
*/
|
|
241
|
+
setFacetFilter(axis, values) {
|
|
242
|
+
if (!values || values.length === 0) {
|
|
243
|
+
this.facetFilters.delete(axis);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this.facetFilters.set(axis, new Set(values));
|
|
247
|
+
}
|
|
248
|
+
this.notify();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Retrieves active filter values for a specific facet axis.
|
|
252
|
+
*
|
|
253
|
+
* @param axis - The facet axis identifier.
|
|
254
|
+
* @returns Array of active facet values.
|
|
255
|
+
*/
|
|
256
|
+
getFacetFilter(axis) {
|
|
257
|
+
const set = this.facetFilters.get(axis);
|
|
258
|
+
return set ? Array.from(set) : [];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Retrieves all active multi-axial facet filters.
|
|
262
|
+
*
|
|
263
|
+
* @returns Key-value map of axis name to selected filter values array.
|
|
264
|
+
*/
|
|
265
|
+
getAllFacetFilters() {
|
|
266
|
+
const res = {};
|
|
267
|
+
for (const [axis, set] of this.facetFilters.entries()) {
|
|
268
|
+
if (set.size > 0) {
|
|
269
|
+
res[axis] = Array.from(set);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return res;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Clears all active multi-axial facet filters.
|
|
276
|
+
*/
|
|
277
|
+
clearFacetFilters() {
|
|
278
|
+
this.facetFilters.clear();
|
|
279
|
+
this.notify();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Tests whether an item's multi-axial facets match the currently active filters.
|
|
283
|
+
*
|
|
284
|
+
* @param itemFacets - Document facet metadata (e.g. { soils: ['argilo-calcaire'], climates: ['mediterraneen'] }).
|
|
285
|
+
* @returns True if the item satisfies all active facet criteria.
|
|
286
|
+
*/
|
|
287
|
+
matchesFilters(itemFacets) {
|
|
288
|
+
// 1. Check thematic selection
|
|
289
|
+
if (this.selectedIds.size > 0) {
|
|
290
|
+
const selected = Array.from(this.selectedIds);
|
|
291
|
+
const itemThematics = Array.isArray(itemFacets.thematics) ? itemFacets.thematics : [itemFacets.category].filter(Boolean);
|
|
292
|
+
const matchesThematic = selected.some(s => itemThematics.includes(s) || itemFacets.category === s);
|
|
293
|
+
if (!matchesThematic)
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
// 2. Check each multi-axial facet
|
|
297
|
+
for (const [axis, filterSet] of this.facetFilters.entries()) {
|
|
298
|
+
if (filterSet.size === 0)
|
|
299
|
+
continue;
|
|
300
|
+
const itemVals = itemFacets[axis];
|
|
301
|
+
if (!itemVals)
|
|
302
|
+
return false;
|
|
303
|
+
const itemValArray = Array.isArray(itemVals) ? itemVals : [itemVals];
|
|
304
|
+
const hasMatch = itemValArray.some((v) => filterSet.has(v));
|
|
305
|
+
if (!hasMatch)
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
notify() {
|
|
311
|
+
const tree = this.getTree();
|
|
312
|
+
const selected = this.getSelected();
|
|
313
|
+
for (const listener of this.listeners) {
|
|
314
|
+
listener(tree, selected);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
exports.TaxonomyController = TaxonomyController;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TaxonomyNode } from './TaxonomyController';
|
|
3
|
+
export interface ThematicBadgeGroupProps {
|
|
4
|
+
/** Available taxonomy nodes / thematics */
|
|
5
|
+
nodes: TaxonomyNode[];
|
|
6
|
+
/** Array of currently selected thematic slugs/ids */
|
|
7
|
+
value: string[];
|
|
8
|
+
/** Callback fired when the selection changes */
|
|
9
|
+
onChange: (selectedIds: string[]) => void;
|
|
10
|
+
/** Label displayed above the badges */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Description or subtext */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Custom CSS class */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Custom styles */
|
|
17
|
+
style?: React.CSSProperties;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Interactive badge group component allowing multi-selection of transversal thematics.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ThematicBadgeGroup: React.FC<ThematicBadgeGroupProps>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ThematicBadgeGroup = void 0;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const core_1 = require("@mantine/core");
|
|
9
|
+
/**
|
|
10
|
+
* Interactive badge group component allowing multi-selection of transversal thematics.
|
|
11
|
+
*/
|
|
12
|
+
const ThematicBadgeGroup = ({ nodes, value = [], onChange, label, description, className = '', style }) => {
|
|
13
|
+
const handleToggle = (id) => {
|
|
14
|
+
if (value.includes(id)) {
|
|
15
|
+
onChange(value.filter((v) => v !== id));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
onChange([...value, id]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return (react_1.default.createElement(core_1.Box, { className: `q-thematic-badge-group ${className}`, style: style },
|
|
22
|
+
label && (react_1.default.createElement(core_1.Text, { size: "sm", fw: 500, mb: 2 }, label)),
|
|
23
|
+
description && (react_1.default.createElement(core_1.Text, { size: "xs", c: "dimmed", mb: "xs" }, description)),
|
|
24
|
+
react_1.default.createElement(core_1.Group, { gap: "xs" }, nodes.map((node) => {
|
|
25
|
+
const isSelected = value.includes(node.id);
|
|
26
|
+
return (react_1.default.createElement(core_1.Badge, { key: node.id, size: "md", variant: isSelected ? 'filled' : 'outline', color: node.color || (isSelected ? 'blue' : 'gray'), style: { cursor: 'pointer', userSelect: 'none' }, onClick: () => handleToggle(node.id) }, node.label));
|
|
27
|
+
}))));
|
|
28
|
+
};
|
|
29
|
+
exports.ThematicBadgeGroup = ThematicBadgeGroup;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TaxonomyController, type TaxonomyNode } from './TaxonomyController';
|
|
3
|
+
/**
|
|
4
|
+
* Properties for rendering the ThematicTree component.
|
|
5
|
+
*/
|
|
6
|
+
export interface ThematicTreeProps {
|
|
7
|
+
/** The taxonomy controller managing state */
|
|
8
|
+
controller: TaxonomyController;
|
|
9
|
+
/** Optional callback fired when a node is selected */
|
|
10
|
+
onSelect?: (node: TaxonomyNode) => void;
|
|
11
|
+
/** Optional callback fired when the 'Add Sub-thematic' button is clicked */
|
|
12
|
+
onAddSubThematic?: (parentNode?: TaxonomyNode) => void;
|
|
13
|
+
/** Whether to show a live search filter input on top */
|
|
14
|
+
searchable?: boolean;
|
|
15
|
+
/** Custom CSS class */
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Custom styles */
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tree component rendering interactive taxonomic hierarchies with Mantine styling.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ThematicTree: React.FC<ThematicTreeProps>;
|