@siemens/element-ng 51.0.0-rc.8 → 51.0.0

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 (105) hide show
  1. package/fesm2022/siemens-element-ng-ag-grid.mjs +17 -25
  2. package/fesm2022/siemens-element-ng-ag-grid.mjs.map +1 -1
  3. package/fesm2022/siemens-element-ng-application-header.mjs +2 -2
  4. package/fesm2022/siemens-element-ng-application-header.mjs.map +1 -1
  5. package/fesm2022/siemens-element-ng-avatar.mjs +3 -3
  6. package/fesm2022/siemens-element-ng-avatar.mjs.map +1 -1
  7. package/fesm2022/siemens-element-ng-chat-messages.mjs +123 -23
  8. package/fesm2022/siemens-element-ng-chat-messages.mjs.map +1 -1
  9. package/fesm2022/siemens-element-ng-color-picker.mjs +18 -18
  10. package/fesm2022/siemens-element-ng-color-picker.mjs.map +1 -1
  11. package/fesm2022/siemens-element-ng-column-selection-dialog.mjs +2 -2
  12. package/fesm2022/siemens-element-ng-column-selection-dialog.mjs.map +1 -1
  13. package/fesm2022/siemens-element-ng-dashboard.mjs +4 -4
  14. package/fesm2022/siemens-element-ng-dashboard.mjs.map +1 -1
  15. package/fesm2022/siemens-element-ng-date-range-filter.mjs +2 -2
  16. package/fesm2022/siemens-element-ng-date-range-filter.mjs.map +1 -1
  17. package/fesm2022/siemens-element-ng-datepicker.mjs +4 -4
  18. package/fesm2022/siemens-element-ng-datepicker.mjs.map +1 -1
  19. package/fesm2022/siemens-element-ng-file-uploader.mjs +2 -2
  20. package/fesm2022/siemens-element-ng-file-uploader.mjs.map +1 -1
  21. package/fesm2022/siemens-element-ng-filtered-search.mjs +2 -2
  22. package/fesm2022/siemens-element-ng-filtered-search.mjs.map +1 -1
  23. package/fesm2022/siemens-element-ng-form.mjs +19 -6
  24. package/fesm2022/siemens-element-ng-form.mjs.map +1 -1
  25. package/fesm2022/siemens-element-ng-formly.mjs +4 -4
  26. package/fesm2022/siemens-element-ng-formly.mjs.map +1 -1
  27. package/fesm2022/siemens-element-ng-icon.mjs +1 -1
  28. package/fesm2022/siemens-element-ng-icon.mjs.map +1 -1
  29. package/fesm2022/siemens-element-ng-info-page.mjs +2 -2
  30. package/fesm2022/siemens-element-ng-info-page.mjs.map +1 -1
  31. package/fesm2022/siemens-element-ng-landing-page.mjs +4 -4
  32. package/fesm2022/siemens-element-ng-landing-page.mjs.map +1 -1
  33. package/fesm2022/siemens-element-ng-list-details.mjs +62 -16
  34. package/fesm2022/siemens-element-ng-list-details.mjs.map +1 -1
  35. package/fesm2022/siemens-element-ng-main-detail-container.mjs +72 -24
  36. package/fesm2022/siemens-element-ng-main-detail-container.mjs.map +1 -1
  37. package/fesm2022/siemens-element-ng-maplibre.mjs +282 -6
  38. package/fesm2022/siemens-element-ng-maplibre.mjs.map +1 -1
  39. package/fesm2022/siemens-element-ng-markdown-hightlighter-highlightjs.mjs +2 -2
  40. package/fesm2022/siemens-element-ng-markdown-hightlighter-highlightjs.mjs.map +1 -1
  41. package/fesm2022/siemens-element-ng-menu.mjs +6 -6
  42. package/fesm2022/siemens-element-ng-menu.mjs.map +1 -1
  43. package/fesm2022/siemens-element-ng-navbar-vertical-next.mjs +6 -6
  44. package/fesm2022/siemens-element-ng-navbar-vertical-next.mjs.map +1 -1
  45. package/fesm2022/siemens-element-ng-navbar-vertical.mjs +4 -4
  46. package/fesm2022/siemens-element-ng-navbar-vertical.mjs.map +1 -1
  47. package/fesm2022/siemens-element-ng-result-details-list.mjs +2 -2
  48. package/fesm2022/siemens-element-ng-result-details-list.mjs.map +1 -1
  49. package/fesm2022/siemens-element-ng-select.mjs +44 -5
  50. package/fesm2022/siemens-element-ng-select.mjs.map +1 -1
  51. package/fesm2022/siemens-element-ng-side-panel.mjs +48 -13
  52. package/fesm2022/siemens-element-ng-side-panel.mjs.map +1 -1
  53. package/fesm2022/siemens-element-ng-slider.mjs +2 -2
  54. package/fesm2022/siemens-element-ng-slider.mjs.map +1 -1
  55. package/fesm2022/siemens-element-ng-source-chip.mjs +2 -2
  56. package/fesm2022/siemens-element-ng-source-chip.mjs.map +1 -1
  57. package/fesm2022/siemens-element-ng-split.mjs +18 -16
  58. package/fesm2022/siemens-element-ng-split.mjs.map +1 -1
  59. package/fesm2022/siemens-element-ng-status-bar.mjs +2 -2
  60. package/fesm2022/siemens-element-ng-status-bar.mjs.map +1 -1
  61. package/fesm2022/siemens-element-ng-status-toggle.mjs +2 -2
  62. package/fesm2022/siemens-element-ng-status-toggle.mjs.map +1 -1
  63. package/fesm2022/siemens-element-ng-summary-chip.mjs +2 -2
  64. package/fesm2022/siemens-element-ng-summary-chip.mjs.map +1 -1
  65. package/fesm2022/siemens-element-ng-summary-widget.mjs +2 -2
  66. package/fesm2022/siemens-element-ng-summary-widget.mjs.map +1 -1
  67. package/fesm2022/siemens-element-ng-system-banner.mjs +5 -5
  68. package/fesm2022/siemens-element-ng-system-banner.mjs.map +1 -1
  69. package/fesm2022/siemens-element-ng-tabs.mjs +93 -71
  70. package/fesm2022/siemens-element-ng-tabs.mjs.map +1 -1
  71. package/fesm2022/siemens-element-ng-toast-notification.mjs +2 -2
  72. package/fesm2022/siemens-element-ng-toast-notification.mjs.map +1 -1
  73. package/fesm2022/siemens-element-ng-tour.mjs +2 -2
  74. package/fesm2022/siemens-element-ng-tour.mjs.map +1 -1
  75. package/fesm2022/siemens-element-ng-translate.mjs.map +1 -1
  76. package/fesm2022/siemens-element-ng-tree-view.mjs +4 -4
  77. package/fesm2022/siemens-element-ng-tree-view.mjs.map +1 -1
  78. package/package.json +4 -4
  79. package/schematics/collection.json +5 -0
  80. package/schematics/migrate-tabs-legacy/index.js +5 -0
  81. package/schematics/migrate-tabs-legacy/migrate-tabs-legacy-rule.js +675 -0
  82. package/schematics/migrate-tabs-legacy/schema.json +14 -0
  83. package/schematics/migrations/data/css-custom-properties.js +67 -1
  84. package/schematics/migrations/data/element-class-changes.js +31 -12
  85. package/schematics/ng-update/migrate-list-details-units.js +86 -0
  86. package/schematics/ng-update/migrate-main-detail-units.js +89 -0
  87. package/schematics/ng-update/migrate-split-collapse.js +1 -32
  88. package/schematics/ng-update/migrate-split-scale.js +524 -0
  89. package/schematics/ng-update/migrate-split-sizes.js +5 -3
  90. package/schematics/ng-update/migrate-to-v51.js +6 -0
  91. package/schematics/utils/index.js +1 -0
  92. package/schematics/utils/template-edit-utils.js +35 -0
  93. package/schematics/utils/template-utils.js +31 -45
  94. package/template-i18n.json +1 -0
  95. package/types/siemens-element-ng-avatar.d.ts +1 -1
  96. package/types/siemens-element-ng-chat-messages.d.ts +81 -17
  97. package/types/siemens-element-ng-form.d.ts +3 -0
  98. package/types/siemens-element-ng-list-details.d.ts +25 -3
  99. package/types/siemens-element-ng-main-detail-container.d.ts +23 -3
  100. package/types/siemens-element-ng-maplibre.d.ts +131 -7
  101. package/types/siemens-element-ng-select.d.ts +31 -4
  102. package/types/siemens-element-ng-side-panel.d.ts +17 -3
  103. package/types/siemens-element-ng-split.d.ts +3 -10
  104. package/types/siemens-element-ng-system-banner.d.ts +2 -2
  105. package/types/siemens-element-ng-translate.d.ts +1 -0
