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