@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.
Files changed (146) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +369 -0
  3. package/dist/cjs/af-validation-message-BzY5H0xU.js +16 -0
  4. package/dist/cjs/core-selector.cjs.entry.js +476 -0
  5. package/dist/cjs/digi-service-job-selector.cjs.entry.js +1619 -0
  6. package/dist/cjs/fuse-BTLNKxuL.js +1631 -0
  7. package/dist/cjs/geo-valjaren.cjs.entry.js +14910 -0
  8. package/dist/cjs/index-BOuTGlEp.js +1817 -0
  9. package/dist/cjs/index.cjs.js +14 -0
  10. package/dist/cjs/job-selector-demo.cjs.entry.js +692 -0
  11. package/dist/cjs/loader.cjs.js +12 -0
  12. package/dist/cjs/neobrutal-search.cjs.entry.js +171 -0
  13. package/dist/cjs/search.engine-D0cMelRi.js +489 -0
  14. package/dist/cjs/taxonomy-registry-DVmuDUpM.js +435 -0
  15. package/dist/cjs/types-CtHBl6Ra.js +14 -0
  16. package/dist/cjs/yrkesvaljaren.cjs.js +24 -0
  17. package/dist/collection/collection-manifest.json +15 -0
  18. package/dist/collection/components/core-selector/core-selector.css +803 -0
  19. package/dist/collection/components/core-selector/core-selector.js +761 -0
  20. package/dist/collection/components/core-selector/types.js +1 -0
  21. package/dist/collection/components/design-compat/af-icon.js +13 -0
  22. package/dist/collection/components/design-compat/af-label.js +2 -0
  23. package/dist/collection/components/design-compat/af-tag.js +6 -0
  24. package/dist/collection/components/design-compat/af-validation-message.js +11 -0
  25. package/dist/collection/components/design-compat/index.js +4 -0
  26. package/dist/collection/components/geovaljaren/geovaljaren.css +21 -0
  27. package/dist/collection/components/geovaljaren/geovaljaren.js +304 -0
  28. package/dist/collection/components/geovaljaren/logic/adapter.utils.js +20 -0
  29. package/dist/collection/components/geovaljaren/logic/data.utils.js +12 -0
  30. package/dist/collection/components/geovaljaren/logic/history-service.js +25 -0
  31. package/dist/collection/components/geovaljaren/logic/search.engine.js +28 -0
  32. package/dist/collection/components/job-selector/constants.js +26 -0
  33. package/dist/collection/components/job-selector/history-service.js +41 -0
  34. package/dist/collection/components/job-selector/job-selector-controller.js +835 -0
  35. package/dist/collection/components/job-selector/job-selector.css +318 -0
  36. package/dist/collection/components/job-selector/job-selector.js +1378 -0
  37. package/dist/collection/components/job-selector/keyboard-handlers.js +84 -0
  38. package/dist/collection/components/job-selector/logic/adapter.utils.js +75 -0
  39. package/dist/collection/components/job-selector/logic/label-formatter.js +118 -0
  40. package/dist/collection/components/job-selector/logic/search.engine.js +50 -0
  41. package/dist/collection/components/job-selector/search-logic.js +35 -0
  42. package/dist/collection/components/job-selector/search.utils.js +224 -0
  43. package/dist/collection/components/job-selector/suggestions-list.js +31 -0
  44. package/dist/collection/components/job-selector/taxonomy-registry.js +60 -0
  45. package/dist/collection/components/job-selector/test-data.mock.js +33 -0
  46. package/dist/collection/components/job-selector/types.js +10 -0
  47. package/dist/collection/components/job-selector/validation-message.js +24 -0
  48. package/dist/collection/demo-data.js +20 -0
  49. package/dist/collection/index.js +7 -0
  50. package/dist/collection/verkstad/verkstad.js +407 -0
  51. package/dist/components/core-selector.d.ts +11 -0
  52. package/dist/components/core-selector.js +1 -0
  53. package/dist/components/core-selector2.js +1 -0
  54. package/dist/components/digi-service-job-selector.d.ts +11 -0
  55. package/dist/components/digi-service-job-selector.js +1 -0
  56. package/dist/components/fuse.js +1 -0
  57. package/dist/components/geo-valjaren.d.ts +11 -0
  58. package/dist/components/geo-valjaren.js +1 -0
  59. package/dist/components/index.d.ts +35 -0
  60. package/dist/components/index.js +1 -0
  61. package/dist/components/job-selector-demo.d.ts +11 -0
  62. package/dist/components/job-selector-demo.js +1 -0
  63. package/dist/components/job-selector.js +1 -0
  64. package/dist/components/neobrutal-search.d.ts +11 -0
  65. package/dist/components/neobrutal-search.js +1 -0
  66. package/dist/components/search.engine.js +1 -0
  67. package/dist/components/types.js +1 -0
  68. package/dist/esm/af-validation-message-F8NIH2rC.js +14 -0
  69. package/dist/esm/core-selector.entry.js +474 -0
  70. package/dist/esm/digi-service-job-selector.entry.js +1617 -0
  71. package/dist/esm/fuse-BPnhMTul.js +1629 -0
  72. package/dist/esm/geo-valjaren.entry.js +14908 -0
  73. package/dist/esm/index-CGXZllj2.js +1806 -0
  74. package/dist/esm/index.js +10 -0
  75. package/dist/esm/job-selector-demo.entry.js +690 -0
  76. package/dist/esm/loader.js +10 -0
  77. package/dist/esm/neobrutal-search.entry.js +169 -0
  78. package/dist/esm/search.engine-nZ7AcXba.js +479 -0
  79. package/dist/esm/taxonomy-registry-Dw0VR9Aw.js +430 -0
  80. package/dist/esm/types-DHUftoZJ.js +12 -0
  81. package/dist/esm/yrkesvaljaren.js +20 -0
  82. package/dist/index.cjs.js +1 -0
  83. package/dist/index.js +1 -0
  84. package/dist/types/components/core-selector/core-selector.d.ts +46 -0
  85. package/dist/types/components/core-selector/types.d.ts +1 -0
  86. package/dist/types/components/design-compat/af-icon.d.ts +7 -0
  87. package/dist/types/components/design-compat/af-label.d.ts +8 -0
  88. package/dist/types/components/design-compat/af-tag.d.ts +8 -0
  89. package/dist/types/components/design-compat/af-validation-message.d.ts +9 -0
  90. package/dist/types/components/design-compat/index.d.ts +5 -0
  91. package/dist/types/components/geovaljaren/geovaljaren.d.ts +24 -0
  92. package/dist/types/components/geovaljaren/logic/adapter.utils.d.ts +7 -0
  93. package/dist/types/components/geovaljaren/logic/data.utils.d.ts +7 -0
  94. package/dist/types/components/geovaljaren/logic/history-service.d.ts +6 -0
  95. package/dist/types/components/geovaljaren/logic/search.engine.d.ts +7 -0
  96. package/dist/types/components/job-selector/ambient.d.ts +4 -0
  97. package/dist/types/components/job-selector/constants.d.ts +13 -0
  98. package/dist/types/components/job-selector/history-service.d.ts +6 -0
  99. package/dist/types/components/job-selector/job-selector-controller.d.ts +82 -0
  100. package/dist/types/components/job-selector/job-selector.d.ts +140 -0
  101. package/dist/types/components/job-selector/keyboard-handlers.d.ts +13 -0
  102. package/dist/types/components/job-selector/logic/adapter.utils.d.ts +12 -0
  103. package/dist/types/components/job-selector/logic/label-formatter.d.ts +4 -0
  104. package/dist/types/components/job-selector/logic/search.engine.d.ts +21 -0
  105. package/dist/types/components/job-selector/search-logic.d.ts +17 -0
  106. package/dist/types/components/job-selector/search.utils.d.ts +10 -0
  107. package/dist/types/components/job-selector/suggestions-list.d.ts +13 -0
  108. package/dist/types/components/job-selector/taxonomy-registry.d.ts +20 -0
  109. package/dist/types/components/job-selector/test-data.mock.d.ts +7 -0
  110. package/dist/types/components/job-selector/types.d.ts +107 -0
  111. package/dist/types/components/job-selector/validation-message.d.ts +8 -0
  112. package/dist/types/components/job-selector-demo/job-selector-demo.d.ts +55 -0
  113. package/dist/types/components/neobrutal-search/neobrutal-search.d.ts +18 -0
  114. package/dist/types/components.d.ts +425 -0
  115. package/dist/types/demo-data.d.ts +16 -0
  116. package/dist/types/index.d.ts +5 -0
  117. package/dist/types/stencil-public-runtime.d.ts +1873 -0
  118. package/dist/types/verkstad/verkstad.d.ts +1 -0
  119. package/dist/yrkesvaljaren/fuse-B0eHOxow.js.map +1 -0
  120. package/dist/yrkesvaljaren/index-Cy4BCo5s.js.map +1 -0
  121. package/dist/yrkesvaljaren/index-DfJXTQqL.js.map +1 -0
  122. package/dist/yrkesvaljaren/index.esm.js +1 -0
  123. package/dist/yrkesvaljaren/index.esm.js.map +1 -0
  124. package/dist/yrkesvaljaren/p-16636162.entry.js +1 -0
  125. package/dist/yrkesvaljaren/p-273cf6df.entry.js +1 -0
  126. package/dist/yrkesvaljaren/p-9e48afa4.entry.js +1 -0
  127. package/dist/yrkesvaljaren/p-BPnhMTul.js +1 -0
  128. package/dist/yrkesvaljaren/p-CGXZllj2.js +3 -0
  129. package/dist/yrkesvaljaren/p-DHUftoZJ.js +1 -0
  130. package/dist/yrkesvaljaren/p-DYFAzvqX.js +1 -0
  131. package/dist/yrkesvaljaren/p-Dw0VR9Aw.js +1 -0
  132. package/dist/yrkesvaljaren/p-aef7b5ae.entry.js +1 -0
  133. package/dist/yrkesvaljaren/p-b0f16abd.entry.js +1 -0
  134. package/dist/yrkesvaljaren/p-uBrs3sSs.js +1 -0
  135. package/dist/yrkesvaljaren/search.engine-DPT6roRm.js.map +1 -0
  136. package/dist/yrkesvaljaren/taxonomy-registry-s5khf6s4.js.map +1 -0
  137. package/dist/yrkesvaljaren/types-DHUftoZJ.js.map +1 -0
  138. package/dist/yrkesvaljaren/yrkesvaljaren.css +1 -0
  139. package/dist/yrkesvaljaren/yrkesvaljaren.esm.js +1 -0
  140. package/dist/yrkesvaljaren/yrkesvaljaren.esm.js.map +1 -0
  141. package/loader/cdn.js +1 -0
  142. package/loader/index.cjs.js +1 -0
  143. package/loader/index.d.ts +24 -0
  144. package/loader/index.es2017.js +1 -0
  145. package/loader/index.js +2 -0
  146. package/package.json +126 -0