@@ -0,0 +1,675 @@
1
+ /**
2
+ * Copyright (c) Siemens 2016 - 2026
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { SchematicsException } from '@angular-devkit/schematics';
6
+ import { HtmlParser, RecursiveVisitor, visitAll } from '@angular/compiler';
7
+ import { dirname, join } from 'path/posix';
8
+ import ts from 'typescript';
9
+ import { discoverSourceFiles, getInlineTemplates, getTemplateUrl } from '../utils/index.js';
10
+ const LEGACY_TABSET = 'si-tabset-legacy';
11
+ const TABSET = 'si-tabset';
12
+ const LEGACY_TAB = 'si-tab-legacy';
13
+ const TAB = 'si-tab';
14
+ const LEGACY_IMPORT_PATTERN = /^@(siemens|simpl)\/element-ng\/tabs-legacy$/;
15
+ const CURRENT_IMPORT_PATTERN = /^@(siemens|simpl)\/element-ng\/tabs$/;
16
+ class TabsCollector extends RecursiveVisitor {
17
+ legacyTabs = [];
18
+ tabsets = [];
19
+ blocks = [];
20
+ elements = [];
21
+ tabsetStack = [];
22
+ visitBlock(block, context) {
23
+ this.blocks.push(block);
24
+ const result = super.visitBlock(block, context);
25
+ this.blocks.pop();
26
+ return result;
27
+ }
28
+ visitElement(element, context) {
29
+ const isLegacyTabset = element.name === LEGACY_TABSET;
30
+ if (isLegacyTabset) {
31
+ const tabset = {
32
+ element,
33
+ blockDepth: this.blocks.length,
34
+ elementDepth: this.elements.length,
35
+ tabs: []
36
+ };
37
+ this.tabsets.push(tabset);
38
+ this.tabsetStack.push(tabset);
39
+ }
40
+ const legacyTabAttribute = getAttribute(element, LEGACY_TAB);
41
+ const isLegacyTab = element.name === LEGACY_TAB || !!legacyTabAttribute;
42
+ if (isLegacyTab) {
43
+ const tabset = this.tabsetStack.at(-1);
44
+ const relativeElements = tabset ? this.elements.slice(tabset.elementDepth) : this.elements;
45
+ const tab = {
46
+ element,
47
+ routerBased: !!legacyTabAttribute,
48
+ relativeBlocks: tabset ? this.blocks.slice(tabset.blockDepth) : [...this.blocks],
49
+ structuralAncestor: hasStructuralTemplateAttribute(element) ||
50
+ relativeElements.some(hasStructuralTemplateAttribute)
51
+ };
52
+ this.legacyTabs.push(tab);
53
+ tabset?.tabs.push(tab);
54
+ }
55
+ this.elements.push(element);
56
+ const result = super.visitElement(element, context);
57
+ this.elements.pop();
58
+ if (isLegacyTabset) {
59
+ this.tabsetStack.pop();
60
+ }
61
+ return result;
62
+ }
63
+ }
64
+ class TemplateEditor {
65
+ edits = [];
66
+ insertions = new Map();
67
+ insert(position, text) {
68
+ const insertions = this.insertions.get(position) ?? [];
69
+ insertions.push(text);
70
+ this.insertions.set(position, insertions);
71
+ }
72
+ replace(start, end, text) {
73
+ this.edits.push({ start, end, text });
74
+ }
75
+ removeAttribute(attribute) {
76
+ this.replace(attribute.sourceSpan.start.offset, attribute.sourceSpan.end.offset, '');
77
+ }
78
+ allEdits() {
79
+ return [
80
+ ...this.edits,
81
+ ...[...this.insertions].map(([position, insertions]) => ({
82
+ start: position,
83
+ end: position,
84
+ text: insertions.join('')
85
+ }))
86
+ ];
87
+ }
88
+ }
89
+ export const migrateTabsLegacy = (options) => {
90
+ return async (tree, context) => {
91
+ const processedTemplates = new Map();
92
+ for await (const { path: filePath, sourceFile } of discoverSourceFiles(tree, context, options.path ?? '/')) {
93
+ const sourceContent = tree.readText(filePath);
94
+ const sourceEdits = [];
95
+ const uses = { tab: false, tabLink: false, tabset: false };
96
+ for (const template of getInlineTemplates(sourceFile)) {
97
+ const start = template.getStart(sourceFile) + 1;
98
+ const templateContent = sourceContent.substring(start, template.getEnd() - 1);
99
+ const result = migrateTemplate(templateContent, filePath, context);
100
+ mergeUses(uses, result.uses);
101
+ if (result.changed) {
102
+ sourceEdits.push({ start, end: template.getEnd() - 1, text: result.content });
103
+ }
104
+ }
105
+ for (const templateUrl of getTemplateUrl(sourceFile)) {
106
+ const templatePath = join(dirname(filePath), templateUrl);
107
+ const templateBuffer = tree.read(templatePath);
108
+ if (!templateBuffer) {
109
+ continue;
110
+ }
111
+ const templateContent = templateBuffer.toString('utf-8');
112
+ const result = processedTemplates.get(templatePath) ??
113
+ migrateTemplate(templateContent, templatePath, context);
114
+ mergeUses(uses, result.uses);
115
+ if (result.changed && !processedTemplates.has(templatePath)) {
116
+ tree.overwrite(templatePath, result.content);
117
+ }
118
+ processedTemplates.set(templatePath, result);
119
+ }
120
+ sourceEdits.push(...migrateImports(sourceFile, sourceContent, uses, filePath, context));
121
+ if (sourceEdits.length) {
122
+ tree.overwrite(filePath, applyTextEdits(sourceContent, sourceEdits));
123
+ }
124
+ }
125
+ return tree;
126
+ };
127
+ };
128
+ const migrateTemplate = (template, filePath, context) => {
129
+ const parsed = new HtmlParser().parse(template, filePath, {
130
+ tokenizeExpansionForms: true,
131
+ tokenizeBlocks: true,
132
+ preserveLineEndings: true
133
+ });
134
+ if (parsed.errors.length) {
135
+ throw new SchematicsException(`Could not parse ${filePath}; legacy tabs were not migrated.`);
136
+ }
137
+ const collector = new TabsCollector();
138
+ visitAll(collector, parsed.rootNodes);
139
+ const editor = new TemplateEditor();
140
+ const uses = {
141
+ tab: collector.legacyTabs.some(tab => !tab.routerBased),
142
+ tabLink: collector.legacyTabs.some(tab => tab.routerBased),
143
+ tabset: collector.tabsets.length > 0
144
+ };
145
+ for (const tabset of collector.tabsets) {
146
+ renameElement(tabset.element, LEGACY_TABSET, TABSET, editor);
147
+ migrateTabsetSelection(template, tabset, editor, filePath, context);
148
+ }
149
+ for (const tab of collector.legacyTabs) {
150
+ if (tab.routerBased) {
151
+ const selector = getAttribute(tab.element, LEGACY_TAB);
152
+ editor.replace(selector.sourceSpan.start.offset, selector.sourceSpan.start.offset + LEGACY_TAB.length, TAB);
153
+ }
154
+ else {
155
+ renameElement(tab.element, LEGACY_TAB, TAB, editor);
156
+ }
157
+ migrateHeading(tab.element, editor, filePath, context);
158
+ }
159
+ const edits = editor.allEdits();
160
+ return {
161
+ content: edits.length ? applyTextEdits(template, edits) : template,
162
+ changed: edits.length > 0,
163
+ uses
164
+ };
165
+ };
166
+ const migrateTabsetSelection = (template, tabset, editor, filePath, context) => {
167
+ const element = tabset.element;
168
+ const selectionAttributes = element.attrs.filter(attribute => ['selectedTabIndex', '[selectedTabIndex]', '[(selectedTabIndex)]'].includes(attribute.name));
169
+ const selection = selectionAttributes.find(attribute => attribute.name === '[(selectedTabIndex)]') ??
170
+ selectionAttributes.find(attribute => attribute.name === '[selectedTabIndex]') ??
171
+ selectionAttributes.find(attribute => attribute.name === 'selectedTabIndex');
172
+ const selectedTabIndexChange = getAttribute(element, '(selectedTabIndexChange)');
173
+ const deselect = getAttribute(element, '(deselect)');
174
+ const closeHandlers = tabset.tabs
175
+ .map(tab => getAttribute(tab.element, '(closeTriggered)'))
176
+ .filter((attribute) => !!attribute);
177
+ const selectDefault = getAttribute(element, '[selectDefaultTab]') ?? getAttribute(element, 'selectDefaultTab');
178
+ if (deselect) {
179
+ editor.removeAttribute(deselect);
180
+ warnAt(context, filePath, deselect, '`deselect` cannot be migrated automatically. Replace its cancellation logic with `canDeactivate` on the affected tab.');
181
+ }
182
+ for (const closeHandler of closeHandlers) {
183
+ if (/\$event\b/.test(getAttributeValue(template, closeHandler))) {
184
+ warnAt(context, filePath, closeHandler, '`closeTriggered` no longer emits the tab component. Update handlers that consume `$event`.');
185
+ }
186
+ }
187
+ if (selectionAttributes.length > 1) {
188
+ warnAt(context, filePath, element, 'Multiple `selectedTabIndex` bindings were found; the highest-precedence binding was migrated.');
189
+ }
190
+ const positions = getTabPositions(tabset);
191
+ const hasRouterTabs = tabset.tabs.some(tab => tab.routerBased);
192
+ const needsPositionMapping = !!selection || !!selectedTabIndexChange;
193
+ if (hasRouterTabs && needsPositionMapping) {
194
+ warnAt(context, filePath, element, '`selectedTabIndex` cannot control router tabs. Their active state is derived from the Angular router.');
195
+ }
196
+ if (needsPositionMapping && (!positions || hasRouterTabs)) {
197
+ warnAt(context, filePath, element, '`selectedTabIndex` could not be distributed because runtime tab positions are ambiguous.');
198
+ return;
199
+ }
200
+ const selectionExpression = selection ? getAttributeValue(template, selection) : undefined;
201
+ const numericSelection = selectionExpression ? parseInteger(selectionExpression) : undefined;
202
+ const canMigrateSelection = selection?.name !== 'selectedTabIndex' || numericSelection !== undefined;
203
+ if (canMigrateSelection) {
204
+ selectionAttributes.forEach(attribute => editor.removeAttribute(attribute));
205
+ if (selectedTabIndexChange) {
206
+ editor.removeAttribute(selectedTabIndexChange);
207
+ }
208
+ }
209
+ if (selectDefault) {
210
+ editor.removeAttribute(selectDefault);
211
+ }
212
+ const isTwoWay = selection?.name === '[(selectedTabIndex)]';
213
+ const outputExpression = selectedTabIndexChange
214
+ ? getAttributeValue(template, selectedTabIndexChange)
215
+ : undefined;
216
+ if (positions) {
217
+ if (selection?.name === 'selectedTabIndex' && numericSelection === undefined) {
218
+ warnAt(context, filePath, selection, 'A non-numeric unbound `selectedTabIndex` value cannot be migrated automatically.');
219
+ }
220
+ else if (selectionExpression && numericSelection === undefined) {
221
+ for (const position of positions) {
222
+ addActiveAttribute(position.tab.element, `${parenthesizeExpression(selectionExpression)} === ${position.index}`, editor, filePath, context);
223
+ }
224
+ }
225
+ else if (numericSelection !== undefined && numericSelection >= 0) {
226
+ const selectedPosition = positions.find(position => position.index === `${numericSelection}`);
227
+ if (selectedPosition) {
228
+ addActiveAttribute(selectedPosition.tab.element, 'true', editor, filePath, context);
229
+ }
230
+ else if (positions.some(position => position.index === '$index')) {
231
+ addActiveAttribute(positions[0].tab.element, `${numericSelection} === $index`, editor, filePath, context);
232
+ }
233
+ else {
234
+ warnAt(context, filePath, selection, `The selected tab index ${numericSelection} is outside the statically known tab range.`);
235
+ }
236
+ }
237
+ else if (!selection) {
238
+ migrateDefaultSelection(template, selectDefault, positions, editor, filePath, context);
239
+ }
240
+ if (canMigrateSelection && (isTwoWay || outputExpression !== undefined)) {
241
+ const assignableSelection = !isTwoWay || !selectionExpression || isAssignableExpression(selectionExpression);
242
+ if (!assignableSelection) {
243
+ warnAt(context, filePath, selection, 'The two-way `selectedTabIndex` expression is not assignable; its update could not be migrated.');
244
+ }
245
+ for (const position of positions) {
246
+ const statements = [];
247
+ if (isTwoWay && selectionExpression && assignableSelection) {
248
+ statements.push(`$event && (${selectionExpression} = ${position.index})`);
249
+ }
250
+ if (outputExpression !== undefined) {
251
+ const migratedOutput = outputExpression.replace(/\$event\b/g, position.index);
252
+ statements.push(guardEventExpression(migratedOutput));
253
+ }
254
+ if (statements.length) {
255
+ appendEventAttribute(position.tab.element, 'activeChange', statements.join('; '), editor);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ };
261
+ const migrateDefaultSelection = (template, selectDefault, positions, editor, filePath, context) => {
262
+ if (!positions.length) {
263
+ return;
264
+ }
265
+ const existingActive = positions
266
+ .map(position => findInput(position.tab.element, 'active'))
267
+ .find((attribute) => !!attribute);
268
+ if (existingActive) {
269
+ warnAt(context, filePath, existingActive, 'The existing `active` binding was preserved; review it against the migrated default selection.');
270
+ return;
271
+ }
272
+ if (!selectDefault) {
273
+ const first = positions[0];
274
+ addActiveAttribute(first.tab.element, first.index === '$index' ? '$first' : 'true', editor, filePath, context);
275
+ return;
276
+ }
277
+ const value = getAttributeValue(template, selectDefault).trim();
278
+ if (selectDefault.name === 'selectDefaultTab') {
279
+ if (!value || value === 'true') {
280
+ const first = positions[0];
281
+ addActiveAttribute(first.tab.element, first.index === '$index' ? '$first' : 'true', editor, filePath, context);
282
+ }
283
+ else if (value !== 'false') {
284
+ warnAt(context, filePath, selectDefault, 'The unbound `selectDefaultTab` value is not a recognized boolean.');
285
+ }
286
+ return;
287
+ }
288
+ if (value === 'false') {
289
+ return;
290
+ }
291
+ const first = positions[0];
292
+ if (value === 'true') {
293
+ addActiveAttribute(first.tab.element, first.index === '$index' ? '$first' : 'true', editor, filePath, context);
294
+ }
295
+ else {
296
+ warnAt(context, filePath, selectDefault, 'A bound `selectDefaultTab` expression cannot be migrated automatically.');
297
+ }
298
+ };
299
+ const addActiveAttribute = (element, value, editor, filePath, context) => {
300
+ const existing = findInput(element, 'active');
301
+ if (existing) {
302
+ warnAt(context, filePath, existing, 'The existing `active` binding was preserved; review it against the migrated tab index state.');
303
+ return;
304
+ }
305
+ setAttribute(element, 'active', value, editor);
306
+ };
307
+ const getTabPositions = (tabset) => {
308
+ const componentTabs = tabset.tabs.filter(tab => !tab.routerBased);
309
+ if (!componentTabs.length) {
310
+ return [];
311
+ }
312
+ if (componentTabs.length === 1 &&
313
+ componentTabs.length === tabset.tabs.length &&
314
+ componentTabs[0].relativeBlocks.length === 1 &&
315
+ componentTabs[0].relativeBlocks[0].name === 'for' &&
316
+ !componentTabs[0].structuralAncestor) {
317
+ return [{ tab: componentTabs[0], index: '$index' }];
318
+ }
319
+ if (componentTabs.every(tab => !tab.relativeBlocks.length && !tab.structuralAncestor) &&
320
+ componentTabs.length === tabset.tabs.length) {
321
+ return componentTabs.map((tab, index) => ({ tab, index: `${index}` }));
322
+ }
323
+ return undefined;
324
+ };
325
+ const migrateHeading = (element, editor, filePath, context) => {
326
+ const heading = findInput(element, 'heading');
327
+ const iconAltText = findInput(element, 'iconAltText');
328
+ if (iconAltText && !heading) {
329
+ const value = iconAltText.valueSpan?.toString() ?? '';
330
+ const replacement = iconAltText.name === 'iconAltText' ? 'heading' : '[heading]';
331
+ editor.replace(iconAltText.sourceSpan.start.offset, iconAltText.sourceSpan.end.offset, value ? `${replacement}="${escapeAttributeValue(value)}"` : replacement);
332
+ }
333
+ else if (iconAltText) {
334
+ editor.removeAttribute(iconAltText);
335
+ }
336
+ else if (!heading) {
337
+ warnAt(context, filePath, element, 'The migrated tab requires a `heading` input, but neither `heading` nor `iconAltText` was found.');
338
+ }
339
+ };
340
+ const setAttribute = (element, name, value, editor) => {
341
+ const existing = findInput(element, name);
342
+ const escapedValue = escapeAttributeValue(value);
343
+ if (!existing) {
344
+ editor.insert(element.startSourceSpan.start.offset + 1 + element.name.length, ` [${name}]="${escapedValue}"`);
345
+ return;
346
+ }
347
+ editor.replace(existing.sourceSpan.start.offset, existing.sourceSpan.end.offset, `[${name}]="${escapedValue}"`);
348
+ };
349
+ const appendEventAttribute = (element, name, value, editor) => {
350
+ const existing = getAttribute(element, `(${name})`);
351
+ const combinedValue = existing?.valueSpan ? `${existing.valueSpan.toString()}; ${value}` : value;
352
+ const escapedValue = escapeAttributeValue(combinedValue);
353
+ if (existing) {
354
+ editor.replace(existing.sourceSpan.start.offset, existing.sourceSpan.end.offset, `(${name})="${escapedValue}"`);
355
+ }
356
+ else {
357
+ editor.insert(element.startSourceSpan.start.offset + 1 + element.name.length, ` (${name})="${escapedValue}"`);
358
+ }
359
+ };
360
+ const renameElement = (element, from, to, editor) => {
361
+ editor.replace(element.startSourceSpan.start.offset + 1, element.startSourceSpan.start.offset + 1 + from.length, to);
362
+ if (element.endSourceSpan && element.startSourceSpan.start !== element.endSourceSpan.start) {
363
+ editor.replace(element.endSourceSpan.start.offset + 2, element.endSourceSpan.start.offset + 2 + from.length, to);
364
+ }
365
+ };
366
+ const migrateImports = (sourceFile, sourceContent, uses, filePath, context) => {
367
+ const relevantImports = [];
368
+ const currentImports = new Map();
369
+ const legacyImports = new Map();
370
+ const identifierReplacements = new Map();
371
+ const splitIdentifierReplacements = new Map();
372
+ const legacyModuleLocals = new Set();
373
+ let firstLegacyCurrentModule;
374
+ let hasLegacyImport = false;
375
+ for (const statement of sourceFile.statements) {
376
+ if (!ts.isImportDeclaration(statement) ||
377
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
378
+ !CURRENT_IMPORT_PATTERN.test(statement.moduleSpecifier.text) ||
379
+ !statement.importClause?.namedBindings ||
380
+ !ts.isNamedImports(statement.importClause.namedBindings)) {
381
+ continue;
382
+ }
383
+ relevantImports.push(statement);
384
+ appendImportSpecifiers(currentImports, statement.moduleSpecifier.text, statement.importClause.namedBindings.elements.map(toImportSpecifierData));
385
+ }
386
+ for (const statement of sourceFile.statements) {
387
+ if (!ts.isImportDeclaration(statement) ||
388
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
389
+ !statement.importClause?.namedBindings ||
390
+ !ts.isNamedImports(statement.importClause.namedBindings)) {
391
+ continue;
392
+ }
393
+ const moduleName = statement.moduleSpecifier.text;
394
+ if (!LEGACY_IMPORT_PATTERN.test(moduleName)) {
395
+ continue;
396
+ }
397
+ relevantImports.push(statement);
398
+ const specifiers = statement.importClause.namedBindings.elements.map(toImportSpecifierData);
399
+ hasLegacyImport = true;
400
+ const currentModule = moduleName.replace(/tabs-legacy$/, 'tabs');
401
+ firstLegacyCurrentModule ??= currentModule;
402
+ for (const specifier of specifiers) {
403
+ if (specifier.imported === 'SiTabLegacyComponent') {
404
+ const targets = uses.tabLink
405
+ ? uses.tab
406
+ ? ['SiTabComponent', 'SiTabLinkComponent']
407
+ : ['SiTabLinkComponent']
408
+ : ['SiTabComponent'];
409
+ if (targets.length === 1) {
410
+ const preferredLocal = specifier.local === specifier.imported ? targets[0] : specifier.local;
411
+ const local = ensureImport(currentImports, currentModule, targets[0], preferredLocal, specifier.typeOnly);
412
+ if (specifier.local !== local) {
413
+ identifierReplacements.set(specifier.local, local);
414
+ }
415
+ }
416
+ else {
417
+ const locals = targets.map(target => ensureImport(currentImports, currentModule, target, target, specifier.typeOnly));
418
+ splitIdentifierReplacements.set(specifier.local, locals);
419
+ }
420
+ }
421
+ else if (specifier.imported === 'SiTabsetLegacyComponent') {
422
+ const preferredLocal = specifier.local === specifier.imported ? 'SiTabsetComponent' : specifier.local;
423
+ const local = ensureImport(currentImports, currentModule, 'SiTabsetComponent', preferredLocal, specifier.typeOnly);
424
+ if (specifier.local !== local) {
425
+ identifierReplacements.set(specifier.local, local);
426
+ }
427
+ }
428
+ else if (specifier.imported === 'SiTabsLegacyModule') {
429
+ legacyModuleLocals.add(specifier.local);
430
+ }
431
+ else {
432
+ appendImportSpecifiers(legacyImports, moduleName, [specifier]);
433
+ if (specifier.imported === 'SiTabDeselectionEvent') {
434
+ context.logger.warn(`${filePath}: SiTabDeselectionEvent has no direct replacement. Convert the handler to a canDeactivate guard.`);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ if (!hasLegacyImport) {
440
+ return [];
441
+ }
442
+ const defaultCurrentModule = [...currentImports.keys()][0] ??
443
+ firstLegacyCurrentModule ??
444
+ [...legacyImports.keys()][0]?.replace(/tabs-legacy$/, 'tabs') ??
445
+ '@siemens/element-ng/tabs';
446
+ if (uses.tab) {
447
+ ensureImport(currentImports, defaultCurrentModule, 'SiTabComponent');
448
+ }
449
+ if (uses.tabset) {
450
+ ensureImport(currentImports, defaultCurrentModule, 'SiTabsetComponent');
451
+ }
452
+ if (uses.tabLink) {
453
+ ensureImport(currentImports, defaultCurrentModule, 'SiTabLinkComponent');
454
+ }
455
+ const moduleReplacementImports = uses.tab || uses.tabLink || uses.tabset
456
+ ? [
457
+ ...(uses.tab ? ['SiTabComponent'] : []),
458
+ ...(uses.tabLink ? ['SiTabLinkComponent'] : []),
459
+ ...(uses.tabset ? ['SiTabsetComponent'] : [])
460
+ ]
461
+ : ['SiTabComponent', 'SiTabsetComponent'];
462
+ const moduleReplacementNames = legacyModuleLocals.size
463
+ ? moduleReplacementImports.map(name => ensureImport(currentImports, defaultCurrentModule, name))
464
+ : [];
465
+ const moduleReplacement = moduleReplacementNames.join(', ');
466
+ const edits = [];
467
+ const unsupportedModuleLocals = new Set();
468
+ const unsupportedSplitLocals = new Set();
469
+ const visit = (node) => {
470
+ if (ts.isImportDeclaration(node)) {
471
+ return;
472
+ }
473
+ if (ts.isIdentifier(node)) {
474
+ const replacement = identifierReplacements.get(node.text);
475
+ if (replacement) {
476
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: replacement });
477
+ return;
478
+ }
479
+ const splitReplacement = splitIdentifierReplacements.get(node.text);
480
+ if (splitReplacement) {
481
+ edits.push({
482
+ start: node.getStart(sourceFile),
483
+ end: node.getEnd(),
484
+ text: ts.isArrayLiteralExpression(node.parent)
485
+ ? splitReplacement.join(', ')
486
+ : splitReplacement[0]
487
+ });
488
+ if (!ts.isArrayLiteralExpression(node.parent)) {
489
+ unsupportedSplitLocals.add(node.text);
490
+ }
491
+ return;
492
+ }
493
+ if (legacyModuleLocals.has(node.text)) {
494
+ if (ts.isArrayLiteralExpression(node.parent)) {
495
+ edits.push({
496
+ start: node.getStart(sourceFile),
497
+ end: node.getEnd(),
498
+ text: moduleReplacement
499
+ });
500
+ }
501
+ else {
502
+ unsupportedModuleLocals.add(node.text);
503
+ }
504
+ return;
505
+ }
506
+ }
507
+ ts.forEachChild(node, visit);
508
+ };
509
+ sourceFile.forEachChild(visit);
510
+ if (unsupportedModuleLocals.size) {
511
+ for (const local of unsupportedModuleLocals) {
512
+ appendImportSpecifiers(legacyImports, defaultCurrentModule.replace(/tabs$/, 'tabs-legacy'), [
513
+ { imported: 'SiTabsLegacyModule', local, typeOnly: false }
514
+ ]);
515
+ }
516
+ context.logger.warn(`${filePath}: SiTabsLegacyModule is used outside an Angular imports array and requires manual migration.`);
517
+ }
518
+ if (unsupportedSplitLocals.size) {
519
+ context.logger.warn(`${filePath}: A legacy tab component is used outside an Angular imports array while the template uses both content and router tabs. The reference was migrated to SiTabComponent and requires review.`);
520
+ }
521
+ const replacement = [...renderImports(currentImports), ...renderImports(legacyImports)].join('\n');
522
+ const [firstImport, ...remainingImports] = relevantImports;
523
+ edits.push({
524
+ start: firstImport.getStart(sourceFile),
525
+ end: firstImport.getEnd(),
526
+ text: replacement
527
+ });
528
+ for (const importDeclaration of remainingImports) {
529
+ edits.push({
530
+ start: importDeclaration.getStart(sourceFile),
531
+ end: importDeclaration.getEnd(),
532
+ text: ''
533
+ });
534
+ }
535
+ return edits.filter(edit => sourceContent.substring(edit.start, edit.end) !== edit.text);
536
+ };
537
+ const renderImports = (imports) => {
538
+ return [...imports]
539
+ .filter(([, specifiers]) => specifiers.length)
540
+ .map(([moduleName, specifiers]) => {
541
+ const unique = deduplicateSpecifiers(specifiers);
542
+ const rendered = unique
543
+ .map(specifier => {
544
+ const typePrefix = specifier.typeOnly ? 'type ' : '';
545
+ const alias = specifier.imported === specifier.local ? '' : ` as ${specifier.local}`;
546
+ return `${typePrefix}${specifier.imported}${alias}`;
547
+ })
548
+ .join(', ');
549
+ return `import { ${rendered} } from '${moduleName}';`;
550
+ });
551
+ };
552
+ const toImportSpecifierData = (specifier) => ({
553
+ imported: specifier.propertyName?.text ?? specifier.name.text,
554
+ local: specifier.name.text,
555
+ typeOnly: specifier.isTypeOnly
556
+ });
557
+ const appendImportSpecifiers = (imports, moduleName, specifiers) => {
558
+ imports.set(moduleName, [...(imports.get(moduleName) ?? []), ...specifiers]);
559
+ };
560
+ const ensureImport = (imports, moduleName, imported, preferredLocal = imported, typeOnly = false) => {
561
+ for (const specifiers of imports.values()) {
562
+ const existing = specifiers.find(specifier => specifier.imported === imported);
563
+ if (existing) {
564
+ return existing.local;
565
+ }
566
+ }
567
+ appendImportSpecifiers(imports, moduleName, [{ imported, local: preferredLocal, typeOnly }]);
568
+ return preferredLocal;
569
+ };
570
+ const deduplicateSpecifiers = (specifiers) => {
571
+ const seen = new Set();
572
+ return specifiers.filter(specifier => {
573
+ const key = `${specifier.typeOnly}:${specifier.imported}:${specifier.local}`;
574
+ if (seen.has(key)) {
575
+ return false;
576
+ }
577
+ seen.add(key);
578
+ return true;
579
+ });
580
+ };
581
+ const getAttribute = (element, name) => element.attrs.find(attribute => attribute.name === name);
582
+ const findInput = (element, name) => element.attrs.find(attribute => [name, `[${name}]`, `bind-${name}`, `[(${name})]`].includes(attribute.name));
583
+ const getAttributeValue = (template, attribute) => attribute.valueSpan
584
+ ? template.substring(attribute.valueSpan.start.offset, attribute.valueSpan.end.offset)
585
+ : '';
586
+ const parseInteger = (value) => {
587
+ const normalized = value.trim();
588
+ return /^-?\d+$/.test(normalized) ? Number.parseInt(normalized, 10) : undefined;
589
+ };
590
+ const parenthesizeExpression = (expression) => {
591
+ const normalized = expression.trim();
592
+ return /^[A-Za-z_$][\w$]*(?:(?:\.|\?\.)[A-Za-z_$][\w$]*|\(\))*$/.test(normalized)
593
+ ? normalized
594
+ : `(${normalized})`;
595
+ };
596
+ const isAssignableExpression = (expression) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/.test(expression.trim());
597
+ const hasStructuralTemplateAttribute = (element) => element.name === 'ng-template' ||
598
+ element.attrs.some(attribute => [
599
+ '*ngFor',
600
+ '*ngIf',
601
+ '*ngSwitchCase',
602
+ '*ngSwitchDefault',
603
+ '[ngForOf]',
604
+ '[ngSwitch]',
605
+ 'ngFor'
606
+ ].includes(attribute.name));
607
+ const escapeAttributeValue = (value) => value.replaceAll('"', '"');
608
+ const guardEventExpression = (value) => splitStatements(value)
609
+ .map(statement => `$event && (${statement})`)
610
+ .join('; ');
611
+ const splitStatements = (value) => {
612
+ const statements = [];
613
+ let start = 0;
614
+ let depth = 0;
615
+ let quote;
616
+ let escaped = false;
617
+ for (let index = 0; index < value.length; index++) {
618
+ const character = value[index];
619
+ if (quote) {
620
+ if (escaped) {
621
+ escaped = false;
622
+ }
623
+ else if (character === '\\') {
624
+ escaped = true;
625
+ }
626
+ else if (character === quote) {
627
+ quote = undefined;
628
+ }
629
+ continue;
630
+ }
631
+ if (character === "'" || character === '"' || character === '`') {
632
+ quote = character;
633
+ }
634
+ else if (character === '(' || character === '[' || character === '{') {
635
+ depth++;
636
+ }
637
+ else if (character === ')' || character === ']' || character === '}') {
638
+ depth--;
639
+ }
640
+ else if (character === ';' && depth === 0) {
641
+ const statement = value.substring(start, index).trim();
642
+ if (statement) {
643
+ statements.push(statement);
644
+ }
645
+ start = index + 1;
646
+ }
647
+ }
648
+ const statement = value.substring(start).trim();
649
+ if (statement) {
650
+ statements.push(statement);
651
+ }
652
+ return statements;
653
+ };
654
+ const mergeUses = (target, source) => {
655
+ target.tab ||= source.tab;
656
+ target.tabLink ||= source.tabLink;
657
+ target.tabset ||= source.tabset;
658
+ };
659
+ const warnAt = (context, filePath, node, message) => {
660
+ const location = node.sourceSpan.start;
661
+ context.logger.warn(`${filePath}:${location.line + 1}:${location.col + 1}: ${message}`);
662
+ };
663
+ const applyTextEdits = (content, edits) => {
664
+ const ordered = [...edits].sort((left, right) => right.start - left.start || right.end - left.end);
665
+ let result = content;
666
+ let previousStart = content.length + 1;
667
+ for (const edit of ordered) {
668
+ if (edit.end > previousStart) {
669
+ throw new Error(`Overlapping migration edits at offsets ${edit.start}-${edit.end}.`);
670
+ }
671
+ result = result.substring(0, edit.start) + edit.text + result.substring(edit.end);
672
+ previousStart = edit.start;
673
+ }
674
+ return result;
675
+ };
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "migrateTabsLegacySchema",
4
+ "title": "Migrate legacy Element tabs to the current tabs API",
5
+ "type": "object",
6
+ "properties": {
7
+ "path": {
8
+ "type": "string",
9
+ "description": "Path to the directory where the migration should be applied.",
10
+ "format": "path",
11
+ "default": "/"
12
+ }
13
+ }
14
+ }