@arbetsformedlingen/yrkesvaljaren 2.0.5
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/LICENSE +190 -0
- package/README.md +369 -0
- package/dist/cjs/af-validation-message-BzY5H0xU.js +16 -0
- package/dist/cjs/core-selector.cjs.entry.js +476 -0
- package/dist/cjs/digi-service-job-selector.cjs.entry.js +1619 -0
- package/dist/cjs/fuse-BTLNKxuL.js +1631 -0
- package/dist/cjs/geo-valjaren.cjs.entry.js +14910 -0
- package/dist/cjs/index-BOuTGlEp.js +1817 -0
- package/dist/cjs/index.cjs.js +14 -0
- package/dist/cjs/job-selector-demo.cjs.entry.js +692 -0
- package/dist/cjs/loader.cjs.js +12 -0
- package/dist/cjs/neobrutal-search.cjs.entry.js +171 -0
- package/dist/cjs/search.engine-D0cMelRi.js +489 -0
- package/dist/cjs/taxonomy-registry-DVmuDUpM.js +435 -0
- package/dist/cjs/types-CtHBl6Ra.js +14 -0
- package/dist/cjs/yrkesvaljaren.cjs.js +24 -0
- package/dist/collection/collection-manifest.json +15 -0
- package/dist/collection/components/core-selector/core-selector.css +803 -0
- package/dist/collection/components/core-selector/core-selector.js +761 -0
- package/dist/collection/components/core-selector/types.js +1 -0
- package/dist/collection/components/design-compat/af-icon.js +13 -0
- package/dist/collection/components/design-compat/af-label.js +2 -0
- package/dist/collection/components/design-compat/af-tag.js +6 -0
- package/dist/collection/components/design-compat/af-validation-message.js +11 -0
- package/dist/collection/components/design-compat/index.js +4 -0
- package/dist/collection/components/geovaljaren/geovaljaren.css +21 -0
- package/dist/collection/components/geovaljaren/geovaljaren.js +304 -0
- package/dist/collection/components/geovaljaren/logic/adapter.utils.js +20 -0
- package/dist/collection/components/geovaljaren/logic/data.utils.js +12 -0
- package/dist/collection/components/geovaljaren/logic/history-service.js +25 -0
- package/dist/collection/components/geovaljaren/logic/search.engine.js +28 -0
- package/dist/collection/components/job-selector/constants.js +26 -0
- package/dist/collection/components/job-selector/history-service.js +41 -0
- package/dist/collection/components/job-selector/job-selector-controller.js +835 -0
- package/dist/collection/components/job-selector/job-selector.css +318 -0
- package/dist/collection/components/job-selector/job-selector.js +1378 -0
- package/dist/collection/components/job-selector/keyboard-handlers.js +84 -0
- package/dist/collection/components/job-selector/logic/adapter.utils.js +75 -0
- package/dist/collection/components/job-selector/logic/label-formatter.js +118 -0
- package/dist/collection/components/job-selector/logic/search.engine.js +50 -0
- package/dist/collection/components/job-selector/search-logic.js +35 -0
- package/dist/collection/components/job-selector/search.utils.js +224 -0
- package/dist/collection/components/job-selector/suggestions-list.js +31 -0
- package/dist/collection/components/job-selector/taxonomy-registry.js +60 -0
- package/dist/collection/components/job-selector/test-data.mock.js +33 -0
- package/dist/collection/components/job-selector/types.js +10 -0
- package/dist/collection/components/job-selector/validation-message.js +24 -0
- package/dist/collection/demo-data.js +20 -0
- package/dist/collection/index.js +7 -0
- package/dist/collection/verkstad/verkstad.js +407 -0
- package/dist/components/core-selector.d.ts +11 -0
- package/dist/components/core-selector.js +1 -0
- package/dist/components/core-selector2.js +1 -0
- package/dist/components/digi-service-job-selector.d.ts +11 -0
- package/dist/components/digi-service-job-selector.js +1 -0
- package/dist/components/fuse.js +1 -0
- package/dist/components/geo-valjaren.d.ts +11 -0
- package/dist/components/geo-valjaren.js +1 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +1 -0
- package/dist/components/job-selector-demo.d.ts +11 -0
- package/dist/components/job-selector-demo.js +1 -0
- package/dist/components/job-selector.js +1 -0
- package/dist/components/neobrutal-search.d.ts +11 -0
- package/dist/components/neobrutal-search.js +1 -0
- package/dist/components/search.engine.js +1 -0
- package/dist/components/types.js +1 -0
- package/dist/esm/af-validation-message-F8NIH2rC.js +14 -0
- package/dist/esm/core-selector.entry.js +474 -0
- package/dist/esm/digi-service-job-selector.entry.js +1617 -0
- package/dist/esm/fuse-BPnhMTul.js +1629 -0
- package/dist/esm/geo-valjaren.entry.js +14908 -0
- package/dist/esm/index-CGXZllj2.js +1806 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/job-selector-demo.entry.js +690 -0
- package/dist/esm/loader.js +10 -0
- package/dist/esm/neobrutal-search.entry.js +169 -0
- package/dist/esm/search.engine-nZ7AcXba.js +479 -0
- package/dist/esm/taxonomy-registry-Dw0VR9Aw.js +430 -0
- package/dist/esm/types-DHUftoZJ.js +12 -0
- package/dist/esm/yrkesvaljaren.js +20 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/types/components/core-selector/core-selector.d.ts +46 -0
- package/dist/types/components/core-selector/types.d.ts +1 -0
- package/dist/types/components/design-compat/af-icon.d.ts +7 -0
- package/dist/types/components/design-compat/af-label.d.ts +8 -0
- package/dist/types/components/design-compat/af-tag.d.ts +8 -0
- package/dist/types/components/design-compat/af-validation-message.d.ts +9 -0
- package/dist/types/components/design-compat/index.d.ts +5 -0
- package/dist/types/components/geovaljaren/geovaljaren.d.ts +24 -0
- package/dist/types/components/geovaljaren/logic/adapter.utils.d.ts +7 -0
- package/dist/types/components/geovaljaren/logic/data.utils.d.ts +7 -0
- package/dist/types/components/geovaljaren/logic/history-service.d.ts +6 -0
- package/dist/types/components/geovaljaren/logic/search.engine.d.ts +7 -0
- package/dist/types/components/job-selector/ambient.d.ts +4 -0
- package/dist/types/components/job-selector/constants.d.ts +13 -0
- package/dist/types/components/job-selector/history-service.d.ts +6 -0
- package/dist/types/components/job-selector/job-selector-controller.d.ts +82 -0
- package/dist/types/components/job-selector/job-selector.d.ts +140 -0
- package/dist/types/components/job-selector/keyboard-handlers.d.ts +13 -0
- package/dist/types/components/job-selector/logic/adapter.utils.d.ts +12 -0
- package/dist/types/components/job-selector/logic/label-formatter.d.ts +4 -0
- package/dist/types/components/job-selector/logic/search.engine.d.ts +21 -0
- package/dist/types/components/job-selector/search-logic.d.ts +17 -0
- package/dist/types/components/job-selector/search.utils.d.ts +10 -0
- package/dist/types/components/job-selector/suggestions-list.d.ts +13 -0
- package/dist/types/components/job-selector/taxonomy-registry.d.ts +20 -0
- package/dist/types/components/job-selector/test-data.mock.d.ts +7 -0
- package/dist/types/components/job-selector/types.d.ts +107 -0
- package/dist/types/components/job-selector/validation-message.d.ts +8 -0
- package/dist/types/components/job-selector-demo/job-selector-demo.d.ts +55 -0
- package/dist/types/components/neobrutal-search/neobrutal-search.d.ts +18 -0
- package/dist/types/components.d.ts +425 -0
- package/dist/types/demo-data.d.ts +16 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/stencil-public-runtime.d.ts +1873 -0
- package/dist/types/verkstad/verkstad.d.ts +1 -0
- package/dist/yrkesvaljaren/fuse-B0eHOxow.js.map +1 -0
- package/dist/yrkesvaljaren/index-Cy4BCo5s.js.map +1 -0
- package/dist/yrkesvaljaren/index-DfJXTQqL.js.map +1 -0
- package/dist/yrkesvaljaren/index.esm.js +1 -0
- package/dist/yrkesvaljaren/index.esm.js.map +1 -0
- package/dist/yrkesvaljaren/p-16636162.entry.js +1 -0
- package/dist/yrkesvaljaren/p-273cf6df.entry.js +1 -0
- package/dist/yrkesvaljaren/p-9e48afa4.entry.js +1 -0
- package/dist/yrkesvaljaren/p-BPnhMTul.js +1 -0
- package/dist/yrkesvaljaren/p-CGXZllj2.js +3 -0
- package/dist/yrkesvaljaren/p-DHUftoZJ.js +1 -0
- package/dist/yrkesvaljaren/p-DYFAzvqX.js +1 -0
- package/dist/yrkesvaljaren/p-Dw0VR9Aw.js +1 -0
- package/dist/yrkesvaljaren/p-aef7b5ae.entry.js +1 -0
- package/dist/yrkesvaljaren/p-b0f16abd.entry.js +1 -0
- package/dist/yrkesvaljaren/p-uBrs3sSs.js +1 -0
- package/dist/yrkesvaljaren/search.engine-DPT6roRm.js.map +1 -0
- package/dist/yrkesvaljaren/taxonomy-registry-s5khf6s4.js.map +1 -0
- package/dist/yrkesvaljaren/types-DHUftoZJ.js.map +1 -0
- package/dist/yrkesvaljaren/yrkesvaljaren.css +1 -0
- package/dist/yrkesvaljaren/yrkesvaljaren.esm.js +1 -0
- package/dist/yrkesvaljaren/yrkesvaljaren.esm.js.map +1 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +126 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import { TaxonomyAdapter } from "./logic/adapter.utils";
|
|
2
|
+
import { HistoryService } from "./history-service";
|
|
3
|
+
import { getRegisteredTaxonomy } from "./taxonomy-registry";
|
|
4
|
+
import { segmentsToHtml } from "./search.utils";
|
|
5
|
+
import { DEBOUNCE_TIME, MAX_RESULTS } from "./constants";
|
|
6
|
+
import log from "loglevel";
|
|
7
|
+
// NEW IMPORTS
|
|
8
|
+
import { TaxonomySearchEngine } from "./logic/search.engine";
|
|
9
|
+
import { LabelFormatter } from "./logic/label-formatter";
|
|
10
|
+
export class JobSelectorController {
|
|
11
|
+
callbacks;
|
|
12
|
+
config;
|
|
13
|
+
state = { status: 'loading' };
|
|
14
|
+
taxonomyData = [];
|
|
15
|
+
popularItems = [];
|
|
16
|
+
// REPLACED: private fuseInstance: Fuse<TaxonomyItem> | null = null;
|
|
17
|
+
searchEngine = null;
|
|
18
|
+
debounceTimer;
|
|
19
|
+
validationTimer;
|
|
20
|
+
isTouched = false;
|
|
21
|
+
activeErrorType = null;
|
|
22
|
+
constructor(callbacks, config) {
|
|
23
|
+
this.callbacks = callbacks;
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
// --- PUBLIC API ---
|
|
27
|
+
getState() {
|
|
28
|
+
return this.state;
|
|
29
|
+
}
|
|
30
|
+
getActiveErrorType() {
|
|
31
|
+
return this.activeErrorType;
|
|
32
|
+
}
|
|
33
|
+
// --- CONFIG UPDATE ---
|
|
34
|
+
updateConfig(newConfig) {
|
|
35
|
+
this.config = { ...this.config, ...newConfig };
|
|
36
|
+
// Kör validering om reglerna för obligatoriskt fält eller fritext ändras.
|
|
37
|
+
if (newConfig.required !== undefined || newConfig.requireSelection !== undefined) {
|
|
38
|
+
this.runValidation();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
getTaxonomyData() {
|
|
42
|
+
// Return internal data if already loaded
|
|
43
|
+
if (this.taxonomyData.length > 0)
|
|
44
|
+
return this.taxonomyData;
|
|
45
|
+
// Try to fetch from registry (handles late registration)
|
|
46
|
+
const registeredData = getRegisteredTaxonomy();
|
|
47
|
+
if (registeredData && registeredData.length > 0) {
|
|
48
|
+
log.debug('[Controller] getTaxonomyData - Late initializing from registry');
|
|
49
|
+
this.taxonomyData = registeredData;
|
|
50
|
+
this.popularItems = this.computePopularItems(this.taxonomyData);
|
|
51
|
+
this.searchEngine = new TaxonomySearchEngine(this.taxonomyData);
|
|
52
|
+
}
|
|
53
|
+
return this.taxonomyData;
|
|
54
|
+
}
|
|
55
|
+
destroy() {
|
|
56
|
+
if (this.debounceTimer) {
|
|
57
|
+
clearTimeout(this.debounceTimer);
|
|
58
|
+
this.debounceTimer = undefined;
|
|
59
|
+
}
|
|
60
|
+
if (this.validationTimer) {
|
|
61
|
+
clearTimeout(this.validationTimer);
|
|
62
|
+
this.validationTimer = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// --- INITIALIZATION ---
|
|
66
|
+
async initialize(initialData) {
|
|
67
|
+
log.debug('[Controller] initialize - START', { hasInitialData: !!initialData });
|
|
68
|
+
this.updateState({ status: 'loading' });
|
|
69
|
+
const registeredData = (initialData && initialData.length > 0) ? initialData : getRegisteredTaxonomy();
|
|
70
|
+
if (registeredData && registeredData.length > 0) {
|
|
71
|
+
log.debug('[Controller] initialize - Loaded taxonomy from registry');
|
|
72
|
+
this.setStateToActive(registeredData, 'registry', new Date().toISOString());
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
log.warn('[Controller] initialize - No taxonomy found in registry. Has registerTaxonomy() been called?');
|
|
76
|
+
this.updateState({ status: 'error', message: 'Kunde inte hitta yrkesdata. Kontrollera att komponenten är initierad korrekt.' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
handleFocus() {
|
|
80
|
+
this.isTouched = true;
|
|
81
|
+
}
|
|
82
|
+
setStateToActive(data, source, sourceDate) {
|
|
83
|
+
this.taxonomyData = data;
|
|
84
|
+
this.popularItems = this.computePopularItems(this.taxonomyData);
|
|
85
|
+
// NEW: Initialize the separated SearchEngine
|
|
86
|
+
this.searchEngine = new TaxonomySearchEngine(this.taxonomyData);
|
|
87
|
+
// OLD: this.fuseInstance = new Fuse(this.taxonomyData, FUSE_OPTIONS);
|
|
88
|
+
const commonState = { dataSource: source, dataSourceDate: sourceDate };
|
|
89
|
+
if (this.config.enableMultiple) {
|
|
90
|
+
this.updateState({
|
|
91
|
+
status: 'multiSelect',
|
|
92
|
+
inputValue: '',
|
|
93
|
+
selectedItems: [],
|
|
94
|
+
suggestions: [],
|
|
95
|
+
validation: 'idle',
|
|
96
|
+
...commonState
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.updateState({ status: 'searching', inputValue: '', suggestions: [], validation: 'idle', ...commonState });
|
|
101
|
+
}
|
|
102
|
+
// --- SEARCH ---
|
|
103
|
+
handleInput(value) {
|
|
104
|
+
this.isTouched = true;
|
|
105
|
+
log.debug('[Controller] handleInput - Value:', value);
|
|
106
|
+
// Reset validation on input
|
|
107
|
+
const wasInError = this.state.validation === 'error';
|
|
108
|
+
const validationContext = wasInError || this.state.validationContext === 'from-error' ? 'from-error' : undefined;
|
|
109
|
+
clearTimeout(this.debounceTimer);
|
|
110
|
+
if (value.trim() === '') {
|
|
111
|
+
if (this.config.enableMultiple) {
|
|
112
|
+
const currentSelected = 'selectedItems' in this.state ? this.state.selectedItems : [];
|
|
113
|
+
this.updateState({
|
|
114
|
+
status: 'multiSelect',
|
|
115
|
+
inputValue: '',
|
|
116
|
+
selectedItems: currentSelected,
|
|
117
|
+
suggestions: [],
|
|
118
|
+
validation: 'idle',
|
|
119
|
+
validationContext, // BEVARA CONTEXT
|
|
120
|
+
...this.getCommonState()
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.updateState({
|
|
125
|
+
status: 'searching',
|
|
126
|
+
inputValue: '',
|
|
127
|
+
suggestions: [],
|
|
128
|
+
validation: 'idle',
|
|
129
|
+
validationContext, // BEVARA CONTEXT
|
|
130
|
+
...this.getCommonState()
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
this.showHistory();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
switch (this.state.status) {
|
|
137
|
+
case 'multiSelect':
|
|
138
|
+
case 'multiSelectShowingHistory': {
|
|
139
|
+
const selectedItems = 'selectedItems' in this.state ? this.state.selectedItems : [];
|
|
140
|
+
this.updateState({
|
|
141
|
+
status: 'multiSelect',
|
|
142
|
+
inputValue: value,
|
|
143
|
+
selectedItems,
|
|
144
|
+
suggestions: 'suggestions' in this.state ? this.state.suggestions : ('historyItems' in this.state ? this.state.historyItems : []),
|
|
145
|
+
validation: 'idle',
|
|
146
|
+
validationContext,
|
|
147
|
+
dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
|
|
148
|
+
dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'itemSelected':
|
|
153
|
+
case 'searching':
|
|
154
|
+
case 'showingHistory': {
|
|
155
|
+
this.updateState({
|
|
156
|
+
status: 'searching',
|
|
157
|
+
inputValue: value,
|
|
158
|
+
suggestions: 'suggestions' in this.state ? this.state.suggestions : ('historyItems' in this.state ? this.state.historyItems : []),
|
|
159
|
+
validation: 'idle',
|
|
160
|
+
validationContext,
|
|
161
|
+
dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
|
|
162
|
+
dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'loading':
|
|
167
|
+
case 'error':
|
|
168
|
+
return; // Interaction not allowed or ignored
|
|
169
|
+
default: {
|
|
170
|
+
const _exhaustive = this.state;
|
|
171
|
+
return _exhaustive;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (value.length >= this.config.minLength) {
|
|
175
|
+
this.debounceTimer = setTimeout(() => this.performSearch(value), DEBOUNCE_TIME);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
performSearch(value) {
|
|
179
|
+
if (!this.searchEngine)
|
|
180
|
+
return;
|
|
181
|
+
switch (this.state.status) {
|
|
182
|
+
case 'searching':
|
|
183
|
+
case 'multiSelect': {
|
|
184
|
+
const genericResults = this.searchEngine.searchAsGeneric(value, this.config.maxResults);
|
|
185
|
+
const results = genericResults.flatMap(g => g.metadata?.originalResult ? [g.metadata.originalResult] : []);
|
|
186
|
+
// Do not filter out selected items here - allow UI to handle grey-out
|
|
187
|
+
this.updateState({ ...this.state, suggestions: results });
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'loading':
|
|
191
|
+
case 'error':
|
|
192
|
+
case 'itemSelected':
|
|
193
|
+
case 'showingHistory':
|
|
194
|
+
case 'multiSelectShowingHistory':
|
|
195
|
+
return; // Nothing to search from here
|
|
196
|
+
default: {
|
|
197
|
+
const _exhaustive = this.state;
|
|
198
|
+
return _exhaustive;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// --- HISTORY HANDLING ---
|
|
203
|
+
addToHistory(item) {
|
|
204
|
+
if (!this.config.history)
|
|
205
|
+
return;
|
|
206
|
+
// Reload history from cache to ensure we have the latest state from other instances
|
|
207
|
+
const historyItems = HistoryService.loadHistory();
|
|
208
|
+
// Clone item and reset highlightedLabel to displayLabel (removes bolding)
|
|
209
|
+
// AND apply grey styling to parenthesis (via DisplaySegments)
|
|
210
|
+
const unhighlightedSegments = LabelFormatter.format(item.rawLabel, '', item.occupationLabel);
|
|
211
|
+
const historyItem = {
|
|
212
|
+
...item,
|
|
213
|
+
displaySegments: unhighlightedSegments,
|
|
214
|
+
// Legacy support: construct HTML string for fallback
|
|
215
|
+
highlightedLabel: segmentsToHtml(unhighlightedSegments)
|
|
216
|
+
};
|
|
217
|
+
// Filter out existing item with same ID (to move it to top)
|
|
218
|
+
const filtered = historyItems.filter((h) => {
|
|
219
|
+
if (h.id && historyItem.id)
|
|
220
|
+
return h.id !== historyItem.id;
|
|
221
|
+
return h.rawLabel !== historyItem.rawLabel;
|
|
222
|
+
});
|
|
223
|
+
// Add to top and limit
|
|
224
|
+
const newHistory = [historyItem, ...filtered].slice(0, this.config.maxHistoryItems);
|
|
225
|
+
HistoryService.saveHistory(newHistory);
|
|
226
|
+
}
|
|
227
|
+
clearHistory() {
|
|
228
|
+
HistoryService.clearHistory();
|
|
229
|
+
this.showHistory(true);
|
|
230
|
+
}
|
|
231
|
+
computePopularItems(data, limit = this.config.maxResults || MAX_RESULTS) {
|
|
232
|
+
if (!data || data.length === 0)
|
|
233
|
+
return [];
|
|
234
|
+
const hasWeights = data.some(t => typeof t.weight === 'number' && t.weight > 0);
|
|
235
|
+
if (!hasWeights) {
|
|
236
|
+
return data.slice(0, limit);
|
|
237
|
+
}
|
|
238
|
+
const sorted = [...data].sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0));
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const popular = [];
|
|
241
|
+
for (const item of sorted) {
|
|
242
|
+
const key = item.occupation_name_preferred_label || item.preferred_label;
|
|
243
|
+
if (!seen.has(key)) {
|
|
244
|
+
seen.add(key);
|
|
245
|
+
popular.push(item);
|
|
246
|
+
}
|
|
247
|
+
if (popular.length === limit)
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
return popular;
|
|
251
|
+
}
|
|
252
|
+
showHistory(force = false) {
|
|
253
|
+
if (this.state.status === 'loading' || this.state.status === 'error')
|
|
254
|
+
return;
|
|
255
|
+
if (!this.config.history)
|
|
256
|
+
return;
|
|
257
|
+
const historyItems = HistoryService.loadHistory();
|
|
258
|
+
let displayItems = historyItems;
|
|
259
|
+
let isPopular = false;
|
|
260
|
+
if (historyItems.length === 0) {
|
|
261
|
+
if (this.config.popular === false) {
|
|
262
|
+
displayItems = [];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (this.popularItems.length === 0 && this.taxonomyData.length > 0) {
|
|
266
|
+
this.popularItems = this.computePopularItems(this.taxonomyData);
|
|
267
|
+
}
|
|
268
|
+
// Provide some popular items if history is empty (and we have taxonomy data)
|
|
269
|
+
displayItems = this.popularItems.map(t => this.toSearchResult(t));
|
|
270
|
+
isPopular = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Lazy Validation: Filter items against current taxonomy and config
|
|
274
|
+
const validatedHistory = displayItems.filter((item) => {
|
|
275
|
+
// 1. Check if it's a ManualInput item
|
|
276
|
+
if (item.type === 'ManualInput') {
|
|
277
|
+
return !this.config.requireSelection;
|
|
278
|
+
}
|
|
279
|
+
// 2. Check if it's a valid Taxonomy item (must exist in current data)
|
|
280
|
+
// Note: We use this.taxonomyData which is already loaded
|
|
281
|
+
const exists = this.taxonomyData.some(t => t.id === item.id);
|
|
282
|
+
return exists;
|
|
283
|
+
});
|
|
284
|
+
const filteredHistory = validatedHistory.map((item) => {
|
|
285
|
+
const unhighlightedSegments = LabelFormatter.format(item.rawLabel, '', item.occupationLabel);
|
|
286
|
+
return {
|
|
287
|
+
...item,
|
|
288
|
+
displaySegments: unhighlightedSegments,
|
|
289
|
+
highlightedLabel: segmentsToHtml(unhighlightedSegments)
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
const { dataSource, dataSourceDate } = this.getCommonState();
|
|
293
|
+
const currentInputValue = 'inputValue' in this.state ? this.state.inputValue : '';
|
|
294
|
+
// If we have input text, we don't want to show history unless explicitly in a history state
|
|
295
|
+
// OR if we are forcing it (e.g. from editSelection)
|
|
296
|
+
if (!force && currentInputValue.trim() !== '' && this.state.status !== 'showingHistory' && this.state.status !== 'multiSelectShowingHistory') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (this.config.enableMultiple) {
|
|
300
|
+
const selectedItems = 'selectedItems' in this.state ? this.state.selectedItems : [];
|
|
301
|
+
const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
|
|
302
|
+
const validationContext = wasInError ? 'from-error' : undefined;
|
|
303
|
+
const validation = 'validation' in this.state ? this.state.validation : 'idle';
|
|
304
|
+
this.updateState({
|
|
305
|
+
status: 'multiSelectShowingHistory',
|
|
306
|
+
selectedItems,
|
|
307
|
+
historyItems: filteredHistory,
|
|
308
|
+
inputValue: currentInputValue,
|
|
309
|
+
validation,
|
|
310
|
+
validationContext,
|
|
311
|
+
dataSource,
|
|
312
|
+
dataSourceDate,
|
|
313
|
+
isPopularView: isPopular // Flag for view
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
|
|
318
|
+
const validationContext = wasInError ? 'from-error' : undefined;
|
|
319
|
+
const validation = 'validation' in this.state ? this.state.validation : 'idle';
|
|
320
|
+
this.updateState({
|
|
321
|
+
status: 'showingHistory',
|
|
322
|
+
historyItems: filteredHistory,
|
|
323
|
+
inputValue: currentInputValue,
|
|
324
|
+
validation,
|
|
325
|
+
validationContext,
|
|
326
|
+
dataSource,
|
|
327
|
+
dataSourceDate,
|
|
328
|
+
isPopularView: isPopular // Flag for view
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// --- SELECTION ---
|
|
333
|
+
selectItem(item) {
|
|
334
|
+
log.debug('[Controller] selectItem', item);
|
|
335
|
+
// Add to history
|
|
336
|
+
this.isTouched = true;
|
|
337
|
+
this.addToHistory(item);
|
|
338
|
+
// Standardized Success Message Logic:
|
|
339
|
+
// Check for error state before update to allow transition to success.
|
|
340
|
+
// This applies to ALL selection events (Single, Multi, History, Search).
|
|
341
|
+
const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
|
|
342
|
+
const nextValidationContext = wasInError ? 'from-error' : undefined;
|
|
343
|
+
switch (this.state.status) {
|
|
344
|
+
case 'multiSelect':
|
|
345
|
+
case 'multiSelectShowingHistory': {
|
|
346
|
+
const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
|
|
347
|
+
const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
|
|
348
|
+
const currentSelected = 'selectedItems' in this.state ? this.state.selectedItems : [];
|
|
349
|
+
// Prevent duplicates
|
|
350
|
+
const existingIndex = currentSelected.findIndex(existing => {
|
|
351
|
+
if (existing.id && item.id)
|
|
352
|
+
return existing.id === item.id;
|
|
353
|
+
return existing.rawLabel === item.rawLabel;
|
|
354
|
+
});
|
|
355
|
+
if (existingIndex > -1) {
|
|
356
|
+
// Already selected - skip to avoid double addition or accidental removal in this context
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const newItems = [...currentSelected, item];
|
|
360
|
+
// Uppdatera state: rensa input och förslag
|
|
361
|
+
this.updateState({
|
|
362
|
+
...this.state,
|
|
363
|
+
status: 'multiSelect',
|
|
364
|
+
selectedItems: newItems,
|
|
365
|
+
inputValue: '',
|
|
366
|
+
suggestions: [],
|
|
367
|
+
validation: 'idle',
|
|
368
|
+
validationContext: nextValidationContext, // Use centralized context
|
|
369
|
+
dataSource,
|
|
370
|
+
dataSourceDate
|
|
371
|
+
});
|
|
372
|
+
this.callbacks.onEmitSelection(newItems.map(i => this.toPublicSelection(i)));
|
|
373
|
+
this.showHistory(true);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case 'searching':
|
|
377
|
+
case 'showingHistory': {
|
|
378
|
+
const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
|
|
379
|
+
const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
|
|
380
|
+
this.updateState({
|
|
381
|
+
status: 'itemSelected',
|
|
382
|
+
selectedItem: item,
|
|
383
|
+
inputValue: item.preferred_label, // Keep label as text
|
|
384
|
+
validation: 'idle',
|
|
385
|
+
validationContext: nextValidationContext, // Use centralized context
|
|
386
|
+
dataSource,
|
|
387
|
+
dataSourceDate,
|
|
388
|
+
});
|
|
389
|
+
this.callbacks.onEmitSelection(this.toPublicSelection(item));
|
|
390
|
+
if (this.config.enableMultiple) {
|
|
391
|
+
this.showHistory(true);
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'loading':
|
|
396
|
+
case 'error':
|
|
397
|
+
// Do nothing in these states
|
|
398
|
+
break;
|
|
399
|
+
case 'itemSelected': {
|
|
400
|
+
const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
|
|
401
|
+
const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
|
|
402
|
+
this.updateState({
|
|
403
|
+
...this.state,
|
|
404
|
+
status: 'itemSelected',
|
|
405
|
+
selectedItem: item,
|
|
406
|
+
inputValue: item.preferred_label,
|
|
407
|
+
validation: 'idle',
|
|
408
|
+
validationContext: nextValidationContext,
|
|
409
|
+
dataSource,
|
|
410
|
+
dataSourceDate,
|
|
411
|
+
});
|
|
412
|
+
this.callbacks.onEmitSelection(this.toPublicSelection(item));
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
default: {
|
|
416
|
+
const _exhaustive = this.state;
|
|
417
|
+
log.warn('[Controller] selectItem - Unexpected state:', _exhaustive);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
this.runValidation();
|
|
421
|
+
}
|
|
422
|
+
clearSelection() {
|
|
423
|
+
if (this.state.status !== 'itemSelected')
|
|
424
|
+
return;
|
|
425
|
+
this.isTouched = true;
|
|
426
|
+
const currentInputValue = this.state.inputValue || '';
|
|
427
|
+
this.updateState({
|
|
428
|
+
status: 'searching',
|
|
429
|
+
inputValue: currentInputValue,
|
|
430
|
+
suggestions: [],
|
|
431
|
+
validation: 'idle',
|
|
432
|
+
...this.getCommonState()
|
|
433
|
+
});
|
|
434
|
+
this.callbacks.onEmitSelection([]); // Empty array instead of null
|
|
435
|
+
// Try to show history if enabled
|
|
436
|
+
this.showHistory();
|
|
437
|
+
this.runValidation();
|
|
438
|
+
}
|
|
439
|
+
removeItem(item) {
|
|
440
|
+
switch (this.state.status) {
|
|
441
|
+
case 'multiSelect':
|
|
442
|
+
case 'multiSelectShowingHistory': {
|
|
443
|
+
const newItems = this.state.selectedItems.filter(i => {
|
|
444
|
+
if (i.id && item.id)
|
|
445
|
+
return i.id !== item.id;
|
|
446
|
+
return i.rawLabel !== item.rawLabel;
|
|
447
|
+
});
|
|
448
|
+
this.isTouched = true;
|
|
449
|
+
this.updateState({ ...this.state, selectedItems: newItems });
|
|
450
|
+
this.callbacks.onEmitSelection(newItems.map(i => this.toPublicSelection(i)));
|
|
451
|
+
// Try to show history if enabled (since we are likely still focused)
|
|
452
|
+
const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
|
|
453
|
+
if (inputValue === '') {
|
|
454
|
+
this.showHistory();
|
|
455
|
+
}
|
|
456
|
+
this.runValidation();
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
default:
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// --- BLUR & VALIDATION ---
|
|
464
|
+
handleBlur() {
|
|
465
|
+
this.isTouched = true;
|
|
466
|
+
if (this.state.status === 'searching') {
|
|
467
|
+
const trimmedValue = (this.state.inputValue || '').trim();
|
|
468
|
+
if (trimmedValue !== '') {
|
|
469
|
+
const exactMatch = this.findExactMatch(trimmedValue);
|
|
470
|
+
if (exactMatch) {
|
|
471
|
+
this.selectItem(this.toSearchResult(exactMatch));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// In TEA/FSM, handleBlur should focus on state transitions and validation,
|
|
477
|
+
// rather than side-effects like clearing UI data.
|
|
478
|
+
// We transition back to primary states (searching/multiSelect) but KEEP suggestions.
|
|
479
|
+
switch (this.state.status) {
|
|
480
|
+
case 'searching':
|
|
481
|
+
case 'showingHistory':
|
|
482
|
+
this.updateState({
|
|
483
|
+
...this.state,
|
|
484
|
+
status: 'searching',
|
|
485
|
+
inputValue: 'inputValue' in this.state ? this.state.inputValue : '',
|
|
486
|
+
suggestions: []
|
|
487
|
+
});
|
|
488
|
+
break;
|
|
489
|
+
case 'multiSelect':
|
|
490
|
+
case 'multiSelectShowingHistory': {
|
|
491
|
+
this.updateState({
|
|
492
|
+
...this.state,
|
|
493
|
+
status: 'multiSelect',
|
|
494
|
+
inputValue: 'inputValue' in this.state ? this.state.inputValue : '',
|
|
495
|
+
suggestions: []
|
|
496
|
+
});
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case 'loading':
|
|
500
|
+
case 'error':
|
|
501
|
+
case 'itemSelected':
|
|
502
|
+
break;
|
|
503
|
+
default: {
|
|
504
|
+
const _exhaustive = this.state;
|
|
505
|
+
return _exhaustive;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
this.runValidation();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Validerar nuvarande värde och sätter eventuella valideringsfel.
|
|
512
|
+
* @param forceTouch - Tvingar valideringen att köras (och därmed visa fel) även om fältet inte tidigare blivit rört (touched). Default är false.
|
|
513
|
+
*/
|
|
514
|
+
runValidation(forceTouch = false) {
|
|
515
|
+
if (forceTouch) {
|
|
516
|
+
this.isTouched = true;
|
|
517
|
+
}
|
|
518
|
+
if (!this.isTouched)
|
|
519
|
+
return true;
|
|
520
|
+
if (this.state.status === 'loading' || this.state.status === 'error')
|
|
521
|
+
return true;
|
|
522
|
+
const hasValue = this.checkHasValue();
|
|
523
|
+
let isValid = true;
|
|
524
|
+
let newErrorType = null;
|
|
525
|
+
// Check for invalid input (text that doesn't match a selection)
|
|
526
|
+
// This applies in both single 'searching' mode and 'multiSelect' mode
|
|
527
|
+
const isSearchingOrMulti = this.state.status === 'searching' || this.state.status === 'multiSelect' || this.state.status === 'multiSelectShowingHistory';
|
|
528
|
+
const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
|
|
529
|
+
const hasInputText = inputValue.trim() !== '';
|
|
530
|
+
if (isSearchingOrMulti && hasInputText) {
|
|
531
|
+
// New rule: If there is text in the box that isn't confirmed as a selection, it's ALWAYS an error
|
|
532
|
+
// regardless of whether manual input is enabled or not.
|
|
533
|
+
// The user MUST confirm by selecting the manual input option.
|
|
534
|
+
isValid = false;
|
|
535
|
+
newErrorType = 'invalidInput';
|
|
536
|
+
}
|
|
537
|
+
else if (this.config.required && !hasValue) {
|
|
538
|
+
isValid = false;
|
|
539
|
+
newErrorType = 'required';
|
|
540
|
+
}
|
|
541
|
+
this.activeErrorType = newErrorType;
|
|
542
|
+
// Strict FSM for validation state
|
|
543
|
+
// Rules:
|
|
544
|
+
// 1. Error only if invalid.
|
|
545
|
+
// 2. Idle if valid (never use success to avoid aggressive green checks).
|
|
546
|
+
// Valid state: only show success if recovering from an error (Red -> Green pattern) AND we actually have a value.
|
|
547
|
+
const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
|
|
548
|
+
let validationState;
|
|
549
|
+
if (!isValid) {
|
|
550
|
+
validationState = newErrorType === 'invalidInput' || forceTouch || wasInError ? 'error' : 'idle';
|
|
551
|
+
}
|
|
552
|
+
else if (wasInError && hasValue) {
|
|
553
|
+
validationState = 'success';
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
validationState = 'idle';
|
|
557
|
+
}
|
|
558
|
+
if (validationState === 'success') {
|
|
559
|
+
clearTimeout(this.validationTimer);
|
|
560
|
+
this.validationTimer = setTimeout(() => {
|
|
561
|
+
// Only clear if still success (user hasn't messed up again)
|
|
562
|
+
const isSuccess = 'validation' in this.state && this.state.validation === 'success';
|
|
563
|
+
const allowedStatus = ['itemSelected', 'multiSelect', 'multiSelectShowingHistory'].includes(this.state.status);
|
|
564
|
+
if (allowedStatus && isSuccess) {
|
|
565
|
+
this.updateState({ ...this.state, validation: 'idle', validationContext: undefined });
|
|
566
|
+
}
|
|
567
|
+
this.validationTimer = undefined;
|
|
568
|
+
}, 3000);
|
|
569
|
+
}
|
|
570
|
+
// Update state based on current status (respecting type definitions)
|
|
571
|
+
switch (this.state.status) {
|
|
572
|
+
case 'searching':
|
|
573
|
+
this.updateState({ ...this.state, validation: validationState });
|
|
574
|
+
break;
|
|
575
|
+
case 'showingHistory':
|
|
576
|
+
this.updateState({ ...this.state, validation: validationState });
|
|
577
|
+
break;
|
|
578
|
+
case 'multiSelect':
|
|
579
|
+
// Allow error AND success states for multiSelect
|
|
580
|
+
this.updateState({ ...this.state, validation: validationState });
|
|
581
|
+
break;
|
|
582
|
+
case 'multiSelectShowingHistory':
|
|
583
|
+
this.updateState({ ...this.state, validation: validationState });
|
|
584
|
+
break;
|
|
585
|
+
case 'itemSelected':
|
|
586
|
+
// ItemSelected does not support 'error'. But checkHasValue() returns true, so isValid is true.
|
|
587
|
+
if (validationState !== 'error') {
|
|
588
|
+
this.updateState({ ...this.state, validation: validationState });
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
default: {
|
|
592
|
+
// If we reach here, we must have handled all states that aren't 'loading' or 'error'
|
|
593
|
+
// But TypeScript needs to know that.
|
|
594
|
+
const _exhaustive = this.state;
|
|
595
|
+
return _exhaustive;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
this.callbacks.onEmitValidation({ isValid, errorType: newErrorType });
|
|
599
|
+
return isValid;
|
|
600
|
+
}
|
|
601
|
+
selectItemById(id) {
|
|
602
|
+
if (!id || this.config.enableMultiple || !this.taxonomyData)
|
|
603
|
+
return;
|
|
604
|
+
const itemToSelect = this.taxonomyData.find(t => t.id === id);
|
|
605
|
+
if (itemToSelect) {
|
|
606
|
+
const searchResult = this.toSearchResult(itemToSelect);
|
|
607
|
+
const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
|
|
608
|
+
const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
|
|
609
|
+
this.updateState({
|
|
610
|
+
status: 'itemSelected',
|
|
611
|
+
selectedItem: searchResult,
|
|
612
|
+
inputValue: searchResult.preferred_label,
|
|
613
|
+
validation: 'idle',
|
|
614
|
+
dataSource,
|
|
615
|
+
dataSourceDate,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
this.runValidation();
|
|
619
|
+
}
|
|
620
|
+
selectItemsByIds(ids) {
|
|
621
|
+
if (!ids || !this.config.enableMultiple || !this.taxonomyData)
|
|
622
|
+
return;
|
|
623
|
+
const taxonomyItems = ids.map(id => this.taxonomyData.find(item => item.id === id));
|
|
624
|
+
if (taxonomyItems.some((item) => !item))
|
|
625
|
+
return;
|
|
626
|
+
const newSelectedItems = taxonomyItems
|
|
627
|
+
.filter((item) => !!item)
|
|
628
|
+
.map(item => this.toSearchResult(item));
|
|
629
|
+
switch (this.state.status) {
|
|
630
|
+
case 'multiSelect':
|
|
631
|
+
case 'multiSelectShowingHistory':
|
|
632
|
+
this.updateState({
|
|
633
|
+
...this.state,
|
|
634
|
+
selectedItems: newSelectedItems
|
|
635
|
+
});
|
|
636
|
+
break;
|
|
637
|
+
default:
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
this.runValidation();
|
|
641
|
+
}
|
|
642
|
+
setSelection(selection) {
|
|
643
|
+
const invalidIds = [];
|
|
644
|
+
const selectedItems = selection.map((item) => {
|
|
645
|
+
if (item.id !== null) {
|
|
646
|
+
const taxonomyItem = this.taxonomyData.find((taxonomy) => taxonomy.id === item.id);
|
|
647
|
+
if (!taxonomyItem) {
|
|
648
|
+
invalidIds.push(item.id);
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
return this.toSearchResult(taxonomyItem);
|
|
652
|
+
}
|
|
653
|
+
return this.createManualInputItem(item.preferred_label);
|
|
654
|
+
});
|
|
655
|
+
if (invalidIds.length > 0) {
|
|
656
|
+
return { ok: false, invalidIds };
|
|
657
|
+
}
|
|
658
|
+
const resolvedItems = selectedItems;
|
|
659
|
+
const commonState = this.getCommonState();
|
|
660
|
+
if (this.config.enableMultiple) {
|
|
661
|
+
this.updateState({
|
|
662
|
+
...this.state,
|
|
663
|
+
status: 'multiSelect',
|
|
664
|
+
selectedItems: resolvedItems,
|
|
665
|
+
inputValue: '',
|
|
666
|
+
suggestions: [],
|
|
667
|
+
validation: 'idle',
|
|
668
|
+
...commonState
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
else if (resolvedItems.length === 1) {
|
|
672
|
+
const selectedItem = resolvedItems[0];
|
|
673
|
+
this.updateState({
|
|
674
|
+
status: 'itemSelected',
|
|
675
|
+
selectedItem,
|
|
676
|
+
inputValue: selectedItem.preferred_label,
|
|
677
|
+
validation: 'idle',
|
|
678
|
+
...commonState
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
this.updateState({
|
|
683
|
+
status: 'searching',
|
|
684
|
+
inputValue: '',
|
|
685
|
+
suggestions: [],
|
|
686
|
+
validation: 'idle',
|
|
687
|
+
...commonState
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
this.runValidation();
|
|
691
|
+
return { ok: true, selection: resolvedItems.map((item) => this.toPublicSelection(item)) };
|
|
692
|
+
}
|
|
693
|
+
editSelection() {
|
|
694
|
+
const selectedItem = this.state.selectedItem;
|
|
695
|
+
if (this.state.status !== 'itemSelected' || !selectedItem || !this.searchEngine)
|
|
696
|
+
return;
|
|
697
|
+
// Transition to searching first to ensure input shows up
|
|
698
|
+
const professionLabel = selectedItem.displayLabel;
|
|
699
|
+
const { dataSource, dataSourceDate } = this.state;
|
|
700
|
+
// Rule: On activation, show search results for the current string instead of history
|
|
701
|
+
const genericResults = this.searchEngine.searchAsGeneric(professionLabel, this.config.maxResults);
|
|
702
|
+
const searchResults = genericResults.flatMap(g => g.metadata?.originalResult ? [g.metadata.originalResult] : []);
|
|
703
|
+
this.updateState({
|
|
704
|
+
status: 'searching',
|
|
705
|
+
inputValue: professionLabel,
|
|
706
|
+
suggestions: searchResults,
|
|
707
|
+
validation: 'idle',
|
|
708
|
+
dataSource,
|
|
709
|
+
dataSourceDate
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
checkHasValue() {
|
|
713
|
+
switch (this.state.status) {
|
|
714
|
+
case 'itemSelected':
|
|
715
|
+
return true;
|
|
716
|
+
case 'multiSelect':
|
|
717
|
+
case 'multiSelectShowingHistory':
|
|
718
|
+
return this.state.selectedItems.length > 0;
|
|
719
|
+
case 'searching':
|
|
720
|
+
case 'showingHistory': {
|
|
721
|
+
const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
|
|
722
|
+
return !this.config.requireSelection && inputValue.trim() !== '';
|
|
723
|
+
}
|
|
724
|
+
case 'loading':
|
|
725
|
+
case 'error':
|
|
726
|
+
return false;
|
|
727
|
+
default: {
|
|
728
|
+
const _exhaustive = this.state;
|
|
729
|
+
return _exhaustive;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
getCommonState() {
|
|
734
|
+
return {
|
|
735
|
+
dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
|
|
736
|
+
dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
resetMultiSelection() {
|
|
740
|
+
const commonState = this.getCommonState();
|
|
741
|
+
this.updateState({
|
|
742
|
+
...this.state,
|
|
743
|
+
status: 'multiSelect',
|
|
744
|
+
selectedItems: [],
|
|
745
|
+
inputValue: '',
|
|
746
|
+
suggestions: [],
|
|
747
|
+
validation: 'idle',
|
|
748
|
+
...commonState
|
|
749
|
+
});
|
|
750
|
+
this.callbacks.onEmitSelection([]);
|
|
751
|
+
}
|
|
752
|
+
// --- HELPERS ---
|
|
753
|
+
updateState(newState) {
|
|
754
|
+
this.state = newState;
|
|
755
|
+
this.callbacks.onStateChange(newState);
|
|
756
|
+
}
|
|
757
|
+
toSearchResult(item) {
|
|
758
|
+
return {
|
|
759
|
+
...item,
|
|
760
|
+
rawLabel: item.preferred_label,
|
|
761
|
+
displayLabel: item.occupation_name_preferred_label
|
|
762
|
+
? `${item.preferred_label} (${item.occupation_name_preferred_label})`
|
|
763
|
+
: item.preferred_label,
|
|
764
|
+
highlightedLabel: item.preferred_label,
|
|
765
|
+
score: 3.0,
|
|
766
|
+
// Map v2 fields to match SearchResult interface expectations if needed
|
|
767
|
+
occupationLabel: item.occupation_name_preferred_label,
|
|
768
|
+
occupationId: item.occupation_name_id,
|
|
769
|
+
ssyk_level_4_id: item.ssyk_level_4_id,
|
|
770
|
+
ssyk_code_2012: item.ssyk_code_2012
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
toPublicSelection(item) {
|
|
774
|
+
return TaxonomyAdapter.toPublicSelection(item);
|
|
775
|
+
}
|
|
776
|
+
createManualInputItem(value) {
|
|
777
|
+
return {
|
|
778
|
+
id: '',
|
|
779
|
+
type: 'ManualInput',
|
|
780
|
+
preferred_label: value,
|
|
781
|
+
rawLabel: value,
|
|
782
|
+
displayLabel: value,
|
|
783
|
+
highlightedLabel: value,
|
|
784
|
+
score: 1.0
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
findExactMatch(value) {
|
|
788
|
+
return this.taxonomyData.find(item => {
|
|
789
|
+
const displayLabel = item.occupation_name_preferred_label
|
|
790
|
+
? `${item.preferred_label} (${item.occupation_name_preferred_label})`
|
|
791
|
+
: item.preferred_label;
|
|
792
|
+
return displayLabel.toLowerCase() === value.toLowerCase() || item.preferred_label.toLowerCase() === value.toLowerCase();
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
// --- FREETEXT HANDLING (used by keyboard handler) ---
|
|
796
|
+
handleFreeTextSelection(value) {
|
|
797
|
+
if (!value)
|
|
798
|
+
return;
|
|
799
|
+
this.isTouched = true;
|
|
800
|
+
const exactMatch = this.findExactMatch(value);
|
|
801
|
+
if (exactMatch) {
|
|
802
|
+
this.selectItem(this.toSearchResult(exactMatch));
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
this.selectItem(this.createManualInputItem(value));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
loadMore() {
|
|
809
|
+
// Increment maxResults or handle paging if supported by searchEngine
|
|
810
|
+
this.config.maxResults += 10;
|
|
811
|
+
const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
|
|
812
|
+
if (inputValue) {
|
|
813
|
+
this.performSearch(inputValue);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
resetSearch() {
|
|
817
|
+
switch (this.state.status) {
|
|
818
|
+
case 'searching':
|
|
819
|
+
case 'multiSelect':
|
|
820
|
+
this.updateState({ ...this.state, inputValue: '', suggestions: [] });
|
|
821
|
+
this.showHistory(true);
|
|
822
|
+
break;
|
|
823
|
+
case 'showingHistory':
|
|
824
|
+
case 'multiSelectShowingHistory':
|
|
825
|
+
// Already showing history, just ensure input is empty
|
|
826
|
+
this.updateState({ ...this.state, inputValue: '' });
|
|
827
|
+
break;
|
|
828
|
+
case 'itemSelected':
|
|
829
|
+
this.clearSelection();
|
|
830
|
+
break;
|
|
831
|
+
default:
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|