@@ -0,0 +1,1617 @@
1
+ import { a as getElement, r as registerInstance, c as createEvent, h, F as Fragment } from './index-CGXZllj2.js';
2
+ import { T as TaxonomySearchEngine, D as DEBOUNCE_TIME, L as LabelFormatter, s as segmentsToHtml, M as MAX_RESULTS, a as TaxonomyAdapter, P as PLACEHOLDER_TEXT, b as MIN_INPUT_LENGTH, c as styleParentheticalText } from './search.engine-nZ7AcXba.js';
3
+ import { l as log, g as getRegisteredTaxonomy, i as isTaxonomyRegistration, r as registerTaxonomy } from './taxonomy-registry-Dw0VR9Aw.js';
4
+ import { I as InvalidSelectionError } from './types-DHUftoZJ.js';
5
+ import { A as AfValidationMessage } from './af-validation-message-F8NIH2rC.js';
6
+ import './fuse-BPnhMTul.js';
7
+
8
+ let memoryCache = null;
9
+ const HistoryService = {
10
+ saveHistory(history) {
11
+ memoryCache = history; // Omedelbar uppdatering av UI-state
12
+ try {
13
+ localStorage.setItem('job-selector_history', JSON.stringify(history));
14
+ log.debug('[HistoryService] saveHistory - History saved.');
15
+ }
16
+ catch (e) {
17
+ log.error('Kunde inte spara historik.', e);
18
+ }
19
+ },
20
+ loadHistory() {
21
+ if (memoryCache) {
22
+ return memoryCache;
23
+ }
24
+ try {
25
+ const historyString = localStorage.getItem('job-selector_history');
26
+ if (historyString) {
27
+ memoryCache = JSON.parse(historyString);
28
+ return memoryCache;
29
+ }
30
+ }
31
+ catch (e) {
32
+ log.error('Kunde inte läsa historik.', e);
33
+ }
34
+ memoryCache = [];
35
+ return memoryCache;
36
+ },
37
+ clearHistory() {
38
+ memoryCache = [];
39
+ try {
40
+ localStorage.removeItem('job-selector_history');
41
+ log.debug('[HistoryService] clearHistory - History cleared.');
42
+ }
43
+ catch (e) {
44
+ log.error('Kunde inte rensa historik.', e);
45
+ }
46
+ }
47
+ };
48
+
49
+ class JobSelectorController {
50
+ callbacks;
51
+ config;
52
+ state = { status: 'loading' };
53
+ taxonomyData = [];
54
+ popularItems = [];
55
+ // REPLACED: private fuseInstance: Fuse<TaxonomyItem> | null = null;
56
+ searchEngine = null;
57
+ debounceTimer;
58
+ validationTimer;
59
+ isTouched = false;
60
+ activeErrorType = null;
61
+ constructor(callbacks, config) {
62
+ this.callbacks = callbacks;
63
+ this.config = config;
64
+ }
65
+ // --- PUBLIC API ---
66
+ getState() {
67
+ return this.state;
68
+ }
69
+ getActiveErrorType() {
70
+ return this.activeErrorType;
71
+ }
72
+ // --- CONFIG UPDATE ---
73
+ updateConfig(newConfig) {
74
+ this.config = { ...this.config, ...newConfig };
75
+ // Kör validering om reglerna för obligatoriskt fält eller fritext ändras.
76
+ if (newConfig.required !== undefined || newConfig.requireSelection !== undefined) {
77
+ this.runValidation();
78
+ }
79
+ }
80
+ getTaxonomyData() {
81
+ // Return internal data if already loaded
82
+ if (this.taxonomyData.length > 0)
83
+ return this.taxonomyData;
84
+ // Try to fetch from registry (handles late registration)
85
+ const registeredData = getRegisteredTaxonomy();
86
+ if (registeredData && registeredData.length > 0) {
87
+ log.debug('[Controller] getTaxonomyData - Late initializing from registry');
88
+ this.taxonomyData = registeredData;
89
+ this.popularItems = this.computePopularItems(this.taxonomyData);
90
+ this.searchEngine = new TaxonomySearchEngine(this.taxonomyData);
91
+ }
92
+ return this.taxonomyData;
93
+ }
94
+ destroy() {
95
+ if (this.debounceTimer) {
96
+ clearTimeout(this.debounceTimer);
97
+ this.debounceTimer = undefined;
98
+ }
99
+ if (this.validationTimer) {
100
+ clearTimeout(this.validationTimer);
101
+ this.validationTimer = undefined;
102
+ }
103
+ }
104
+ // --- INITIALIZATION ---
105
+ async initialize(initialData) {
106
+ log.debug('[Controller] initialize - START', { hasInitialData: !!initialData });
107
+ this.updateState({ status: 'loading' });
108
+ const registeredData = (initialData && initialData.length > 0) ? initialData : getRegisteredTaxonomy();
109
+ if (registeredData && registeredData.length > 0) {
110
+ log.debug('[Controller] initialize - Loaded taxonomy from registry');
111
+ this.setStateToActive(registeredData, 'registry', new Date().toISOString());
112
+ }
113
+ else {
114
+ log.warn('[Controller] initialize - No taxonomy found in registry. Has registerTaxonomy() been called?');
115
+ this.updateState({ status: 'error', message: 'Kunde inte hitta yrkesdata. Kontrollera att komponenten är initierad korrekt.' });
116
+ }
117
+ }
118
+ handleFocus() {
119
+ this.isTouched = true;
120
+ }
121
+ setStateToActive(data, source, sourceDate) {
122
+ this.taxonomyData = data;
123
+ this.popularItems = this.computePopularItems(this.taxonomyData);
124
+ // NEW: Initialize the separated SearchEngine
125
+ this.searchEngine = new TaxonomySearchEngine(this.taxonomyData);
126
+ // OLD: this.fuseInstance = new Fuse(this.taxonomyData, FUSE_OPTIONS);
127
+ const commonState = { dataSource: source, dataSourceDate: sourceDate };
128
+ if (this.config.enableMultiple) {
129
+ this.updateState({
130
+ status: 'multiSelect',
131
+ inputValue: '',
132
+ selectedItems: [],
133
+ suggestions: [],
134
+ validation: 'idle',
135
+ ...commonState
136
+ });
137
+ return;
138
+ }
139
+ this.updateState({ status: 'searching', inputValue: '', suggestions: [], validation: 'idle', ...commonState });
140
+ }
141
+ // --- SEARCH ---
142
+ handleInput(value) {
143
+ this.isTouched = true;
144
+ log.debug('[Controller] handleInput - Value:', value);
145
+ // Reset validation on input
146
+ const wasInError = this.state.validation === 'error';
147
+ const validationContext = wasInError || this.state.validationContext === 'from-error' ? 'from-error' : undefined;
148
+ clearTimeout(this.debounceTimer);
149
+ if (value.trim() === '') {
150
+ if (this.config.enableMultiple) {
151
+ const currentSelected = 'selectedItems' in this.state ? this.state.selectedItems : [];
152
+ this.updateState({
153
+ status: 'multiSelect',
154
+ inputValue: '',
155
+ selectedItems: currentSelected,
156
+ suggestions: [],
157
+ validation: 'idle',
158
+ validationContext, // BEVARA CONTEXT
159
+ ...this.getCommonState()
160
+ });
161
+ }
162
+ else {
163
+ this.updateState({
164
+ status: 'searching',
165
+ inputValue: '',
166
+ suggestions: [],
167
+ validation: 'idle',
168
+ validationContext, // BEVARA CONTEXT
169
+ ...this.getCommonState()
170
+ });
171
+ }
172
+ this.showHistory();
173
+ return;
174
+ }
175
+ switch (this.state.status) {
176
+ case 'multiSelect':
177
+ case 'multiSelectShowingHistory': {
178
+ const selectedItems = 'selectedItems' in this.state ? this.state.selectedItems : [];
179
+ this.updateState({
180
+ status: 'multiSelect',
181
+ inputValue: value,
182
+ selectedItems,
183
+ suggestions: 'suggestions' in this.state ? this.state.suggestions : ('historyItems' in this.state ? this.state.historyItems : []),
184
+ validation: 'idle',
185
+ validationContext,
186
+ dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
187
+ dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
188
+ });
189
+ break;
190
+ }
191
+ case 'itemSelected':
192
+ case 'searching':
193
+ case 'showingHistory': {
194
+ this.updateState({
195
+ status: 'searching',
196
+ inputValue: value,
197
+ suggestions: 'suggestions' in this.state ? this.state.suggestions : ('historyItems' in this.state ? this.state.historyItems : []),
198
+ validation: 'idle',
199
+ validationContext,
200
+ dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
201
+ dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
202
+ });
203
+ break;
204
+ }
205
+ case 'loading':
206
+ case 'error':
207
+ return; // Interaction not allowed or ignored
208
+ default: {
209
+ const _exhaustive = this.state;
210
+ return _exhaustive;
211
+ }
212
+ }
213
+ if (value.length >= this.config.minLength) {
214
+ this.debounceTimer = setTimeout(() => this.performSearch(value), DEBOUNCE_TIME);
215
+ }
216
+ }
217
+ performSearch(value) {
218
+ if (!this.searchEngine)
219
+ return;
220
+ switch (this.state.status) {
221
+ case 'searching':
222
+ case 'multiSelect': {
223
+ const genericResults = this.searchEngine.searchAsGeneric(value, this.config.maxResults);
224
+ const results = genericResults.flatMap(g => g.metadata?.originalResult ? [g.metadata.originalResult] : []);
225
+ // Do not filter out selected items here - allow UI to handle grey-out
226
+ this.updateState({ ...this.state, suggestions: results });
227
+ break;
228
+ }
229
+ case 'loading':
230
+ case 'error':
231
+ case 'itemSelected':
232
+ case 'showingHistory':
233
+ case 'multiSelectShowingHistory':
234
+ return; // Nothing to search from here
235
+ default: {
236
+ const _exhaustive = this.state;
237
+ return _exhaustive;
238
+ }
239
+ }
240
+ }
241
+ // --- HISTORY HANDLING ---
242
+ addToHistory(item) {
243
+ if (!this.config.history)
244
+ return;
245
+ // Reload history from cache to ensure we have the latest state from other instances
246
+ const historyItems = HistoryService.loadHistory();
247
+ // Clone item and reset highlightedLabel to displayLabel (removes bolding)
248
+ // AND apply grey styling to parenthesis (via DisplaySegments)
249
+ const unhighlightedSegments = LabelFormatter.format(item.rawLabel, '', item.occupationLabel);
250
+ const historyItem = {
251
+ ...item,
252
+ displaySegments: unhighlightedSegments,
253
+ // Legacy support: construct HTML string for fallback
254
+ highlightedLabel: segmentsToHtml(unhighlightedSegments)
255
+ };
256
+ // Filter out existing item with same ID (to move it to top)
257
+ const filtered = historyItems.filter((h) => {
258
+ if (h.id && historyItem.id)
259
+ return h.id !== historyItem.id;
260
+ return h.rawLabel !== historyItem.rawLabel;
261
+ });
262
+ // Add to top and limit
263
+ const newHistory = [historyItem, ...filtered].slice(0, this.config.maxHistoryItems);
264
+ HistoryService.saveHistory(newHistory);
265
+ }
266
+ clearHistory() {
267
+ HistoryService.clearHistory();
268
+ this.showHistory(true);
269
+ }
270
+ computePopularItems(data, limit = this.config.maxResults || MAX_RESULTS) {
271
+ if (!data || data.length === 0)
272
+ return [];
273
+ const hasWeights = data.some(t => typeof t.weight === 'number' && t.weight > 0);
274
+ if (!hasWeights) {
275
+ return data.slice(0, limit);
276
+ }
277
+ const sorted = [...data].sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0));
278
+ const seen = new Set();
279
+ const popular = [];
280
+ for (const item of sorted) {
281
+ const key = item.occupation_name_preferred_label || item.preferred_label;
282
+ if (!seen.has(key)) {
283
+ seen.add(key);
284
+ popular.push(item);
285
+ }
286
+ if (popular.length === limit)
287
+ break;
288
+ }
289
+ return popular;
290
+ }
291
+ showHistory(force = false) {
292
+ if (this.state.status === 'loading' || this.state.status === 'error')
293
+ return;
294
+ if (!this.config.history)
295
+ return;
296
+ const historyItems = HistoryService.loadHistory();
297
+ let displayItems = historyItems;
298
+ let isPopular = false;
299
+ if (historyItems.length === 0) {
300
+ if (this.config.popular === false) {
301
+ displayItems = [];
302
+ }
303
+ else {
304
+ if (this.popularItems.length === 0 && this.taxonomyData.length > 0) {
305
+ this.popularItems = this.computePopularItems(this.taxonomyData);
306
+ }
307
+ // Provide some popular items if history is empty (and we have taxonomy data)
308
+ displayItems = this.popularItems.map(t => this.toSearchResult(t));
309
+ isPopular = true;
310
+ }
311
+ }
312
+ // Lazy Validation: Filter items against current taxonomy and config
313
+ const validatedHistory = displayItems.filter((item) => {
314
+ // 1. Check if it's a ManualInput item
315
+ if (item.type === 'ManualInput') {
316
+ return !this.config.requireSelection;
317
+ }
318
+ // 2. Check if it's a valid Taxonomy item (must exist in current data)
319
+ // Note: We use this.taxonomyData which is already loaded
320
+ const exists = this.taxonomyData.some(t => t.id === item.id);
321
+ return exists;
322
+ });
323
+ const filteredHistory = validatedHistory.map((item) => {
324
+ const unhighlightedSegments = LabelFormatter.format(item.rawLabel, '', item.occupationLabel);
325
+ return {
326
+ ...item,
327
+ displaySegments: unhighlightedSegments,
328
+ highlightedLabel: segmentsToHtml(unhighlightedSegments)
329
+ };
330
+ });
331
+ const { dataSource, dataSourceDate } = this.getCommonState();
332
+ const currentInputValue = 'inputValue' in this.state ? this.state.inputValue : '';
333
+ // If we have input text, we don't want to show history unless explicitly in a history state
334
+ // OR if we are forcing it (e.g. from editSelection)
335
+ if (!force && currentInputValue.trim() !== '' && this.state.status !== 'showingHistory' && this.state.status !== 'multiSelectShowingHistory') {
336
+ return;
337
+ }
338
+ if (this.config.enableMultiple) {
339
+ const selectedItems = 'selectedItems' in this.state ? this.state.selectedItems : [];
340
+ const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
341
+ const validationContext = wasInError ? 'from-error' : undefined;
342
+ const validation = 'validation' in this.state ? this.state.validation : 'idle';
343
+ this.updateState({
344
+ status: 'multiSelectShowingHistory',
345
+ selectedItems,
346
+ historyItems: filteredHistory,
347
+ inputValue: currentInputValue,
348
+ validation,
349
+ validationContext,
350
+ dataSource,
351
+ dataSourceDate,
352
+ isPopularView: isPopular // Flag for view
353
+ });
354
+ }
355
+ else {
356
+ const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
357
+ const validationContext = wasInError ? 'from-error' : undefined;
358
+ const validation = 'validation' in this.state ? this.state.validation : 'idle';
359
+ this.updateState({
360
+ status: 'showingHistory',
361
+ historyItems: filteredHistory,
362
+ inputValue: currentInputValue,
363
+ validation,
364
+ validationContext,
365
+ dataSource,
366
+ dataSourceDate,
367
+ isPopularView: isPopular // Flag for view
368
+ });
369
+ }
370
+ }
371
+ // --- SELECTION ---
372
+ selectItem(item) {
373
+ log.debug('[Controller] selectItem', item);
374
+ // Add to history
375
+ this.isTouched = true;
376
+ this.addToHistory(item);
377
+ // Standardized Success Message Logic:
378
+ // Check for error state before update to allow transition to success.
379
+ // This applies to ALL selection events (Single, Multi, History, Search).
380
+ const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
381
+ const nextValidationContext = wasInError ? 'from-error' : undefined;
382
+ switch (this.state.status) {
383
+ case 'multiSelect':
384
+ case 'multiSelectShowingHistory': {
385
+ const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
386
+ const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
387
+ const currentSelected = 'selectedItems' in this.state ? this.state.selectedItems : [];
388
+ // Prevent duplicates
389
+ const existingIndex = currentSelected.findIndex(existing => {
390
+ if (existing.id && item.id)
391
+ return existing.id === item.id;
392
+ return existing.rawLabel === item.rawLabel;
393
+ });
394
+ if (existingIndex > -1) {
395
+ // Already selected - skip to avoid double addition or accidental removal in this context
396
+ return;
397
+ }
398
+ const newItems = [...currentSelected, item];
399
+ // Uppdatera state: rensa input och förslag
400
+ this.updateState({
401
+ ...this.state,
402
+ status: 'multiSelect',
403
+ selectedItems: newItems,
404
+ inputValue: '',
405
+ suggestions: [],
406
+ validation: 'idle',
407
+ validationContext: nextValidationContext, // Use centralized context
408
+ dataSource,
409
+ dataSourceDate
410
+ });
411
+ this.callbacks.onEmitSelection(newItems.map(i => this.toPublicSelection(i)));
412
+ this.showHistory(true);
413
+ break;
414
+ }
415
+ case 'searching':
416
+ case 'showingHistory': {
417
+ const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
418
+ const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
419
+ this.updateState({
420
+ status: 'itemSelected',
421
+ selectedItem: item,
422
+ inputValue: item.preferred_label, // Keep label as text
423
+ validation: 'idle',
424
+ validationContext: nextValidationContext, // Use centralized context
425
+ dataSource,
426
+ dataSourceDate,
427
+ });
428
+ this.callbacks.onEmitSelection(this.toPublicSelection(item));
429
+ if (this.config.enableMultiple) {
430
+ this.showHistory(true);
431
+ }
432
+ break;
433
+ }
434
+ case 'loading':
435
+ case 'error':
436
+ // Do nothing in these states
437
+ break;
438
+ case 'itemSelected': {
439
+ const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
440
+ const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
441
+ this.updateState({
442
+ ...this.state,
443
+ status: 'itemSelected',
444
+ selectedItem: item,
445
+ inputValue: item.preferred_label,
446
+ validation: 'idle',
447
+ validationContext: nextValidationContext,
448
+ dataSource,
449
+ dataSourceDate,
450
+ });
451
+ this.callbacks.onEmitSelection(this.toPublicSelection(item));
452
+ break;
453
+ }
454
+ default: {
455
+ const _exhaustive = this.state;
456
+ log.warn('[Controller] selectItem - Unexpected state:', _exhaustive);
457
+ }
458
+ }
459
+ this.runValidation();
460
+ }
461
+ clearSelection() {
462
+ if (this.state.status !== 'itemSelected')
463
+ return;
464
+ this.isTouched = true;
465
+ const currentInputValue = this.state.inputValue || '';
466
+ this.updateState({
467
+ status: 'searching',
468
+ inputValue: currentInputValue,
469
+ suggestions: [],
470
+ validation: 'idle',
471
+ ...this.getCommonState()
472
+ });
473
+ this.callbacks.onEmitSelection([]); // Empty array instead of null
474
+ // Try to show history if enabled
475
+ this.showHistory();
476
+ this.runValidation();
477
+ }
478
+ removeItem(item) {
479
+ switch (this.state.status) {
480
+ case 'multiSelect':
481
+ case 'multiSelectShowingHistory': {
482
+ const newItems = this.state.selectedItems.filter(i => {
483
+ if (i.id && item.id)
484
+ return i.id !== item.id;
485
+ return i.rawLabel !== item.rawLabel;
486
+ });
487
+ this.isTouched = true;
488
+ this.updateState({ ...this.state, selectedItems: newItems });
489
+ this.callbacks.onEmitSelection(newItems.map(i => this.toPublicSelection(i)));
490
+ // Try to show history if enabled (since we are likely still focused)
491
+ const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
492
+ if (inputValue === '') {
493
+ this.showHistory();
494
+ }
495
+ this.runValidation();
496
+ break;
497
+ }
498
+ }
499
+ }
500
+ // --- BLUR & VALIDATION ---
501
+ handleBlur() {
502
+ this.isTouched = true;
503
+ if (this.state.status === 'searching') {
504
+ const trimmedValue = (this.state.inputValue || '').trim();
505
+ if (trimmedValue !== '') {
506
+ const exactMatch = this.findExactMatch(trimmedValue);
507
+ if (exactMatch) {
508
+ this.selectItem(this.toSearchResult(exactMatch));
509
+ return;
510
+ }
511
+ }
512
+ }
513
+ // In TEA/FSM, handleBlur should focus on state transitions and validation,
514
+ // rather than side-effects like clearing UI data.
515
+ // We transition back to primary states (searching/multiSelect) but KEEP suggestions.
516
+ switch (this.state.status) {
517
+ case 'searching':
518
+ case 'showingHistory':
519
+ this.updateState({
520
+ ...this.state,
521
+ status: 'searching',
522
+ inputValue: 'inputValue' in this.state ? this.state.inputValue : '',
523
+ suggestions: []
524
+ });
525
+ break;
526
+ case 'multiSelect':
527
+ case 'multiSelectShowingHistory': {
528
+ this.updateState({
529
+ ...this.state,
530
+ status: 'multiSelect',
531
+ inputValue: 'inputValue' in this.state ? this.state.inputValue : '',
532
+ suggestions: []
533
+ });
534
+ break;
535
+ }
536
+ case 'loading':
537
+ case 'error':
538
+ case 'itemSelected':
539
+ break;
540
+ default: {
541
+ const _exhaustive = this.state;
542
+ return _exhaustive;
543
+ }
544
+ }
545
+ this.runValidation();
546
+ }
547
+ /**
548
+ * Validerar nuvarande värde och sätter eventuella valideringsfel.
549
+ * @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.
550
+ */
551
+ runValidation(forceTouch = false) {
552
+ if (forceTouch) {
553
+ this.isTouched = true;
554
+ }
555
+ if (!this.isTouched)
556
+ return true;
557
+ if (this.state.status === 'loading' || this.state.status === 'error')
558
+ return true;
559
+ const hasValue = this.checkHasValue();
560
+ let isValid = true;
561
+ let newErrorType = null;
562
+ // Check for invalid input (text that doesn't match a selection)
563
+ // This applies in both single 'searching' mode and 'multiSelect' mode
564
+ const isSearchingOrMulti = this.state.status === 'searching' || this.state.status === 'multiSelect' || this.state.status === 'multiSelectShowingHistory';
565
+ const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
566
+ const hasInputText = inputValue.trim() !== '';
567
+ if (isSearchingOrMulti && hasInputText) {
568
+ // New rule: If there is text in the box that isn't confirmed as a selection, it's ALWAYS an error
569
+ // regardless of whether manual input is enabled or not.
570
+ // The user MUST confirm by selecting the manual input option.
571
+ isValid = false;
572
+ newErrorType = 'invalidInput';
573
+ }
574
+ else if (this.config.required && !hasValue) {
575
+ isValid = false;
576
+ newErrorType = 'required';
577
+ }
578
+ this.activeErrorType = newErrorType;
579
+ // Strict FSM for validation state
580
+ // Rules:
581
+ // 1. Error only if invalid.
582
+ // 2. Idle if valid (never use success to avoid aggressive green checks).
583
+ // Valid state: only show success if recovering from an error (Red -> Green pattern) AND we actually have a value.
584
+ const wasInError = this.state.validation === 'error' || this.state.validationContext === 'from-error';
585
+ let validationState;
586
+ if (!isValid) {
587
+ validationState = newErrorType === 'invalidInput' || forceTouch || wasInError ? 'error' : 'idle';
588
+ }
589
+ else if (wasInError && hasValue) {
590
+ validationState = 'success';
591
+ }
592
+ else {
593
+ validationState = 'idle';
594
+ }
595
+ if (validationState === 'success') {
596
+ clearTimeout(this.validationTimer);
597
+ this.validationTimer = setTimeout(() => {
598
+ // Only clear if still success (user hasn't messed up again)
599
+ const isSuccess = 'validation' in this.state && this.state.validation === 'success';
600
+ const allowedStatus = ['itemSelected', 'multiSelect', 'multiSelectShowingHistory'].includes(this.state.status);
601
+ if (allowedStatus && isSuccess) {
602
+ this.updateState({ ...this.state, validation: 'idle', validationContext: undefined });
603
+ }
604
+ this.validationTimer = undefined;
605
+ }, 3000);
606
+ }
607
+ // Update state based on current status (respecting type definitions)
608
+ switch (this.state.status) {
609
+ case 'searching':
610
+ this.updateState({ ...this.state, validation: validationState });
611
+ break;
612
+ case 'showingHistory':
613
+ this.updateState({ ...this.state, validation: validationState });
614
+ break;
615
+ case 'multiSelect':
616
+ // Allow error AND success states for multiSelect
617
+ this.updateState({ ...this.state, validation: validationState });
618
+ break;
619
+ case 'multiSelectShowingHistory':
620
+ this.updateState({ ...this.state, validation: validationState });
621
+ break;
622
+ case 'itemSelected':
623
+ // ItemSelected does not support 'error'. But checkHasValue() returns true, so isValid is true.
624
+ if (validationState !== 'error') {
625
+ this.updateState({ ...this.state, validation: validationState });
626
+ }
627
+ break;
628
+ default: {
629
+ // If we reach here, we must have handled all states that aren't 'loading' or 'error'
630
+ // But TypeScript needs to know that.
631
+ const _exhaustive = this.state;
632
+ return _exhaustive;
633
+ }
634
+ }
635
+ this.callbacks.onEmitValidation({ isValid, errorType: newErrorType });
636
+ return isValid;
637
+ }
638
+ selectItemById(id) {
639
+ if (!id || this.config.enableMultiple || !this.taxonomyData)
640
+ return;
641
+ const itemToSelect = this.taxonomyData.find(t => t.id === id);
642
+ if (itemToSelect) {
643
+ const searchResult = this.toSearchResult(itemToSelect);
644
+ const dataSource = 'dataSource' in this.state ? this.state.dataSource : undefined;
645
+ const dataSourceDate = 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined;
646
+ this.updateState({
647
+ status: 'itemSelected',
648
+ selectedItem: searchResult,
649
+ inputValue: searchResult.preferred_label,
650
+ validation: 'idle',
651
+ dataSource,
652
+ dataSourceDate,
653
+ });
654
+ }
655
+ this.runValidation();
656
+ }
657
+ selectItemsByIds(ids) {
658
+ if (!ids || !this.config.enableMultiple || !this.taxonomyData)
659
+ return;
660
+ const taxonomyItems = ids.map(id => this.taxonomyData.find(item => item.id === id));
661
+ if (taxonomyItems.some((item) => !item))
662
+ return;
663
+ const newSelectedItems = taxonomyItems
664
+ .filter((item) => !!item)
665
+ .map(item => this.toSearchResult(item));
666
+ switch (this.state.status) {
667
+ case 'multiSelect':
668
+ case 'multiSelectShowingHistory':
669
+ this.updateState({
670
+ ...this.state,
671
+ selectedItems: newSelectedItems
672
+ });
673
+ break;
674
+ }
675
+ this.runValidation();
676
+ }
677
+ setSelection(selection) {
678
+ const invalidIds = [];
679
+ const selectedItems = selection.map((item) => {
680
+ if (item.id !== null) {
681
+ const taxonomyItem = this.taxonomyData.find((taxonomy) => taxonomy.id === item.id);
682
+ if (!taxonomyItem) {
683
+ invalidIds.push(item.id);
684
+ return undefined;
685
+ }
686
+ return this.toSearchResult(taxonomyItem);
687
+ }
688
+ return this.createManualInputItem(item.preferred_label);
689
+ });
690
+ if (invalidIds.length > 0) {
691
+ return { ok: false, invalidIds };
692
+ }
693
+ const resolvedItems = selectedItems;
694
+ const commonState = this.getCommonState();
695
+ if (this.config.enableMultiple) {
696
+ this.updateState({
697
+ ...this.state,
698
+ status: 'multiSelect',
699
+ selectedItems: resolvedItems,
700
+ inputValue: '',
701
+ suggestions: [],
702
+ validation: 'idle',
703
+ ...commonState
704
+ });
705
+ }
706
+ else if (resolvedItems.length === 1) {
707
+ const selectedItem = resolvedItems[0];
708
+ this.updateState({
709
+ status: 'itemSelected',
710
+ selectedItem,
711
+ inputValue: selectedItem.preferred_label,
712
+ validation: 'idle',
713
+ ...commonState
714
+ });
715
+ }
716
+ else {
717
+ this.updateState({
718
+ status: 'searching',
719
+ inputValue: '',
720
+ suggestions: [],
721
+ validation: 'idle',
722
+ ...commonState
723
+ });
724
+ }
725
+ this.runValidation();
726
+ return { ok: true, selection: resolvedItems.map((item) => this.toPublicSelection(item)) };
727
+ }
728
+ editSelection() {
729
+ const selectedItem = this.state.selectedItem;
730
+ if (this.state.status !== 'itemSelected' || !selectedItem || !this.searchEngine)
731
+ return;
732
+ // Transition to searching first to ensure input shows up
733
+ const professionLabel = selectedItem.displayLabel;
734
+ const { dataSource, dataSourceDate } = this.state;
735
+ // Rule: On activation, show search results for the current string instead of history
736
+ const genericResults = this.searchEngine.searchAsGeneric(professionLabel, this.config.maxResults);
737
+ const searchResults = genericResults.flatMap(g => g.metadata?.originalResult ? [g.metadata.originalResult] : []);
738
+ this.updateState({
739
+ status: 'searching',
740
+ inputValue: professionLabel,
741
+ suggestions: searchResults,
742
+ validation: 'idle',
743
+ dataSource,
744
+ dataSourceDate
745
+ });
746
+ }
747
+ checkHasValue() {
748
+ switch (this.state.status) {
749
+ case 'itemSelected':
750
+ return true;
751
+ case 'multiSelect':
752
+ case 'multiSelectShowingHistory':
753
+ return this.state.selectedItems.length > 0;
754
+ case 'searching':
755
+ case 'showingHistory': {
756
+ const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
757
+ return !this.config.requireSelection && inputValue.trim() !== '';
758
+ }
759
+ case 'loading':
760
+ case 'error':
761
+ return false;
762
+ default: {
763
+ const _exhaustive = this.state;
764
+ return _exhaustive;
765
+ }
766
+ }
767
+ }
768
+ getCommonState() {
769
+ return {
770
+ dataSource: 'dataSource' in this.state ? this.state.dataSource : undefined,
771
+ dataSourceDate: 'dataSourceDate' in this.state ? this.state.dataSourceDate : undefined
772
+ };
773
+ }
774
+ resetMultiSelection() {
775
+ const commonState = this.getCommonState();
776
+ this.updateState({
777
+ ...this.state,
778
+ status: 'multiSelect',
779
+ selectedItems: [],
780
+ inputValue: '',
781
+ suggestions: [],
782
+ validation: 'idle',
783
+ ...commonState
784
+ });
785
+ this.callbacks.onEmitSelection([]);
786
+ }
787
+ // --- HELPERS ---
788
+ updateState(newState) {
789
+ this.state = newState;
790
+ this.callbacks.onStateChange(newState);
791
+ }
792
+ toSearchResult(item) {
793
+ return {
794
+ ...item,
795
+ rawLabel: item.preferred_label,
796
+ displayLabel: item.occupation_name_preferred_label
797
+ ? `${item.preferred_label} (${item.occupation_name_preferred_label})`
798
+ : item.preferred_label,
799
+ highlightedLabel: item.preferred_label,
800
+ score: 3.0,
801
+ // Map v2 fields to match SearchResult interface expectations if needed
802
+ occupationLabel: item.occupation_name_preferred_label,
803
+ occupationId: item.occupation_name_id,
804
+ ssyk_level_4_id: item.ssyk_level_4_id,
805
+ ssyk_code_2012: item.ssyk_code_2012
806
+ };
807
+ }
808
+ toPublicSelection(item) {
809
+ return TaxonomyAdapter.toPublicSelection(item);
810
+ }
811
+ createManualInputItem(value) {
812
+ return {
813
+ id: '',
814
+ type: 'ManualInput',
815
+ preferred_label: value,
816
+ rawLabel: value,
817
+ displayLabel: value,
818
+ highlightedLabel: value,
819
+ score: 1.0
820
+ };
821
+ }
822
+ findExactMatch(value) {
823
+ return this.taxonomyData.find(item => {
824
+ const displayLabel = item.occupation_name_preferred_label
825
+ ? `${item.preferred_label} (${item.occupation_name_preferred_label})`
826
+ : item.preferred_label;
827
+ return displayLabel.toLowerCase() === value.toLowerCase() || item.preferred_label.toLowerCase() === value.toLowerCase();
828
+ });
829
+ }
830
+ // --- FREETEXT HANDLING (used by keyboard handler) ---
831
+ handleFreeTextSelection(value) {
832
+ if (!value)
833
+ return;
834
+ this.isTouched = true;
835
+ const exactMatch = this.findExactMatch(value);
836
+ if (exactMatch) {
837
+ this.selectItem(this.toSearchResult(exactMatch));
838
+ }
839
+ else {
840
+ this.selectItem(this.createManualInputItem(value));
841
+ }
842
+ }
843
+ loadMore() {
844
+ // Increment maxResults or handle paging if supported by searchEngine
845
+ this.config.maxResults += 10;
846
+ const inputValue = 'inputValue' in this.state ? this.state.inputValue : '';
847
+ if (inputValue) {
848
+ this.performSearch(inputValue);
849
+ }
850
+ }
851
+ resetSearch() {
852
+ switch (this.state.status) {
853
+ case 'searching':
854
+ case 'multiSelect':
855
+ this.updateState({ ...this.state, inputValue: '', suggestions: [] });
856
+ this.showHistory(true);
857
+ break;
858
+ case 'showingHistory':
859
+ case 'multiSelectShowingHistory':
860
+ // Already showing history, just ensure input is empty
861
+ this.updateState({ ...this.state, inputValue: '' });
862
+ break;
863
+ case 'itemSelected':
864
+ this.clearSelection();
865
+ break;
866
+ }
867
+ }
868
+ }
869
+
870
+ const jobSelectorCss = () => `:host{--digi--grayscale-0:#ffffff;--digi--grayscale-50:#fbfbfb;--digi--grayscale-100:#f4f4f4;--digi--grayscale-200:#ededed;--digi--grayscale-300:#e4e4e4;--digi--grayscale-400:#dddddd;--digi--grayscale-500:#cacaca;--digi--grayscale-600:#acacac;--digi--grayscale-700:#757575;--digi--grayscale-800:#5c5c5c;--digi--grayscale-900:#333333;--digi--grayscale-1000:#000000;--digi--global--color--cta--link--base:#1616b2;--digi--global--color--function--danger--base:#da0000;--digi--global--color--function--success--base:#008a20;--digi--global--color--function--success--lightest-3:#d4edda;--digi--global--color--function--warning--base:#ffe200;--digi--color--text--primary:var(--digi--grayscale-900);--digi--color--text--secondary:var(--digi--grayscale-700);--digi--color--text--link:var(--digi--global--color--cta--link--base);--digi--color--icons--success:var(--digi--global--color--function--success--base);--digi--color--icons--warning:#8a7a00;--digi--color--icons--danger:var(--digi--global--color--function--danger--base);--digi--global--typography--font-family--default:"Open Sans", Arial, sans-serif;--digi--global--typography--font-size--base:1rem;--digi--global--typography--font-size--small:0.875rem;--digi--global--typography--font-weight--regular:400;--digi--global--typography--font-weight--semibold:600;--digi--global--typography--font-weight--bold:700;--digi--global--typography--line-height--base:1.5;--digi--typography--label--font-size--desktop:1rem;--digi--typography--label--font-weight--desktop:600;--digi--typography--label--line-height--desktop:1.5;--digi--typography--label-description--font-size--desktop:1rem;--digi--typography--label-description--line-height--desktop:1.5;--digi--typography--tag--font-size--desktop:0.875rem;--digi--gutter--icon:0.625rem;--digi--gutter--smaller:0.75rem;--digi--gutter--small:0.875rem;--digi--gutter--medium:1rem;--digi--border-width--primary:1px;--digi--border-style--primary:solid;--digi--border-radius--primary:0;--digi--border-radius--secondary:6px;--af-tag-color:#091d54;--af-tag-hover-background:#ddf1fc}.af-form-label{color:var(--digi--color--text--primary);display:flex;flex-direction:column;font-family:var(--digi--global--typography--font-family--default);font-size:var(--digi--typography--label--font-size--desktop);margin-bottom:0.5rem}.af-form-label__label{cursor:pointer;display:block;font-weight:var(--digi--typography--label--font-weight--desktop);line-height:var(--digi--typography--label--line-height--desktop)}.af-form-label__description{display:block;font-size:var(--digi--typography--label-description--font-size--desktop);line-height:var(--digi--typography--label-description--line-height--desktop);max-width:35rem}.af-validation-message{align-items:center;display:flex;flex-wrap:wrap}.af-validation-message__icon{flex:0 0 auto;height:1.25em;margin-inline-end:var(--digi--gutter--icon);width:1.25em}.af-validation-message__icon--success,.af-validation-message__icon--error{background-color:var(--af-validation-icon-color, currentColor);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.af-validation-message__icon--success{--af-validation-icon-color:var(--digi--color--icons--success);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 25 25'%3E%3Cpath d='M25 12.5C25 19.404 19.404 25 12.5 25S0 19.404 0 12.5 5.596 0 12.5 0 25 5.596 25 12.5zm-13.946 6.619l9.274-9.275a.806.806 0 000-1.14l-1.14-1.14a.806.806 0 00-1.14 0l-7.564 7.563-3.531-3.531a.807.807 0 00-1.14 0l-1.141 1.14a.806.806 0 000 1.14l5.242 5.243a.806.806 0 001.14 0z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 25 25'%3E%3Cpath d='M25 12.5C25 19.404 19.404 25 12.5 25S0 19.404 0 12.5 5.596 0 12.5 0 25 5.596 25 12.5zm-13.946 6.619l9.274-9.275a.806.806 0 000-1.14l-1.14-1.14a.806.806 0 00-1.14 0l-7.564 7.563-3.531-3.531a.807.807 0 00-1.14 0l-1.141 1.14a.806.806 0 000 1.14l5.242 5.243a.806.806 0 001.14 0z'/%3E%3C/svg%3E")}.af-validation-message__icon--error{--af-validation-icon-color:var(--digi--color--icons--danger);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath d='M34.4328058,0 C35.5494848,0 36.5900526,0.609146757 37.1483921,1.58958113 L47.5812454,19.9104189 C48.1395849,20.8909496 48.1395849,22.1090504 47.5812454,23.0895811 L37.1483921,41.4104189 C36.5900526,42.3908532 35.5494848,43 34.4328058,43 L13.5671942,43 C12.4505152,43 11.4099474,42.3908532 10.8516079,41.4104189 L0.41875464,23.0895811 C-0.13958488,22.1090504 -0.13958488,20.8909496 0.41875464,19.9104189 L10.8516079,1.58958113 C11.4099474,0.609146757 12.4505152,0 13.5671942,0 Z M18.6736823,12.3728178 C18.2711762,11.964729 17.6203819,11.964729 17.2178757,12.3728178 L15.2825964,14.334939 C14.8800903,14.7430278 14.8800903,15.4028485 15.2825964,15.8109373 L20.6089616,21.2111779 L15.2825964,26.6114185 C14.8800903,27.0195074 14.8800903,27.6793281 15.2825964,28.0874169 L17.2178757,30.049538 C17.6203819,30.4576269 18.2711762,30.4576269 18.6736823,30.049538 L24.0000475,24.6492974 L29.3264127,30.049538 C29.7289188,30.4576269 30.3797132,30.4576269 30.7822193,30.049538 L32.7174986,28.0874169 C33.1200048,27.6793281 33.1200048,27.0195074 32.7174986,26.6114185 L27.3911334,21.2111779 L32.7174986,15.8109373 C33.1200048,15.4028485 33.1200048,14.7430278 32.7174986,14.334939 L30.7822193,12.3728178 C30.3797132,11.964729 29.7289188,11.964729 29.3264127,12.3728178 L24.0000475,17.7730584 Z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath d='M34.4328058,0 C35.5494848,0 36.5900526,0.609146757 37.1483921,1.58958113 L47.5812454,19.9104189 C48.1395849,20.8909496 48.1395849,22.1090504 47.5812454,23.0895811 L37.1483921,41.4104189 C36.5900526,42.3908532 35.5494848,43 34.4328058,43 L13.5671942,43 C12.4505152,43 11.4099474,42.3908532 10.8516079,41.4104189 L0.41875464,23.0895811 C-0.13958488,22.1090504 -0.13958488,20.8909496 0.41875464,19.9104189 L10.8516079,1.58958113 C11.4099474,0.609146757 12.4505152,0 13.5671942,0 Z M18.6736823,12.3728178 C18.2711762,11.964729 17.6203819,11.964729 17.2178757,12.3728178 L15.2825964,14.334939 C14.8800903,14.7430278 14.8800903,15.4028485 15.2825964,15.8109373 L20.6089616,21.2111779 L15.2825964,26.6114185 C14.8800903,27.0195074 14.8800903,27.6793281 15.2825964,28.0874169 L17.2178757,30.049538 C17.6203819,30.4576269 18.2711762,30.4576269 18.6736823,30.049538 L24.0000475,24.6492974 L29.3264127,30.049538 C29.7289188,30.4576269 30.3797132,30.4576269 30.7822193,30.049538 L32.7174986,28.0874169 C33.1200048,27.6793281 33.1200048,27.0195074 32.7174986,26.6114185 L27.3911334,21.2111779 L32.7174986,15.8109373 C33.1200048,15.4028485 33.1200048,14.7430278 32.7174986,14.334939 L30.7822193,12.3728178 C30.3797132,11.964729 29.7289188,11.964729 29.3264127,12.3728178 L24.0000475,17.7730584 Z'/%3E%3C/svg%3E")}.af-validation-message__icon--warning{background:center / contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18'%3E%3Ccircle cx='9' cy='9' r='6.75' fill='%23333'/%3E%3Cpath d='m18 9c0 4.9717-4.0298 9-9 9-4.9702 0-9-4.0283-9-9 0-4.9688 4.0298-9 9-9 4.9702 0 9 4.0312 9 9zm-9 1.8145c-0.92196 0-1.6694 0.7474-1.6694 1.6694 0 0.92196 0.7474 1.6694 1.6694 1.6694s1.6694-0.7474 1.6694-1.6694c0-0.92196-0.7474-1.6694-1.6694-1.6694zm-1.3157-1.065c0.012593 0.23095 0.20355 0.41175 0.43483 0.41175h1.7618c0.23128 0 0.42224-0.1808 0.43483-0.41175l0.2692-4.9355c0.013609-0.24946-0.18501-0.45922-0.43483-0.45922h-2.3002c-0.24982 0-0.4484 0.20976-0.43479 0.45922l0.2692 4.9355z' fill='%23FFE200'/%3E%3C/svg%3E");position:relative;top:3px}.af-validation-message__text{color:var(--digi--color--text--primary);flex:1;font-family:var(--digi--global--typography--font-family--default);font-size:var(--digi--typography--tag--font-size--desktop);transform:translateY(1px)}.af-tag{display:block;max-width:100%;min-width:0}.af-tag__button{align-items:center;background:var(--digi--grayscale-0);border:var(--digi--border-width--primary) var(--digi--border-style--primary) var(--af-tag-color);border-radius:var(--digi--border-radius--secondary);color:var(--af-tag-color);cursor:pointer;display:inline-flex;font-family:var(--digi--global--typography--font-family--default);font-size:0.875rem;font-weight:var(--digi--global--typography--font-weight--semibold);gap:10px;height:auto;max-width:100%;min-height:unset;min-width:0;padding:6px 8px;text-align:left;white-space:normal}.af-tag__button:hover{background:var(--af-tag-hover-background)}.af-tag__button:focus-visible{background:var(--af-tag-hover-background);border-color:var(--af-tag-color);outline:2px solid var(--digi--grayscale-1000);outline-offset:2px;box-shadow:0 0 0 5px var(--digi--grayscale-0)}.af-tag__button:active{background:var(--af-tag-hover-background)}.af-tag__text{display:inline-block;max-width:100%;min-width:0;white-space:normal}.af-tag__text--break{overflow-wrap:anywhere;word-break:break-word}.af-tag__remove-icon{flex:0 0 auto;height:1em;min-height:0 !important;width:1em}:host{--yrkesvaljaren-width:var(--af-width, 100%);--yrkesvaljaren-min-width:var(--af-min-width, 200px);--yrkesvaljaren-max-width:var(--af-max-width, none);--yrkesvaljaren-height:var(--af-height, auto);--yrkesvaljaren-min-height:var(--af-min-height, auto);--yrkesvaljaren-max-height:var(--af-max-height, none);--selector-width:var(--yrkesvaljaren-width);--selector-min-width:var(--yrkesvaljaren-min-width);--selector-max-width:var(--yrkesvaljaren-max-width);--selector-height:var(--yrkesvaljaren-height);--selector-min-height:var(--yrkesvaljaren-min-height);--selector-max-height:var(--yrkesvaljaren-max-height);--selector-font-family:var(--global-font-family, 'Open Sans', sans-serif);--selector-font-size:var(--sok-font-size, 1rem);--selector-text-color:var(--sok-text-color, #212529);--selector-placeholder-color:var(--sok-placeholder-color, #6c757d);--selector-border-color:var(--sok-border-color, #d1d5db);--selector-focus-border-color:var(--sok-focus-border-color, #86b7fe);--selector-focus-shadow:var(--sok-focus-shadow, 0 0 0 0.25rem rgba(13, 110, 253, 0.25));--selector-background-color:var(--sok-background-color, #fff);--selector-icon-color:var(--sok-icon-color, #6c757d);--selector-clear-button-color:var(--sok-clear-button-color, #6c757d);--selector-clear-button-hover-color:var(--sok-clear-button-hover-color, #212529);--selector-suggestion-background-color:var(--sok-suggestion-background-color, #fff);--selector-suggestion-border-color:var(--sok-suggestion-border-color, #ced4da);--selector-suggestion-hover-background:var(--sok-suggestion-hover-background, #e9ecef);--selector-suggestion-active-background:var(--sok-suggestion-active-background, #dde4eb);--selector-suggestion-text-color:var(--sok-suggestion-text-color, #212529);--selector-suggestion-highlight-weight:var(--sok-suggestion-highlight-weight, bold);--selector-error-color:var(--sok-error-color, #dc3545);--selector-loading-color:var(--sok-loading-color, #6c757d);display:flex;flex-direction:column;width:var(--yrkesvaljaren-width);min-width:var(--yrkesvaljaren-min-width);max-width:var(--yrkesvaljaren-max-width);height:var(--yrkesvaljaren-height);min-height:var(--yrkesvaljaren-min-height);max-height:var(--yrkesvaljaren-max-height);position:relative}:host([af-size="small"]){--cs-size:small;--selector-container-min-height:32px;--selector-padding-y:2px;--selector-icon-size:1em;--selector-gap:4px}:host([af-size="medium"]){--selector-container-min-height:40px;--selector-padding-y:6px}:host([af-size="large"]){--selector-container-min-height:48px;--selector-padding-y:10px;--selector-icon-size:1.25em;--selector-gap:8px}*{box-sizing:border-box}.yrkesvaljaren-wrapper{display:flex;flex-direction:column;flex:1;min-height:inherit}core-selector{margin-top:0.5rem}.status-message{margin-top:0.5rem;font-size:0.9em;font-style:italic;color:var(--sok-loading-color, #6c757d)}.history-header{font-size:0.875rem;color:var(--digi--grayscale-700, #757575);border-bottom:1px solid var(--selector-suggestion-border-color, #ced4da);margin-bottom:0px}.history-header a{color:inherit;text-decoration:underline}.history-header a:hover{color:var(--digi--global--color--cta--link--base, #1616B2)}`;
871
+
872
+ {
873
+ log.setLevel(log.levels.SILENT);
874
+ }
875
+ const YV_LATEST_DATA_URL = 'https://d3hng02m5bee8m.cloudfront.net/yrkesvaljaren/latest.json';
876
+ const CONCEPT_ID_PATTERN = /^[123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ]{4}_[123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ]{3}_[123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ]{3}$/;
877
+ const JobSelector = class {
878
+ get hostElement() { return getElement(this); }
879
+ internals;
880
+ /**
881
+ * HTML-formulärets fältnamn. Följer standardattributet `name`, på samma sätt som
882
+ * `<input name="...">`, och används som nyckel när värdet läses via `FormData`.
883
+ */
884
+ name = 'yrkesvaljaren';
885
+ /**
886
+ * Fullständig, explicit URL till en egenhostad datafil.
887
+ * Utelämna attributet för att använda senaste data automatiskt.
888
+ */
889
+ taxonomyUrl;
890
+ placeholder = PLACEHOLDER_TEXT;
891
+ label = 'Välj yrke';
892
+ /** Om satt till true kan väljaren inte användas med mus eller tangentbord. */
893
+ disabled = false;
894
+ enableMultiple = false;
895
+ showSearchIcon = true;
896
+ errorMessage;
897
+ labelDescription;
898
+ /** Om satt till `true` måste fältet ha ett värde för att vara giltigt. */
899
+ required = false;
900
+ /** Om satt till `true` tillåts fritext som inte finns i taxonomin. */
901
+ manualInput = false;
902
+ /**
903
+ * Aktiverar historiken.
904
+ * Använder Local Storage för att spara valda yrken i webbläsaren.
905
+ * Integritet: Data lagras bara lokalt i webbläsaren.
906
+ * Räckvidd: Historiken delas mellan sidor på samma domän.
907
+ */
908
+ history = true;
909
+ /**
910
+ * Om satt till true visas populära yrken initialt när historiken är tom.
911
+ */
912
+ popular = true;
913
+ // Interna konstanter och inställningar som inte exponeras för klienten
914
+ maxHistoryItems = 10;
915
+ enableSelectionAnimation = false;
916
+ afFocused = false;
917
+ populateInputOnSelect = false;
918
+ maxVisibleRows;
919
+ minLength = MIN_INPUT_LENGTH;
920
+ maxResults = 10;
921
+ get requireSelection() {
922
+ return !this.manualInput;
923
+ }
924
+ /**
925
+ * Storleken på inputfältet.
926
+ * 'small' | 'medium' | 'large'
927
+ */
928
+ size = 'medium';
929
+ constructor(hostRef) {
930
+ registerInstance(this, hostRef);
931
+ this.afJobSelectedEmitter = createEvent(this, "afJobSelected");
932
+ this.dataLoadFailedEmitter = createEvent(this, "afJobDataLoadFailed");
933
+ this.validationChangeEmitter = createEvent(this, "afJobValidationChange");
934
+ if (hostRef.$hostElement$["s-ei"]) {
935
+ this.internals = hostRef.$hostElement$["s-ei"];
936
+ }
937
+ else {
938
+ this.internals = hostRef.$hostElement$.attachInternals();
939
+ hostRef.$hostElement$["s-ei"] = this.internals;
940
+ }
941
+ log.debug('[JobSelector] Constructor - enableMultiple:', this.enableMultiple);
942
+ }
943
+ state = { status: 'loading' };
944
+ /**
945
+ * Spårar vilken typ av fel som är aktivt,
946
+ * för att kunna visa rätt meddelande.
947
+ */
948
+ activeErrorType = null;
949
+ animatingItemId = null;
950
+ animationTimer;
951
+ cloneSelection(selection) {
952
+ return selection.map((item) => item.id === null
953
+ ? { ...item }
954
+ : {
955
+ ...item,
956
+ related: item.related ? { ...item.related } : undefined,
957
+ });
958
+ }
959
+ rebuildValidJobIds() {
960
+ const taxonomyData = this.controller?.getTaxonomyData?.() ?? this.taxonomyData ?? [];
961
+ this.validJobIds = new Set(taxonomyData.map((item) => item.id).filter(Boolean));
962
+ }
963
+ validateSelectionCandidate(candidate) {
964
+ const values = typeof candidate === 'string'
965
+ ? [candidate]
966
+ : Array.isArray(candidate)
967
+ ? candidate
968
+ : null;
969
+ if (!values) {
970
+ return { ok: false, reason: 'invalid-input' };
971
+ }
972
+ const hasStrings = values.some((item) => typeof item === 'string');
973
+ const hasObjects = values.some((item) => typeof item === 'object' && item !== null);
974
+ if ((hasStrings && hasObjects) || values.some((item) => typeof item !== 'string' && (typeof item !== 'object' || item === null))) {
975
+ return { ok: false, reason: 'invalid-input' };
976
+ }
977
+ const selection = [];
978
+ const taxonomyIds = [];
979
+ const manualLabels = [];
980
+ for (const item of values) {
981
+ if (typeof item === 'string') {
982
+ const value = item.trim();
983
+ if (!value) {
984
+ return { ok: false, reason: 'invalid-input' };
985
+ }
986
+ if (CONCEPT_ID_PATTERN.test(value)) {
987
+ taxonomyIds.push(value);
988
+ selection.push({ id: value });
989
+ }
990
+ else {
991
+ if (!this.manualInput) {
992
+ return { ok: false, reason: 'manual-input-disabled' };
993
+ }
994
+ manualLabels.push(value);
995
+ selection.push({ type: 'manual-input', id: null, preferred_label: value });
996
+ }
997
+ continue;
998
+ }
999
+ if (!item || typeof item !== 'object') {
1000
+ return { ok: false, reason: 'invalid-input' };
1001
+ }
1002
+ const candidateItem = item;
1003
+ if (typeof candidateItem.id === 'string') {
1004
+ const id = candidateItem.id.trim();
1005
+ if (!id
1006
+ || (candidateItem.type !== 'job-title'
1007
+ && candidateItem.type !== 'occupation-name')
1008
+ || typeof candidateItem.preferred_label !== 'string'
1009
+ || !candidateItem.preferred_label.trim()) {
1010
+ return { ok: false, reason: 'invalid-input' };
1011
+ }
1012
+ if (candidateItem.related !== undefined) {
1013
+ const related = candidateItem.related;
1014
+ if (!related
1015
+ || related.type !== 'occupation-name'
1016
+ || typeof related.id !== 'string'
1017
+ || !related.id.trim()
1018
+ || typeof related.preferred_label !== 'string'
1019
+ || !related.preferred_label.trim()) {
1020
+ return { ok: false, reason: 'invalid-input' };
1021
+ }
1022
+ }
1023
+ taxonomyIds.push(id);
1024
+ selection.push({ id });
1025
+ }
1026
+ else if (candidateItem.id === null && candidateItem.type === 'manual-input') {
1027
+ if (typeof candidateItem.preferred_label !== 'string' || !candidateItem.preferred_label.trim()) {
1028
+ return { ok: false, reason: 'invalid-input' };
1029
+ }
1030
+ if (!this.manualInput) {
1031
+ return { ok: false, reason: 'manual-input-disabled' };
1032
+ }
1033
+ const label = candidateItem.preferred_label.trim();
1034
+ manualLabels.push(label);
1035
+ selection.push({ type: 'manual-input', id: null, preferred_label: label });
1036
+ }
1037
+ else {
1038
+ return { ok: false, reason: 'invalid-input' };
1039
+ }
1040
+ }
1041
+ if (!this.enableMultiple && selection.length > 1) {
1042
+ return { ok: false, reason: 'multiple-disabled' };
1043
+ }
1044
+ if (new Set(taxonomyIds).size !== taxonomyIds.length) {
1045
+ return { ok: false, reason: 'duplicate-taxonomy-id' };
1046
+ }
1047
+ if (new Set(manualLabels).size !== manualLabels.length) {
1048
+ return { ok: false, reason: 'duplicate-manual-label' };
1049
+ }
1050
+ const invalidIds = taxonomyIds.filter((id) => !this.validJobIds.has(id));
1051
+ if (invalidIds.length > 0) {
1052
+ return { ok: false, reason: 'unknown-id', invalidIds };
1053
+ }
1054
+ return { ok: true, selection };
1055
+ }
1056
+ describeSelectionRejection(rejection) {
1057
+ switch (rejection.reason) {
1058
+ case 'invalid-input':
1059
+ return 'setSelection accepterar en sträng, en homogen strängarray eller en array med fullständiga JobSelectionItem-objekt.';
1060
+ case 'data-unavailable':
1061
+ return 'setSelection kan inte användas eftersom yrkesdata inte kunde laddas.';
1062
+ case 'duplicate-taxonomy-id':
1063
+ return 'selection får inte innehålla dubbla taxonomi-id:n.';
1064
+ case 'duplicate-manual-label':
1065
+ return 'selection får inte innehålla dubbla manuella labels.';
1066
+ case 'multiple-disabled':
1067
+ return 'flera värden kräver af-enable-multiple="true".';
1068
+ case 'manual-input-disabled':
1069
+ return 'manual-val kräver af-manual-input="true".';
1070
+ case 'unknown-id':
1071
+ return `okända Concept ID:n: ${rejection.invalidIds?.join(', ')}`;
1072
+ }
1073
+ }
1074
+ errorCodeForRejection(reason) {
1075
+ switch (reason) {
1076
+ case 'unknown-id':
1077
+ return 'unknown-id';
1078
+ case 'manual-input-disabled':
1079
+ return 'manual-input-disabled';
1080
+ case 'multiple-disabled':
1081
+ return 'multiple-disabled';
1082
+ default:
1083
+ return 'invalid-input';
1084
+ }
1085
+ }
1086
+ rejectSelection(candidate, rejection) {
1087
+ const message = this.describeSelectionRejection(rejection);
1088
+ console.error(`[Yrkesväljaren] Ogiltig selection: ${message} Värdet accepterades inte; senaste giltiga val behålls.`, { selection: candidate, previousSelection: this.lastAcceptedSelection });
1089
+ this.activeErrorType = 'invalidInput';
1090
+ this.validationChangeEmitter.emit({ isValid: false, errorType: 'invalidInput' });
1091
+ this.updateValidity(false);
1092
+ return new InvalidSelectionError(message, this.errorCodeForRejection(rejection.reason), rejection.invalidIds);
1093
+ }
1094
+ syncAcceptedSelection(selection) {
1095
+ const nextSelection = this.cloneSelection(selection);
1096
+ this.lastAcceptedSelection = nextSelection;
1097
+ if (this.activeErrorType === 'invalidInput') {
1098
+ const hasAcceptedSelection = nextSelection.length > 0;
1099
+ const isValid = hasAcceptedSelection || !this.required;
1100
+ const errorType = isValid ? null : 'required';
1101
+ this.activeErrorType = errorType;
1102
+ this.validationChangeEmitter.emit({ isValid, errorType });
1103
+ this.updateValidity(isValid);
1104
+ }
1105
+ }
1106
+ applySelection(candidate) {
1107
+ if (!this.controller) {
1108
+ throw this.rejectSelection(candidate, { ok: false, reason: 'data-unavailable' });
1109
+ }
1110
+ const validation = this.validateSelectionCandidate(candidate);
1111
+ if (!validation.ok) {
1112
+ throw this.rejectSelection(candidate, validation);
1113
+ }
1114
+ const result = this.controller.setSelection(validation.selection);
1115
+ if (!result.ok) {
1116
+ throw this.rejectSelection(candidate, { ok: false, reason: 'unknown-id', invalidIds: result.invalidIds });
1117
+ }
1118
+ this.syncAcceptedSelection(result.selection);
1119
+ return this.cloneSelection(result.selection);
1120
+ }
1121
+ watchRequired(newValue) {
1122
+ this.controller.updateConfig({ required: newValue });
1123
+ }
1124
+ watchManualInput(newValue) {
1125
+ this.controller.updateConfig({ requireSelection: !newValue });
1126
+ }
1127
+ watchHistory(newValue) {
1128
+ this.controller.updateConfig({ history: newValue });
1129
+ }
1130
+ watchPopular(newValue) {
1131
+ this.controller.updateConfig({ popular: newValue });
1132
+ }
1133
+ watchEnableMultiple(newValue) {
1134
+ this.controller.updateConfig({ enableMultiple: newValue });
1135
+ }
1136
+ watchDisabled(newValue) {
1137
+ if (newValue)
1138
+ this.afFocused = false;
1139
+ }
1140
+ watchTaxonomyData(newValue) {
1141
+ if (this.controller && newValue && newValue.length > 0) {
1142
+ this.controller.initialize(newValue).then(() => {
1143
+ this.rebuildValidJobIds();
1144
+ this.restoreAcceptedSelectionAfterDataUpdate();
1145
+ });
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Yrkesdata (taxonomi). Kan injiceras av integratören för deterministisk laddning (t.ex. i testmiljöer).
1150
+ * Om den lämnas tom populeras den internt via det globala registret eller auto-fetch för att exponera aktuell data.
1151
+ */
1152
+ taxonomyData = [];
1153
+ afJobSelectedEmitter;
1154
+ syncFormValue() {
1155
+ if (!this.state)
1156
+ return;
1157
+ if (this.enableMultiple) {
1158
+ const selected = (this.state.status === 'multiSelect' || this.state.status === 'multiSelectShowingHistory')
1159
+ ? this.state.selectedItems
1160
+ : [];
1161
+ const formData = new FormData();
1162
+ selected.forEach(item => {
1163
+ formData.append(this.name, item.id || item.rawLabel);
1164
+ });
1165
+ this.internals.setFormValue(formData);
1166
+ }
1167
+ else {
1168
+ const selected = this.state.status === 'itemSelected' ? this.state.selectedItem : null;
1169
+ this.internals.setFormValue(selected ? selected.id || selected.rawLabel : null);
1170
+ }
1171
+ }
1172
+ handleFormValidationSync(event) {
1173
+ this.updateValidity(event.detail.isValid);
1174
+ }
1175
+ dataLoadFailedEmitter;
1176
+ validationChangeEmitter;
1177
+ static nextId = 0;
1178
+ componentId = `job-selector-${JobSelector.nextId++}`;
1179
+ observer;
1180
+ controller;
1181
+ validJobIds = new Set();
1182
+ lastAcceptedSelection = [];
1183
+ restoreAcceptedSelectionAfterDataUpdate() {
1184
+ if (this.lastAcceptedSelection.length === 0) {
1185
+ return;
1186
+ }
1187
+ const result = this.controller.setSelection(this.lastAcceptedSelection);
1188
+ if (result.ok) {
1189
+ this.syncAcceptedSelection(result.selection);
1190
+ return;
1191
+ }
1192
+ this.rejectSelection(this.lastAcceptedSelection, {
1193
+ ok: false,
1194
+ reason: 'unknown-id',
1195
+ invalidIds: result.invalidIds,
1196
+ });
1197
+ }
1198
+ // --- LIVSCYKEL-METODER ---
1199
+ async componentWillLoad() {
1200
+ log.setLevel(log.levels.WARN);
1201
+ // Använd registrerad data om den finns. Annars används en explicit URL
1202
+ // eller senaste publicerade data.
1203
+ const registeredData = getRegisteredTaxonomy();
1204
+ const hasData = (this.taxonomyData && this.taxonomyData.length > 0) || (registeredData && registeredData.length > 0);
1205
+ if (!hasData) {
1206
+ this.state = { status: 'loading' };
1207
+ try {
1208
+ const dataUrl = this.taxonomyUrl ?? YV_LATEST_DATA_URL;
1209
+ log.debug(`[JobSelector] Hämtar yrkesdata från: ${dataUrl}`);
1210
+ const response = await fetch(dataUrl);
1211
+ if (!response.ok) {
1212
+ throw new Error(`Kunde inte ladda yrkesdata från ${dataUrl}: ${response.status} ${response.statusText}`);
1213
+ }
1214
+ const data = await response.json();
1215
+ if (!isTaxonomyRegistration(data)) {
1216
+ throw new Error('Yrkesdatan har ett ogiltigt format.');
1217
+ }
1218
+ registerTaxonomy(data);
1219
+ log.debug('[JobSelector] Data-laddning lyckades.');
1220
+ }
1221
+ catch (error) {
1222
+ log.error('[JobSelector] Fel vid data-laddning:', error);
1223
+ this.state = { status: 'error', message: 'Kunde inte ladda yrkesdata.' };
1224
+ this.dataLoadFailedEmitter.emit({ error: error instanceof Error ? error : new Error(String(error)) });
1225
+ return;
1226
+ }
1227
+ }
1228
+ this.controller = new JobSelectorController({
1229
+ onStateChange: (state) => {
1230
+ this.state = { ...state };
1231
+ this.syncFormValue();
1232
+ },
1233
+ onEmitSelection: (selection) => {
1234
+ const nextSelection = selection
1235
+ ? (Array.isArray(selection) ? selection : [selection])
1236
+ : [];
1237
+ this.syncAcceptedSelection(nextSelection);
1238
+ this.afJobSelectedEmitter.emit(nextSelection);
1239
+ },
1240
+ onEmitValidation: (payload) => {
1241
+ this.activeErrorType = payload.errorType;
1242
+ this.validationChangeEmitter.emit(payload);
1243
+ },
1244
+ onEmitDataLoadFailed: (error) => { this.dataLoadFailedEmitter.emit({ error }); },
1245
+ onEmitStatusChange: () => { },
1246
+ }, {
1247
+ enableMultiple: this.enableMultiple,
1248
+ required: this.required,
1249
+ requireSelection: this.requireSelection,
1250
+ history: this.history,
1251
+ popular: this.popular,
1252
+ maxHistoryItems: this.maxHistoryItems,
1253
+ minLength: this.minLength,
1254
+ maxResults: this.maxResults,
1255
+ });
1256
+ // Initialisera controller (den läser in data från registret eller använder injicerad data)
1257
+ await this.controller.initialize(this.taxonomyData);
1258
+ this.taxonomyData = this.controller.getTaxonomyData();
1259
+ this.rebuildValidJobIds();
1260
+ // Sätt initial valideringsstatus för FACE
1261
+ if (this.activeErrorType === 'invalidInput') {
1262
+ this.updateValidity(false);
1263
+ }
1264
+ else if (this.required && this.lastAcceptedSelection.length === 0) {
1265
+ this.internals.setValidity({ customError: true }, this.errorMessage || 'Välj ett yrke från listan.');
1266
+ }
1267
+ else {
1268
+ this.internals.setValidity({});
1269
+ }
1270
+ }
1271
+ componentDidLoad() {
1272
+ this.handleHashFocus();
1273
+ }
1274
+ disconnectedCallback() {
1275
+ if (this.observer) {
1276
+ this.observer.disconnect();
1277
+ }
1278
+ if (this.controller) {
1279
+ this.controller.destroy();
1280
+ }
1281
+ clearTimeout(this.animationTimer);
1282
+ this.animationTimer = undefined;
1283
+ }
1284
+ handleHashFocus() {
1285
+ const hash = window.location.hash;
1286
+ if (!hash)
1287
+ return;
1288
+ const targetId = hash.substring(1);
1289
+ const hostId = this.hostElement.getAttribute('id');
1290
+ if (hostId && targetId === hostId) {
1291
+ this.setFocus();
1292
+ this.hostElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1293
+ history.replaceState(null, '', window.location.pathname + window.location.search);
1294
+ }
1295
+ }
1296
+ // --- ANVÄNDARINTERAKTIONER OCH LOGIK (Delegerat till Controller via CoreSelector events) ---
1297
+ handleInput(event) {
1298
+ this.afFocused = true;
1299
+ this.controller.handleInput(event.detail);
1300
+ }
1301
+ handleCsSelect(event) {
1302
+ this.afFocused = true;
1303
+ const genericItem = event.detail;
1304
+ if (this.enableSelectionAnimation) {
1305
+ this.animatingItemId = genericItem.id;
1306
+ clearTimeout(this.animationTimer);
1307
+ this.animationTimer = setTimeout(() => {
1308
+ this.animatingItemId = null;
1309
+ this.animationTimer = undefined;
1310
+ }, 600);
1311
+ }
1312
+ // Hämta originalobjektet från metadata om det finns, annars bygg ett baserat på genericItem
1313
+ const originalItem = genericItem.metadata?.original;
1314
+ if (originalItem && 'displayLabel' in originalItem) {
1315
+ this.controller.selectItem(originalItem);
1316
+ }
1317
+ else {
1318
+ // Fallback för manuell inmatning eller om metadata saknas
1319
+ if (genericItem.metadata?.type === 'ManualInput') {
1320
+ this.controller.handleFreeTextSelection(genericItem.label);
1321
+ }
1322
+ else {
1323
+ // Detta borde inte hända med data från suggestions, men för säkerhets skull
1324
+ const searchResult = {
1325
+ id: genericItem.id,
1326
+ type: 'unknown',
1327
+ preferred_label: genericItem.label,
1328
+ rawLabel: genericItem.label,
1329
+ displayLabel: genericItem.label,
1330
+ highlightedLabel: genericItem.highlightedLabel || genericItem.label,
1331
+ score: 1.0
1332
+ };
1333
+ this.controller.selectItem(searchResult);
1334
+ }
1335
+ }
1336
+ }
1337
+ handleCsClear() {
1338
+ this.controller.resetSearch();
1339
+ }
1340
+ handleCsClearHistory() {
1341
+ this.controller.clearHistory();
1342
+ }
1343
+ handleCsRequestHistory() {
1344
+ this.controller.showHistory();
1345
+ }
1346
+ handleCsLoadMore() {
1347
+ this.controller.loadMore();
1348
+ }
1349
+ handleCsRemoveItem(event) {
1350
+ const genericItem = event.detail;
1351
+ const originalItem = genericItem.metadata?.original;
1352
+ if (originalItem && 'displayLabel' in originalItem) {
1353
+ this.controller.removeItem(originalItem);
1354
+ }
1355
+ }
1356
+ handleCsFocus() {
1357
+ this.afFocused = true;
1358
+ this.controller.handleFocus();
1359
+ }
1360
+ handleCsBlur() {
1361
+ this.afFocused = false;
1362
+ this.controller.handleBlur();
1363
+ this.validate(); // Ensure events are emitted on blur
1364
+ }
1365
+ handleCsEditSelection() {
1366
+ this.afFocused = true;
1367
+ this.controller.editSelection();
1368
+ }
1369
+ /**
1370
+ * Uppdaterar taxonomidatan genom att läsa om från registret.
1371
+ * Används efter att registerTaxonomy() har anropats med ny data.
1372
+ */
1373
+ async updateData() {
1374
+ log.debug('[JobSelector] updateData called');
1375
+ await this.controller.initialize();
1376
+ this.taxonomyData = this.controller.getTaxonomyData();
1377
+ this.rebuildValidJobIds();
1378
+ this.restoreAcceptedSelectionAfterDataUpdate();
1379
+ }
1380
+ /**
1381
+ * Sätter ett programatiskt val och returnerar YV:s normaliserade, rika selection.
1382
+ * Strängar som matchar Concept ID-formatet tolkas som taxonomi-id:n; övriga som fritext.
1383
+ */
1384
+ async setSelection(input) {
1385
+ return this.applySelection(input);
1386
+ }
1387
+ async setFocus() {
1388
+ const coreSelector = this.hostElement.shadowRoot?.querySelector('core-selector');
1389
+ if (coreSelector && typeof coreSelector.setFocus === 'function') {
1390
+ await coreSelector.setFocus();
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Validerar komponenten manuellt.
1395
+ * @param forceTouch - Om true, tvingas valideringsfel att visas även om användaren inte interagerat med fältet.
1396
+ */
1397
+ async validate(forceTouch = false) {
1398
+ const isValid = await this.controller.runValidation(forceTouch);
1399
+ this.updateValidity(isValid);
1400
+ return isValid;
1401
+ }
1402
+ async checkValidity() {
1403
+ return this.internals.checkValidity();
1404
+ }
1405
+ async reportValidity() {
1406
+ return this.internals.reportValidity();
1407
+ }
1408
+ async getValidity() {
1409
+ return this.internals.validity;
1410
+ }
1411
+ async getValidationMessage() {
1412
+ return this.internals.validationMessage;
1413
+ }
1414
+ updateValidity(isValid) {
1415
+ if (!isValid) {
1416
+ const message = this.activeErrorType === 'invalidInput'
1417
+ ? this.errorMessage || 'Komponenten har fått ett ogiltigt värde.'
1418
+ : this.errorMessage || 'Välj ett yrke från listan.';
1419
+ this.internals.setValidity({ customError: true }, message);
1420
+ }
1421
+ else {
1422
+ this.internals.setValidity({});
1423
+ }
1424
+ }
1425
+ // --- MAPPING HELPERS ---
1426
+ _mapSearchResultToGenericItem(item) {
1427
+ const isSelectedResult = this.enableMultiple
1428
+ ? (this.state.status === 'multiSelect' || this.state.status === 'multiSelectShowingHistory') &&
1429
+ this.state.selectedItems.some(si => {
1430
+ if (si.id && item.id)
1431
+ return si.id === item.id;
1432
+ return si.rawLabel === item.rawLabel;
1433
+ })
1434
+ : this.state.status === 'itemSelected' && this.state.selectedItem?.id === item.id;
1435
+ return {
1436
+ id: item.id,
1437
+ label: item.displayLabel, // Use the formatted label
1438
+ isSelected: this.animatingItemId === item.id || isSelectedResult,
1439
+ highlightedLabel: item.highlightedLabel,
1440
+ displaySegments: item.displaySegments, // Pass structured segments
1441
+ group: item.type === 'occupation-name' ? 'Yrkesgrupper' : 'Yrkesbenämningar',
1442
+ metadata: {
1443
+ original: item // Store the full object for retrieval
1444
+ }
1445
+ };
1446
+ }
1447
+ _mapStateToSelectorState(componentState) {
1448
+ let coreStatus = 'loading';
1449
+ let inputValue = '';
1450
+ let suggestions = [];
1451
+ let selectedItem;
1452
+ let selectedItems = [];
1453
+ let validation = 'idle';
1454
+ let message = this.errorMessage || '';
1455
+ let validationContext = undefined;
1456
+ let listHeader;
1457
+ let showHistoryHeader;
1458
+ // Map Validation
1459
+ if ('validation' in componentState && componentState.validation) {
1460
+ validation = componentState.validation;
1461
+ if (componentState.validation === 'error') {
1462
+ if (this.activeErrorType === 'required') {
1463
+ message = this.errorMessage || 'Välj ett yrke från listan.';
1464
+ }
1465
+ else if (this.activeErrorType === 'invalidInput') {
1466
+ message = this.errorMessage || 'Välj ett yrke från listan.';
1467
+ }
1468
+ else if (this.errorMessage) {
1469
+ message = this.errorMessage;
1470
+ }
1471
+ }
1472
+ else if (componentState.validation === 'success') {
1473
+ message = 'Tack!';
1474
+ }
1475
+ }
1476
+ if ('message' in componentState && componentState.message) {
1477
+ message = componentState.message;
1478
+ }
1479
+ if ('validationContext' in componentState) {
1480
+ validationContext = componentState.validationContext;
1481
+ }
1482
+ // Map Status & Data
1483
+ switch (componentState.status) {
1484
+ case 'loading':
1485
+ coreStatus = 'loading';
1486
+ break;
1487
+ case 'error':
1488
+ coreStatus = 'error';
1489
+ break;
1490
+ case 'searching':
1491
+ coreStatus = 'searching';
1492
+ inputValue = componentState.inputValue;
1493
+ suggestions = componentState.suggestions.map(s => this._mapSearchResultToGenericItem(s));
1494
+ break;
1495
+ case 'showingHistory':
1496
+ coreStatus = 'searching';
1497
+ inputValue = componentState.inputValue || '';
1498
+ suggestions = componentState.historyItems.map(s => this._mapSearchResultToGenericItem(s));
1499
+ break;
1500
+ case 'itemSelected':
1501
+ coreStatus = 'itemSelected';
1502
+ selectedItem = this._mapSearchResultToGenericItem(componentState.selectedItem);
1503
+ selectedItem.highlightedLabel = styleParentheticalText(selectedItem.label);
1504
+ inputValue = componentState.inputValue || '';
1505
+ break;
1506
+ case 'multiSelect':
1507
+ coreStatus = 'multiSelect';
1508
+ inputValue = componentState.inputValue;
1509
+ suggestions = componentState.suggestions.map(s => this._mapSearchResultToGenericItem(s));
1510
+ selectedItems = componentState.selectedItems.map(s => this._mapSearchResultToGenericItem(s));
1511
+ break;
1512
+ case 'multiSelectShowingHistory':
1513
+ coreStatus = 'multiSelect';
1514
+ inputValue = componentState.inputValue || '';
1515
+ suggestions = componentState.historyItems.map(s => this._mapSearchResultToGenericItem(s));
1516
+ selectedItems = componentState.selectedItems.map(s => this._mapSearchResultToGenericItem(s));
1517
+ break;
1518
+ }
1519
+ const isHistoryView = componentState.status === 'showingHistory' || componentState.status === 'multiSelectShowingHistory';
1520
+ if (coreStatus === 'multiSelect' || coreStatus === 'itemSelected' || coreStatus === 'searching') {
1521
+ // 1. Hide already selected items from the dropdown (YV specific requirement)
1522
+ if (selectedItems.length > 0) {
1523
+ suggestions = suggestions.filter(s => {
1524
+ return !selectedItems.some(si => (si.id && s.id) ? si.id === s.id : si.label === s.label);
1525
+ });
1526
+ }
1527
+ // 1. Set dynamic list header
1528
+ if (isHistoryView && suggestions.length > 0) {
1529
+ if (componentState.isPopularView) {
1530
+ listHeader = 'Populära sökningar';
1531
+ showHistoryHeader = false; // Hide Rensa link
1532
+ }
1533
+ else {
1534
+ // Rule: Historik should show "Din privata historik - Rensa".
1535
+ // We let CoreSelector's default HistoryHeader handle this
1536
+ showHistoryHeader = true;
1537
+ listHeader = undefined;
1538
+ }
1539
+ }
1540
+ else {
1541
+ // Reset when searching or when no suggestions
1542
+ listHeader = undefined;
1543
+ showHistoryHeader = false;
1544
+ }
1545
+ }
1546
+ else {
1547
+ // Not in a state that should show a list header
1548
+ listHeader = undefined;
1549
+ showHistoryHeader = false;
1550
+ }
1551
+ return {
1552
+ status: coreStatus,
1553
+ inputValue,
1554
+ suggestions,
1555
+ selectedItem,
1556
+ selectedItems,
1557
+ validation,
1558
+ validationContext,
1559
+ message,
1560
+ listHeader,
1561
+ showHistoryHeader,
1562
+ isHistoryView
1563
+ };
1564
+ }
1565
+ render() {
1566
+ const selectorState = this._mapStateToSelectorState(this.state);
1567
+ // Explicitly construct config to match SelectorConfig interface
1568
+ const config = {
1569
+ disabled: this.disabled,
1570
+ placeholder: this.placeholder,
1571
+ minLength: this.minLength,
1572
+ maxResults: this.maxResults,
1573
+ enableMultiple: this.enableMultiple,
1574
+ showSearchIcon: this.showSearchIcon,
1575
+ requireSelection: this.requireSelection,
1576
+ size: this.size,
1577
+ enableSelectionAnimation: this.enableSelectionAnimation,
1578
+ populateInputOnSelect: this.populateInputOnSelect,
1579
+ maxVisibleRows: this.maxVisibleRows,
1580
+ manualInputBehavior: 'select',
1581
+ showHistoryHeader: this.history && !selectorState.listHeader, // Only show default if not override
1582
+ listHeader: selectorState.listHeader,
1583
+ enableInfiniteScroll: true
1584
+ };
1585
+ // Pass external error message if present and we are not in loading/error state
1586
+ // (This logic is now moved to _mapStateToSelectorState to respect validation state)
1587
+ return (h("div", { key: '11b208182109be21dcf759010cb87f1bbd10b618', class: "job-selector-wrapper" }, this.state.status === 'error' ? (h("div", { class: "error-message" }, h(AfValidationMessage, { variant: "error", role: "alert" }, this.state.message || 'Ett fel uppstod.'))) : (h(Fragment, null, h("core-selector", { id: this.componentId, componentId: this.componentId, state: selectorState, config: config, label: this.label, labelDescription: this.labelDescription, size: this.size, afFocused: this.afFocused })))));
1588
+ }
1589
+ static get delegatesFocus() { return true; }
1590
+ static get formAssociated() { return true; }
1591
+ static get watchers() { return {
1592
+ "required": [{
1593
+ "watchRequired": 0
1594
+ }],
1595
+ "manualInput": [{
1596
+ "watchManualInput": 0
1597
+ }],
1598
+ "history": [{
1599
+ "watchHistory": 0
1600
+ }],
1601
+ "popular": [{
1602
+ "watchPopular": 0
1603
+ }],
1604
+ "enableMultiple": [{
1605
+ "watchEnableMultiple": 0
1606
+ }],
1607
+ "disabled": [{
1608
+ "watchDisabled": 0
1609
+ }],
1610
+ "taxonomyData": [{
1611
+ "watchTaxonomyData": 0
1612
+ }]
1613
+ }; }
1614
+ };
1615
+ JobSelector.style = jobSelectorCss();
1616
+
1617
+ export { JobSelector as digi_service_job_selector };