diff --git a/packages/demo/src/app/post-sample/components/datatable/datatable-paginated-demo/datatable-paginated-demo.component.html b/packages/demo/src/app/post-sample/components/datatable/datatable-paginated-demo/datatable-paginated-demo.component.html
index 0027079e48..da4a9ad30f 100644
--- a/packages/demo/src/app/post-sample/components/datatable/datatable-paginated-demo/datatable-paginated-demo.component.html
+++ b/packages/demo/src/app/post-sample/components/datatable/datatable-paginated-demo/datatable-paginated-demo.component.html
@@ -31,7 +31,7 @@
- Load 5 more
+ Load 5 more
diff --git a/packages/demo/src/app/post-sample/post-sample-components.module.ts b/packages/demo/src/app/post-sample/post-sample-components.module.ts
index 1b97dcfb57..2e0b50ef56 100644
--- a/packages/demo/src/app/post-sample/post-sample-components.module.ts
+++ b/packages/demo/src/app/post-sample/post-sample-components.module.ts
@@ -51,8 +51,6 @@ import { DatatableEditableDemoComponent } from './components/datatable/datatable
import { DatatableDemoPageComponent } from './components/datatable/datatable-demo-page/datatable-demo-page.component';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { FormsDemoPageComponent } from './components/forms/forms-demo-page/forms-demo-page.component';
-import { AccordionDemoComponent } from './components/accordion/accordion-demo/accordion-demo.component';
-import { AccordionDemoPageComponent } from './components/accordion/accordion-demo-page/accordion-demo-page.component';
import { DatatableLoadingDemoComponent } from './components/datatable/datatable-loading-demo/datatable-loading-demo.component';
@NgModule({
@@ -70,8 +68,6 @@ import { DatatableLoadingDemoComponent } from './components/datatable/datatable-
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [
- AccordionDemoComponent,
- AccordionDemoPageComponent,
SvgIconsDemoComponent,
IconsDemoPageComponent,
FeedbackDemoComponent,
diff --git a/packages/demo/src/styles.scss b/packages/demo/src/styles.scss
index 7ab0782e9a..20486e155e 100644
--- a/packages/demo/src/styles.scss
+++ b/packages/demo/src/styles.scss
@@ -119,7 +119,7 @@ code {
left: 100%;
top: 50%;
transform: scale(0.50) translateY(-50%);
- background-color: rgba(var(--post-bg-rgb), 0.8);
+ background-color: rgba(var(--post-bg-rgb), 0.95);
padding: 0 post.$size-small-regular;
transform-origin: top left;
z-index: 2;
diff --git a/packages/styles/schematics/migrations.json b/packages/styles/schematics/migrations.json
index e9f563dc29..437bb02ff5 100644
--- a/packages/styles/schematics/migrations.json
+++ b/packages/styles/schematics/migrations.json
@@ -2,55 +2,100 @@
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
- "migration-backgrounds": {
+ "migration-general-classes-bg-opacity": {
"version": "5.0.0",
- "description": "As of Design System v5 backgrounds make extensive use of CSS variables, therefore the .text-auto and .bg-[color]-opacity-80 classes are deprecated.",
- "factory": "./migrations/backgrounds/index"
+ "description": "Migrates general bg-opacity classes.",
+ "factory": "./migrations/general/classes/bg-opacity"
},
- "migration-badges": {
+ "migration-general-classes-secondary": {
"version": "5.0.0",
- "description": "https://getbootstrap.com/docs/5.0/migration/#badges",
- "factory": "./migrations/badges/index"
+ "description": "Migrates general bg-secondary, border-secondary and text-secondary classes.",
+ "factory": "./migrations/general/classes/secondary"
},
- "migration-buttons": {
+ "migration-general-text-auto": {
"version": "5.0.0",
- "description": "https://getbootstrap.com/docs/5.0/migration/#buttons",
- "factory": "./migrations/buttons/index"
+ "description": "Migrates general text-auto classes.",
+ "factory": "./migrations/general/classes/text-auto"
},
- "migration-close-button": {
+ "migration-general-rtl": {
"version": "5.0.0",
- "description": "https://getbootstrap.com/docs/5.0/migration/#close-button",
- "factory": "./migrations/close-button/index"
+ "description": "Migrates general rtl helper classes (https://getbootstrap.com/docs/5.0/migration/#rtl).",
+ "factory": "./migrations/general/classes/rtl"
},
- "migration-form-controls": {
+ "migration-general-sr-only": {
"version": "5.0.0",
- "description": "Applies automatic migrations on form elements (https://getbootstrap.com/docs/5.0/migration/#forms).",
- "factory": "./migrations/forms/index"
+ "description": "Migrates general sr-only helper classes (https://getbootstrap.com/docs/5.0/migration/#helpers).",
+ "factory": "./migrations/general/classes/sr-only"
},
- "migration-rtl": {
+ "migration-bootstrap-badge": {
"version": "5.0.0",
- "description": "https://getbootstrap.com/docs/5.0/migration/#rtl",
- "factory": "./migrations/rtl/index"
+ "description": "Migrates bootstrap badge component.",
+ "factory": "./migrations/bootstrap/badge"
},
- "migration-secondary-classes": {
+ "migration-bootstrap-blockquote": {
"version": "5.0.0",
- "description": "Removes secondary classes \"bg-secondary\", \"border-secondary\" and \"text-secondary\"",
- "factory": "./migrations/secondary-classes/index"
+ "description": "Migrates bootstrap blockquote component.",
+ "factory": "./migrations/bootstrap/blockquote"
+ },
+ "migration-bootstrap-button": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap button component.",
+ "factory": "./migrations/bootstrap/button"
+ },
+ "migration-bootstrap-button-close": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap close-button component.",
+ "factory": "./migrations/bootstrap/button-close"
+ },
+ "migration-bootstrap-form-control": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap form-control component.",
+ "factory": "./migrations/bootstrap/form-control"
+ },
+ "migration-bootstrap-form-select": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap form-select component.",
+ "factory": "./migrations/bootstrap/form-select"
+ },
+ "migration-bootstrap-textarea": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap textarea component.",
+ "factory": "./migrations/bootstrap/textarea"
+ },
+ "migration-bootstrap-form-checkbox": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap form-checkbox component.",
+ "factory": "./migrations/bootstrap/form-checkbox"
+ },
+ "migration-bootstrap-form-radio": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap form-radio component.",
+ "factory": "./migrations/bootstrap/form-radio"
+ },
+ "migration-bootstrap-form-switch": {
+ "version": "5.0.0",
+ "description": "Migrates bootstrap form-switch component.",
+ "factory": "./migrations/bootstrap/form-switch"
},
"migration-ng-buttons": {
"version": "5.0.0",
- "description": "Replaces ng-button classes.",
- "factory": "./migrations/ng-buttons/index"
+ "description": "Migrates ng-bootstrap button component.",
+ "factory": "./migrations/ngbootstrap/buttons"
+ },
+ "migration-post-custom-select": {
+ "version": "5.0.0",
+ "description": "Migrates post custom-select component.",
+ "factory": "./migrations/post/custom-select"
},
"migration-post-subnavigation": {
"version": "5.0.0",
- "description": "Applies the automatic migrations to the post-subnavigation component.",
- "factory": "./migrations/post-subnavigation/index"
+ "description": "Migrates post subnavigation component.",
+ "factory": "./migrations/post/subnavigation"
},
"migration-post-topic-teaser": {
"version": "5.0.0",
- "description": "Applies automatic migrations on topic-teaser elements.",
- "factory": "./migrations/post-topic-teaser/index"
+ "description": "Migrates post topic-teaser component.",
+ "factory": "./migrations/post/topic-teaser"
}
}
}
diff --git a/packages/styles/schematics/migrations/_example-dom-migration/index.ts b/packages/styles/schematics/migrations/_example-dom-migration/index.ts
index 98be0cd006..cb4efe8847 100644
--- a/packages/styles/schematics/migrations/_example-dom-migration/index.ts
+++ b/packages/styles/schematics/migrations/_example-dom-migration/index.ts
@@ -39,7 +39,7 @@
import { Rule } from '@angular-devkit/schematics';
import DomMigration from '../../utils/dom/migration';
import IDomUpdate from '../../utils/dom/update';
-import type { Cheerio } from 'cheerio';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
export default function (): Rule {
return new DomMigration(
@@ -47,41 +47,83 @@ export default function (): Rule {
new AddClassUpdate,
new AddAttributeUpdate,
new AddTextUpdate,
- new RemoveElementUpdate
+ new RemoveElementUpdate,
+ new WrapElementUpdate,
+ new ReplaceWithElementUpdate
).rule;
}
class AddElementUpdate implements IDomUpdate {
- selector = '#example-dom-element';
- update = function ($elements: Cheerio
) {
- $elements.append('It\'s working...
');
+ selector = '.example-dom-element';
+ update = function ($elements: Cheerio) {
+ $elements.append('It\'s working... ');
}
}
class AddClassUpdate implements IDomUpdate {
- selector = '#example-dom-element > div:not([class])';
- update = function ($elements: Cheerio) {
- $elements.addClass('inner');
+ selector = '.example-dom-element';
+ update = function ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((i, element) => {
+ if (i === 1) $(element).addClass('remove');
+ if (i === 2) $(element).addClass('wrap');
+ if (i === 3) $(element).addClass('replace-with');
+ });
}
}
class AddAttributeUpdate implements IDomUpdate {
- selector = '#example-dom-element .inner';
- update = function ($elements: Cheerio) {
+ selector = '.example-dom-element > span';
+ update = function ($elements: Cheerio) {
$elements.attr('style', 'padding: 10px; background-color: white;');
}
}
class AddTextUpdate implements IDomUpdate {
- selector = '#example-dom-element .inner';
- update = function ($elements: Cheerio) {
- $elements.text(`${$elements.text()} cheerio!`);
+ selector = '.example-dom-element > span';
+ update = function ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element.text(`${$element.text()} cheerio!`);
+ });
}
}
class RemoveElementUpdate implements IDomUpdate {
- selector = '#example-dom-element';
- update = function ($elements: Cheerio) {
- $elements.find('.remove-example').remove();
+ selector = '.example-dom-element.remove';
+ update = function ($elements: Cheerio) {
+ $elements.remove();
+ }
+}
+
+class WrapElementUpdate implements IDomUpdate {
+ selector = '.example-dom-element.wrap';
+ update = function ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ // to let the update work correctly you need to copy the data from the sourceElement to the distElement
+ // how to do this depends on the return value of the migration function used on the sourceElement
+ const $wrapper = $('
').data($element.data());
+
+ $element.wrap($wrapper);
+ });
+ }
+}
+
+class ReplaceWithElementUpdate implements IDomUpdate {
+ selector = '.example-dom-element.replace-with';
+ update = function ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ // to let the update work correctly you need to copy the data from the sourceElement to the distElement
+ // how to do this depends on the return value of the migration function used on the sourceElement
+ const $replacement = $(`${$element.prop('outerHTML')}
`).data($element.data());
+
+ $element.replaceWith($replacement);
+ });
}
}
diff --git a/packages/styles/schematics/migrations/backgrounds/index.ts b/packages/styles/schematics/migrations/backgrounds/index.ts
deleted file mode 100644
index d660a82bce..0000000000
--- a/packages/styles/schematics/migrations/backgrounds/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {Rule} from '@angular-devkit/schematics';
-import {CssClassesUpdate} from "../../utils/css/css-classes-update";
-import {CssMigration} from "../../utils/css/css-migration";
-
-/** Entry point for the backgrounds' migration. */
-export default function (): Rule {
- return new CssMigration(new TextAutoClassesUpdate).rule;
-}
-
-// The .text-auto class no longer exists
-class TextAutoClassesUpdate extends CssClassesUpdate {
- searchValue = 'text-auto';
- replaceValue = '';
-}
-
-// TODO: Replace .bg-[themeColor]-opacity-80 by style="--post-bg-opacity: 0.8"
diff --git a/packages/styles/schematics/migrations/badges/index.ts b/packages/styles/schematics/migrations/badges/index.ts
deleted file mode 100644
index f0dc3745a4..0000000000
--- a/packages/styles/schematics/migrations/badges/index.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import {Rule} from '@angular-devkit/schematics';
-import {CssClassesUpdate} from "../../utils/css/css-classes-update";
-import {themeColors} from "../../utils/css/constants";
-import {CssMigration} from "../../utils/css/css-migration";
-import {oneOf, optional} from "../../utils/regex";
-
-/** Entry point for the badges migration. */
-export default function (): Rule {
- return new CssMigration(new BadgeColorClassUpdate, new BadgePillClassUpdate, new GreyCarraraBadgeClassUpdate).rule;
-}
-
-// The .badge-* classes no longer exists
-class BadgeColorClassUpdate extends CssClassesUpdate {
- badgeStyleUpdates = new Map([['badge', 'bg'], ['badge-outline', 'border']]);
-
- searchValue = oneOf(this.badgeStyleUpdates.keys()) + '-' + oneOf(themeColors);
- replaceValue = (badgeStyle: string, color: string) => this.badgeStyleUpdates.get(badgeStyle) + '-' + color;
-}
-
-class BadgePillClassUpdate extends CssClassesUpdate {
- searchValue = 'badge-pill';
- replaceValue = 'rounded-pill';
-}
-
-class GreyCarraraBadgeClassUpdate extends CssClassesUpdate {
- searchValue = 'badge' + optional('(-outline)') + '-gray-carrara' + optional('(-thick)');
- replaceValue = (outline: string, thick: string) => (outline ? 'border-light' : 'bg-light') + (thick ? ' border-2' : '');
-}
diff --git a/packages/styles/schematics/migrations/bootstrap/badge/index.ts b/packages/styles/schematics/migrations/bootstrap/badge/index.ts
new file mode 100644
index 0000000000..2ee33340f8
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/badge/index.ts
@@ -0,0 +1,102 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+import { themeColors } from '../../../utils/constants';
+
+export default function (): Rule {
+ return new DomMigration(
+ new BadgePillClassUpdate,
+ new BadgeBGClassUpdate,
+ new BadgeOutlineClassUpdate,
+ new BadgeCararraClassUpdate,
+ new BadgeCararraThickClassUpdate
+ ).rule;
+}
+
+class BadgePillClassUpdate implements IDomUpdate {
+ selector = '.badge-pill';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('badge-pill')
+ .addClass('rounded-pill');
+ }
+}
+
+class BadgeBGClassUpdate implements IDomUpdate {
+ cssClassRegex: RegExp = new RegExp(`^badge-(${themeColors.join('|')})$`);
+
+ selector = themeColors.map(colorname => `.badge-${colorname}`).join(', ');
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const colorname = match[1];
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`bg-${colorname}`);
+ }
+ });
+ });
+ }
+}
+
+class BadgeOutlineClassUpdate implements IDomUpdate {
+ cssClassRegex: RegExp = new RegExp(`^badge-outline-(${themeColors.join('|')})$`);
+
+ selector = themeColors.map(colorname => `.badge-outline-${colorname}`).join(', ');
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const colorname = match[1];
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`border-${colorname}`);
+ }
+ });
+ });
+ }
+}
+
+class BadgeCararraClassUpdate implements IDomUpdate {
+ selector = '.badge-gray-cararra';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('badge-gray-cararra')
+ .addClass('bg-light');
+ }
+}
+
+class BadgeCararraThickClassUpdate implements IDomUpdate {
+ selector = '.badge-outline-gray-cararra-thick';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('badge-outline-gray-cararra-thick')
+ .addClass('border-light border-2');
+ }
+}
\ No newline at end of file
diff --git a/packages/styles/schematics/migrations/bootstrap/blockquote/index.ts b/packages/styles/schematics/migrations/bootstrap/blockquote/index.ts
new file mode 100644
index 0000000000..1ffe959d82
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/blockquote/index.ts
@@ -0,0 +1,46 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new BlockquoteFigureWrapperUpdate,
+ new BlockquotePClassUpdate
+ ).rule;
+}
+
+class BlockquoteFigureWrapperUpdate implements IDomUpdate {
+ selector = 'blockquote.blockquote';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $footer = $element.find('footer.blockquote-footer');
+ const hasFooter = $footer.length > 0;
+
+ if (hasFooter) {
+ const $wrapper = $(' ').data($element.data());
+ const $replacer = $(`${$footer.prop('innerHTML')} `);
+
+ $footer
+ .prop('attributes')
+ .forEach(({ name, value }) => {
+ $replacer.attr(name, value);
+ });
+
+ $footer.replaceWith($replacer);
+ $element.wrap($wrapper);
+ }
+ });
+ }
+}
+
+class BlockquotePClassUpdate implements IDomUpdate {
+ selector = 'blockquote.blockquote > p.mb-0';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('mb-0');
+ }
+}
\ No newline at end of file
diff --git a/packages/styles/schematics/migrations/bootstrap/button-close/index.ts b/packages/styles/schematics/migrations/bootstrap/button-close/index.ts
new file mode 100644
index 0000000000..49203fd4f3
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/button-close/index.ts
@@ -0,0 +1,39 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new ButtonCloseClassesUpdate,
+ new ButtonCloseRemoveIconContentUpdate
+ ).rule;
+}
+
+class ButtonCloseClassesUpdate implements IDomUpdate {
+ selector = '.close';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $child = $element.find('> span[aria-hidden="true"]');
+
+ if ($child.length > 0) {
+ $element
+ .removeClass('close btn btn-icon')
+ .addClass('btn-close');
+ }
+ })
+ }
+}
+
+class ButtonCloseRemoveIconContentUpdate implements IDomUpdate {
+ selector = '.btn-close';
+
+ update ($elements: Cheerio) {
+ $elements
+ .find('> span[aria-hidden="true"]')
+ .remove();
+ }
+}
\ No newline at end of file
diff --git a/packages/styles/schematics/migrations/bootstrap/button/index.ts b/packages/styles/schematics/migrations/bootstrap/button/index.ts
new file mode 100644
index 0000000000..f03a4f8614
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/button/index.ts
@@ -0,0 +1,67 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+import { themeColors } from '../../../utils/constants';
+
+export default function (): Rule {
+ return new DomMigration(
+ new ButtonOutlineClassUpdate,
+ new ButtonInvertedClassUpdate,
+ new ButtonIconClassesUpdate
+ ).rule;
+}
+
+class ButtonOutlineClassUpdate implements IDomUpdate {
+ cssClassRegex: RegExp = new RegExp(`^btn-outline-(${themeColors.join('|')})$`);
+
+ selector = themeColors.map(colorname => `.btn-outline-${colorname}`).join(', ');
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ $element
+ .removeClass(cssClass)
+ .addClass('btn-secondary');
+ }
+ });
+ });
+ }
+}
+
+class ButtonInvertedClassUpdate implements IDomUpdate {
+ selector = '.btn.btn-inverted';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('btn-inverted');
+ }
+}
+
+class ButtonIconClassesUpdate implements IDomUpdate {
+ selector = '.btn-icon';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $icon = $element.find('.pi');
+ const $text = $element.find(':not(.pi, .sr-only, .sr-only-focusable, .visually-hidden, .visually-hidden-focusable)');
+
+ const isButtonWithIconAndText = $icon.length > 0 && $text.length > 0 && $text.text().length > 0;
+
+ if (isButtonWithIconAndText) {
+ $element.removeClass('btn-icon');
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/form-checkbox/index.ts b/packages/styles/schematics/migrations/bootstrap/form-checkbox/index.ts
new file mode 100644
index 0000000000..f714131b24
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/form-checkbox/index.ts
@@ -0,0 +1,54 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormCheckboxInputClassesUpdate,
+ new FormCheckboxLabelClassesUpdate,
+ new FormCheckboxClassesUpdate
+ ).rule;
+}
+
+class FormCheckboxInputClassesUpdate implements IDomUpdate {
+ selector = '.custom-checkbox.custom-control input.custom-control-input';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('custom-control-input')
+ .addClass('form-check-input');
+ }
+}
+
+class FormCheckboxLabelClassesUpdate implements IDomUpdate {
+ selector = '.custom-checkbox.custom-control label.custom-control-label';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('custom-control-label')
+ .addClass('form-check-label');
+ }
+}
+
+class FormCheckboxClassesUpdate implements IDomUpdate {
+ selector = '.custom-checkbox.custom-control';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const hasInlineClass = $element.hasClass('custom-control-inline');
+
+ $element
+ .removeClass('custom-checkbox custom-control')
+ .addClass('form-check');
+
+ if (hasInlineClass) {
+ $element
+ .removeClass('custom-control-inline')
+ .addClass('form-check-inline');
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/form-control/index.ts b/packages/styles/schematics/migrations/bootstrap/form-control/index.ts
new file mode 100644
index 0000000000..82d0499da9
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/form-control/index.ts
@@ -0,0 +1,32 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormControlFloatingLabelWrapperUpdate
+ ).rule;
+}
+
+class FormControlFloatingLabelWrapperUpdate implements IDomUpdate {
+ selector = '.form-group';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.find('> input.form-control-lg');
+ const $label = $control.next('label');
+ const isFloatingLabel = $control.length > 0 && $label.length > 0;
+
+ if (isFloatingLabel) {
+ $element
+ .removeClass('form-group')
+ .addClass('form-floating');
+
+ $control.removeClass('form-control-lg');
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/form-radio/index.ts b/packages/styles/schematics/migrations/bootstrap/form-radio/index.ts
new file mode 100644
index 0000000000..a9ddd05b43
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/form-radio/index.ts
@@ -0,0 +1,54 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormRadioInputClassesUpdate,
+ new FormRadioLabelClassesUpdate,
+ new FormRadioClassesUpdate
+ ).rule;
+}
+
+class FormRadioInputClassesUpdate implements IDomUpdate {
+ selector = '.custom-radio.custom-control input.custom-control-input';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('custom-control-input')
+ .addClass('form-check-input');
+ }
+}
+
+class FormRadioLabelClassesUpdate implements IDomUpdate {
+ selector = '.custom-radio.custom-control label.custom-control-label';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('custom-control-label')
+ .addClass('form-check-label');
+ }
+}
+
+class FormRadioClassesUpdate implements IDomUpdate {
+ selector = '.custom-radio.custom-control';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const hasInlineClass = $element.hasClass('custom-control-inline');
+
+ $element
+ .removeClass('custom-radio custom-control')
+ .addClass('form-check');
+
+ if (hasInlineClass) {
+ $element
+ .removeClass('custom-control-inline')
+ .addClass('form-check-inline');
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/form-select/index.ts b/packages/styles/schematics/migrations/bootstrap/form-select/index.ts
new file mode 100644
index 0000000000..d63526be22
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/form-select/index.ts
@@ -0,0 +1,64 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+import { breakpoints } from "../../../utils/constants";
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormSelectFloatingLabelWrapperUpdate,
+ new FormSelectCustomClassesUpdate
+ ).rule;
+}
+
+class FormSelectFloatingLabelWrapperUpdate implements IDomUpdate {
+ selector = '.form-group';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.find('> select.form-control-lg');
+ const $label = $control.next('label');
+ const isFloatingLabel = $control.length > 0 && $label.length > 0;
+
+ if (isFloatingLabel) {
+ $element
+ .removeClass('form-group')
+ .addClass('form-floating');
+
+ $control.removeClass('form-control-lg');
+ }
+ });
+ }
+}
+
+class FormSelectCustomClassesUpdate implements IDomUpdate {
+ cssClassRegex: RegExp = new RegExp(`^form-control-(${breakpoints.join('|')})$`);
+ selector = 'select.form-control';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .removeClass('form-control custom-select')
+ .addClass('form-select')
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const breakpoint = match[1];
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`form-select-${breakpoint}`);
+ }
+ });
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/form-switch/index.ts b/packages/styles/schematics/migrations/bootstrap/form-switch/index.ts
new file mode 100644
index 0000000000..b3f87a5367
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/form-switch/index.ts
@@ -0,0 +1,50 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormSwitchInputClassesUpdate,
+ new FormSwitchLabelUpdate,
+ new FormSwitchClassesUpdate
+ ).rule;
+}
+
+class FormSwitchInputClassesUpdate implements IDomUpdate {
+ selector = '.switch input.switch';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('switch')
+ .addClass('form-check-input');
+ }
+}
+
+class FormSwitchLabelUpdate implements IDomUpdate {
+ selector = '.switch';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $toggler = $element.find('label.switch-toggler');
+ const $startLabel = $toggler.prev('label');
+ const $endLabel = $toggler.next('label');
+
+ $startLabel.addClass('form-check-label order-first');
+ $endLabel.addClass('form-check-label');
+ $toggler.remove();
+ });
+ }
+}
+
+class FormSwitchClassesUpdate implements IDomUpdate {
+ selector = '.switch';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('switch')
+ .addClass('form-check form-switch');
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/forms/index.ts b/packages/styles/schematics/migrations/bootstrap/forms/index.ts
new file mode 100644
index 0000000000..72592d3cd9
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/forms/index.ts
@@ -0,0 +1,56 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new FormGroupClassUpdate,
+ new FormLabelClassUpdate,
+ new FormTextClassUpdate
+ ).rule;
+}
+
+class FormGroupClassUpdate implements IDomUpdate {
+ selector = '.form-group';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.find('> input.form-control-lg, select.form-control-lg, > textarea');
+
+ const isFloatingLabel = $control.length > 0;
+
+ if (!isFloatingLabel) {
+ $element
+ .removeClass('form-group')
+ .addClass('mb-regular');
+ }
+ });
+ }
+}
+
+class FormLabelClassUpdate implements IDomUpdate {
+ selector = 'label, [for]';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.siblings('input:visible, select:visible, textarea:visible');
+
+ if ($control.length > 0) {
+ $element.addClass('form-label');
+ }
+ });
+ }
+}
+
+class FormTextClassUpdate implements IDomUpdate {
+ selector = '.form-text';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('small text-muted');
+ }
+}
diff --git a/packages/styles/schematics/migrations/bootstrap/textarea/index.ts b/packages/styles/schematics/migrations/bootstrap/textarea/index.ts
new file mode 100644
index 0000000000..3e5657201f
--- /dev/null
+++ b/packages/styles/schematics/migrations/bootstrap/textarea/index.ts
@@ -0,0 +1,45 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new TextareaFloatingLabelWrapperUpdate
+ ).rule;
+}
+
+class TextareaFloatingLabelWrapperUpdate implements IDomUpdate {
+ selector = '.form-group';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.find('> textarea');
+ const $label = $element.next('label');
+ const $description = $control.siblings(`[for="${$control.attr('id')}"]`);
+
+ const labelText = $label.text() || $description.text();
+
+ const setLabel = $label.length === 0 && $description.length > 0;
+ const setPlaceholder = $description.length > 0 && $control.attr('placeholder') === undefined;
+ const isFloatingLabel = $control.length > 0;
+
+ if (isFloatingLabel) {
+ $element
+ .removeClass('form-group')
+ .addClass('form-floating');
+
+ if (setLabel) {
+ $(`${labelText} `).insertAfter($control);
+ $description.remove();
+ }
+
+ if (setPlaceholder) {
+ $control.attr('placeholder', labelText);
+ }
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/buttons/index.ts b/packages/styles/schematics/migrations/buttons/index.ts
deleted file mode 100644
index 011f356dc7..0000000000
--- a/packages/styles/schematics/migrations/buttons/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {Rule} from '@angular-devkit/schematics';
-import {themeColors} from "../../utils/css/constants";
-import {CssClassesUpdate} from "../../utils/css/css-classes-update";
-import {CssMigration} from "../../utils/css/css-migration";
-import {oneOf} from "../../utils/regex";
-
-/** Entry point for the buttons' migration. */
-export default function (): Rule {
- return new CssMigration(new OutlinedButtonClassesUpdate, new InvertedButtonClassesUpdate).rule;
-}
-
-// The outlined colored buttons no longer exist
-class OutlinedButtonClassesUpdate extends CssClassesUpdate {
- searchValue = 'btn-outline-' + oneOf(themeColors);
- replaceValue = (color: string) => 'btn-' + color;
-}
-
-// The .btn-inverted class no longer exists
-class InvertedButtonClassesUpdate extends CssClassesUpdate {
- searchValue = 'btn-inverted';
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/migrations/close-button/index.ts b/packages/styles/schematics/migrations/close-button/index.ts
deleted file mode 100644
index acaad09b2c..0000000000
--- a/packages/styles/schematics/migrations/close-button/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {Rule} from '@angular-devkit/schematics';
-import {CssClassesUpdate} from "../../utils/css/css-classes-update";
-import {CssMigration} from "../../utils/css/css-migration";
-import { oneOf } from '../../utils/regex';
-
-/** Entry point for the close button migration. */
-export default function (): Rule {
- return new CssMigration(new CloseButtonClassesUpdate, new CloseButtonClassesRemove).rule;
-}
-
-class CloseButtonClassesUpdate extends CssClassesUpdate {
- override classSelector = 'close';
- searchValue = 'close';
- replaceValue = 'btn-close';
-}
-
-class CloseButtonClassesRemove extends CssClassesUpdate {
- override classSelector = oneOf(['close', 'btn-close']);
- searchValue = oneOf(['btn', 'btn-icon']);
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/migrations/forms/index.ts b/packages/styles/schematics/migrations/forms/index.ts
deleted file mode 100644
index 9613ee68d8..0000000000
--- a/packages/styles/schematics/migrations/forms/index.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Rule } from '@angular-devkit/schematics';
-import { CssClassesUpdate } from "../../utils/css/css-classes-update";
-import { CssMigration } from "../../utils/css/css-migration";
-import { oneOf, optional } from "../../utils/regex";
-
-export default function (): Rule {
- return new CssMigration(
- new FormGroupClassesUpdate,
- new FormTextClassesUpdate,
- new FormSelectClassesUpdate,
- new FormSelectMenuClassesUpdate,
- new FormCheckClassesUpdate,
- new FormCheckChildrenClassesUpdate,
- new FormSwitchClassesUpdate,
- new FormSwitchInputClassesUpdate,
- new FormSwitchTogglerClassesUpdate,
- ).rule;
-}
-
-class FormGroupClassesUpdate extends CssClassesUpdate {
- searchValue = 'form-group';
- replaceValue = 'mb-regular';
-}
-
-class FormTextClassesUpdate extends CssClassesUpdate {
- override classSelector = 'form-text';
- searchValue = oneOf(['small', 'text-muted']);
- replaceValue = '';
-}
-
-// Applied on select, multiselect and custom-select elements
-class FormSelectClassesUpdate extends CssClassesUpdate {
- override classSelector = 'custom-select';
- searchValue = `form-control${optional(`-${oneOf(['sm', 'rg', 'md', 'lg'])}`)}`;
- replaceValue = (size: string) => `form-select${size ? `-${size}` : ''}`;
-}
-
-class FormSelectMenuClassesUpdate extends CssClassesUpdate {
- override classSelector = 'custom-select-menu';
- searchValue = 'custom-select-menu';
- replaceValue = 'w-100 mw-100';
-}
-
-class FormCheckClassesUpdate extends CssClassesUpdate {
- override classSelector = oneOf(['custom-checkbox', 'custom-radio']);
- searchValue = oneOf(['custom-control', 'custom-checkbox', 'custom-radio']);
- replaceValue = 'form-check';
-}
-
-class FormCheckChildrenClassesUpdate extends CssClassesUpdate {
- override classSelector = oneOf(['custom-control-input', 'custom-control-label', 'custom-control-inline']);
- searchValue = `custom-control-${oneOf(['input', 'label', 'inline'])}`;
- replaceValue = (child: string) => `form-check-${child}`;
-}
-
-class FormSwitchClassesUpdate extends CssClassesUpdate {
- override tagSelector = 'div';
- override classSelector = 'switch';
- searchValue = 'switch';
- replaceValue = 'form-check form-switch';
-}
-
-class FormSwitchInputClassesUpdate extends CssClassesUpdate {
- override tagSelector = 'input';
- override classSelector = 'switch';
- searchValue = 'switch';
- replaceValue = 'form-check-input';
-}
-
-class FormSwitchTogglerClassesUpdate extends CssClassesUpdate {
- override classSelector = 'switch-toggler';
- searchValue = 'switch-toggler';
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/migrations/general/classes/bg-opacity.ts b/packages/styles/schematics/migrations/general/classes/bg-opacity.ts
new file mode 100644
index 0000000000..10d4ec3129
--- /dev/null
+++ b/packages/styles/schematics/migrations/general/classes/bg-opacity.ts
@@ -0,0 +1,42 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+import { themeColors } from '../../../utils/constants';
+
+export default function (): Rule {
+ return new DomMigration(
+ new BackgroundOpacityClassesUpdate
+ ).rule;
+}
+
+class BackgroundOpacityClassesUpdate implements IDomUpdate {
+ cssClassRegex: RegExp = new RegExp(`^bg-(${themeColors.join('|')})-opacity-(\\d+)$`);
+
+ selector = themeColors.map(colorname => `[class*="bg-${colorname}-opacity-"]`).join(', ');
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const colorname = match[1];
+ const opacityvalue = Number(match[2]);
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`bg-${colorname}`)
+ .css('--post-bg-opacity', `${opacityvalue / 100}`);
+ }
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/packages/styles/schematics/migrations/general/classes/rtl.ts b/packages/styles/schematics/migrations/general/classes/rtl.ts
new file mode 100644
index 0000000000..ee0c6ab4ee
--- /dev/null
+++ b/packages/styles/schematics/migrations/general/classes/rtl.ts
@@ -0,0 +1,92 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+import { breakpoints, sizes } from "../../../utils/constants";
+
+export default function (): Rule {
+ return new DomMigration(
+ new SpacingClassesUpdate,
+ new AlignmentClassesUpdate
+ ).rule;
+}
+
+class SpacingClassesUpdate implements IDomUpdate {
+ cssClassRegex = new RegExp(`^(m|p)(l|r)(?:-(${breakpoints.join('|')}))?-(${sizes.join('|')})$`);
+ sideUpdate = new Map([['l', 's'], ['r', 'e']]);
+
+ selector = this.createSelectors();
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const property = match[1];
+ const side = match[2];
+ const breakpoint = match[3];
+ const size = match[4];
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`${property}${this.sideUpdate.get(side)}${breakpoint ? `-${breakpoint}` : ''}-${size}`);
+ }
+ });
+ })
+ }
+
+ createSelectors () {
+ const selectorProperties = ['m', 'p'];
+ const selectorSides = ['l', 'r'];
+ const selectorBreakpoints = [''].concat(breakpoints);
+
+ return selectorProperties.map(property => selectorSides.map(side => selectorBreakpoints.map(breakpoint => sizes.map(size => `.${property}${side}${breakpoint ? `-${breakpoint}` : ''}-${size}`)))).join(', ');
+ }
+}
+
+class AlignmentClassesUpdate implements IDomUpdate {
+ cssClassRegex = new RegExp(`^(float|text)(?:-(${breakpoints.join('|')}))?-(left|right)$`);
+ sideUpdate = new Map([['left', 'start'], ['right', 'end']]);
+
+ selector = this.createSelectors();
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const match = cssClass.match(this.cssClassRegex);
+
+ if (match) {
+ const property = match[1];
+ const breakpoint = match[2];
+ const side = match[3];
+
+ $element
+ .removeClass(cssClass)
+ .addClass(`${property}${breakpoint ? `-${breakpoint}` : ''}-${this.sideUpdate.get(side)}`);
+ }
+ });
+ })
+ }
+
+ createSelectors () {
+ const selectorProperties = ['float', 'text'];
+ const selectorBreakpoints = [''].concat(breakpoints);
+ const selectorSides = ['left', 'right'];
+
+ return selectorProperties.map(property => selectorBreakpoints.map(breakpoint => selectorSides.map(side => `.${property}${breakpoint ? `-${breakpoint}` : ''}-${side}`))).join(', ');
+ }
+}
diff --git a/packages/styles/schematics/migrations/general/classes/secondary.ts b/packages/styles/schematics/migrations/general/classes/secondary.ts
new file mode 100644
index 0000000000..2b1f89406f
--- /dev/null
+++ b/packages/styles/schematics/migrations/general/classes/secondary.ts
@@ -0,0 +1,18 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new SecondaryClassesUpdate
+ ).rule;
+}
+
+class SecondaryClassesUpdate implements IDomUpdate {
+ selector = '.bg-secondary, .border-secondary, .text-secondary';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('bg-secondary border-secondary text-secondary');
+ }
+}
diff --git a/packages/styles/schematics/migrations/general/classes/sr-only.ts b/packages/styles/schematics/migrations/general/classes/sr-only.ts
new file mode 100644
index 0000000000..ff50e0cc63
--- /dev/null
+++ b/packages/styles/schematics/migrations/general/classes/sr-only.ts
@@ -0,0 +1,31 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new SrOnlyClassUpdate,
+ new SrOnlyFocusableClassUpdate
+ ).rule;
+}
+
+class SrOnlyClassUpdate implements IDomUpdate {
+ selector = '.sr-only';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('sr-only')
+ .addClass('visually-hidden');
+ }
+ }
+
+ class SrOnlyFocusableClassUpdate implements IDomUpdate {
+ selector = '.sr-only-focusable';
+
+ update ($elements: Cheerio) {
+ $elements
+ .removeClass('sr-only-focusable')
+ .addClass('visually-hidden-focusable');
+ }
+}
diff --git a/packages/styles/schematics/migrations/general/classes/text-auto.ts b/packages/styles/schematics/migrations/general/classes/text-auto.ts
new file mode 100644
index 0000000000..a1d9d8cd24
--- /dev/null
+++ b/packages/styles/schematics/migrations/general/classes/text-auto.ts
@@ -0,0 +1,18 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new TextAutoClassUpdate
+ ).rule;
+}
+
+class TextAutoClassUpdate implements IDomUpdate {
+ selector = '.text-auto';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('text-auto');
+ }
+}
diff --git a/packages/styles/schematics/migrations/ng-buttons/index.ts b/packages/styles/schematics/migrations/ng-buttons/index.ts
deleted file mode 100644
index ea221018b2..0000000000
--- a/packages/styles/schematics/migrations/ng-buttons/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Rule } from '@angular-devkit/schematics';
-import { CssClassesUpdate } from "../../utils/css/css-classes-update";
-import { CssMigration } from "../../utils/css/css-migration";
-
-/** Entry point for the ng-buttons migration. */
-export default function (): Rule {
- return new CssMigration(new NgButtonClassUpdate).rule;
-}
-
-// The label classes have changed
-class NgButtonClassUpdate extends CssClassesUpdate {
- override tagSelector = 'label';
- override classSelector = 'btn-primary';
- override attributeSelector = 'ngbButtonLabel';
- searchValue = 'btn-primary';
- replaceValue = 'btn-secondary';
-}
diff --git a/packages/styles/schematics/migrations/ngbootstrap/buttons/index.ts b/packages/styles/schematics/migrations/ngbootstrap/buttons/index.ts
new file mode 100644
index 0000000000..460bfa152e
--- /dev/null
+++ b/packages/styles/schematics/migrations/ngbootstrap/buttons/index.ts
@@ -0,0 +1,46 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new ButtonGroupClassUpdate,
+ new ButtonLabelClassUpdate,
+ new ButtonInputClassUpdate
+ ).rule;
+}
+
+class ButtonGroupClassUpdate implements IDomUpdate {
+ selector = '.btn-group.btn-group-toggle';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('btn-group-toggle');
+ }
+}
+
+class ButtonLabelClassUpdate implements IDomUpdate {
+ selector = '.btn-group label.btn-primary';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const isNgbButtonLabel = $element.attr('ngbButtonLabel') !== undefined;
+
+ if (isNgbButtonLabel) {
+ $element
+ .removeClass('btn-primary')
+ .addClass('btn btn-secondary');
+ }
+ });
+ }
+}
+
+class ButtonInputClassUpdate implements IDomUpdate {
+ selector = '.btn-group input[ngbButton]';
+
+ update ($elements: Cheerio) {
+ $elements.addClass('btn-check');
+ }
+}
diff --git a/packages/styles/schematics/migrations/post-subnavigation/index.ts b/packages/styles/schematics/migrations/post-subnavigation/index.ts
deleted file mode 100644
index 488c53461a..0000000000
--- a/packages/styles/schematics/migrations/post-subnavigation/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Rule } from '@angular-devkit/schematics';
-import { CssClassesUpdate } from '../../utils/css/css-classes-update';
-import { CssMigration } from '../../utils/css/css-migration';
-
-/** Entry point for the post-subnavigation migration. */
-export default function (): Rule {
- return new CssMigration(new SecondaryClassesUpdate).rule;
-}
-
-// The .subnavigation-inverted class no longer exists
-class SecondaryClassesUpdate extends CssClassesUpdate {
- override classSelector: 'subnavigation-inverted';
- searchValue = 'subnavigation-inverted';
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/migrations/post-topic-teaser/index.ts b/packages/styles/schematics/migrations/post-topic-teaser/index.ts
deleted file mode 100644
index e56ad0df87..0000000000
--- a/packages/styles/schematics/migrations/post-topic-teaser/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Rule } from '@angular-devkit/schematics';
-import { CssClassesUpdate } from '../../utils/css/css-classes-update';
-import { CssMigration } from '../../utils/css/css-migration';
-import { oneOf } from '../../utils/regex';
-
-export default function (): Rule {
- return new CssMigration(
- new TopicTeaserImageContainerClassesUpdate,
- new TopicTeaserContentClassesUpdate,
- new TopicTeaserLinkListClassesUpdate
- ).rule;
-}
-
-class TopicTeaserImageContainerClassesUpdate extends CssClassesUpdate {
- override classSelector = 'topic-teaser-image-container';
- searchValue = oneOf(['col-10', 'col-rg-8', 'col-lg-4']);
- replaceValue = '';
-}
-
-class TopicTeaserContentClassesUpdate extends CssClassesUpdate {
- override classSelector = 'topic-teaser-content';
- searchValue = oneOf(['col-12', 'col-lg-8']);
- replaceValue = '';
-}
-
-class TopicTeaserLinkListClassesUpdate extends CssClassesUpdate {
- override tagSelector = 'ul';
- override classSelector = 'link-list';
- searchValue = 'font-curve-regular';
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/migrations/post/custom-select/index.ts b/packages/styles/schematics/migrations/post/custom-select/index.ts
new file mode 100644
index 0000000000..2800eb785e
--- /dev/null
+++ b/packages/styles/schematics/migrations/post/custom-select/index.ts
@@ -0,0 +1,71 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode, CheerioAPI } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new CustomSelectFloatingLabelWrapperUpdate,
+ new CustomSelectClassesUpdate,
+ new CustomSelectMenuClassesUpdate
+ ).rule;
+}
+
+class CustomSelectFloatingLabelWrapperUpdate implements IDomUpdate {
+ selector = '.form-group';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const $control = $element.find('> button.form-control-lg');
+ const $label = $control.next('label');
+ const isNgbDropdown = $element.attr('ngbDropdown') !== undefined;
+ const isFloatingLabel = $control.length > 0 && $label.length > 0;
+
+ if (isNgbDropdown && isFloatingLabel) {
+ $element
+ .removeClass('form-group')
+ .addClass('form-floating');
+
+ $control.removeClass('form-control-lg');
+ }
+ });
+ }
+}
+
+class CustomSelectClassesUpdate implements IDomUpdate {
+ selector = 'button.form-control';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const isNgbDropdownToggle = $element.attr('ngbDropdownToggle') !== undefined;
+
+ if (isNgbDropdownToggle) {
+ $element
+ .removeClass('form-control custom-select')
+ .addClass('form-select');
+ }
+ });
+ }
+}
+
+class CustomSelectMenuClassesUpdate implements IDomUpdate {
+ selector = '.custom-select-menu';
+
+ update ($elements: Cheerio, $: CheerioAPI) {
+ $elements
+ .each((_i, element) => {
+ const $element = $(element);
+ const isNgbDropdownMenu = $element.attr('ngbDropdownMenu') !== undefined;
+
+ if (isNgbDropdownMenu) {
+ $element
+ .removeClass('custom-select-menu')
+ .addClass('w-100 mw-100');
+ }
+ });
+ }
+}
diff --git a/packages/styles/schematics/migrations/post/subnavigation/index.ts b/packages/styles/schematics/migrations/post/subnavigation/index.ts
new file mode 100644
index 0000000000..c80fd41bf9
--- /dev/null
+++ b/packages/styles/schematics/migrations/post/subnavigation/index.ts
@@ -0,0 +1,18 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new SubnavigationInvertedClassUpdate
+ ).rule;
+}
+
+class SubnavigationInvertedClassUpdate implements IDomUpdate {
+ selector = '.subnavigation-inverted';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('subnavigation-inverted');
+ }
+}
diff --git a/packages/styles/schematics/migrations/post/topic-teaser/index.ts b/packages/styles/schematics/migrations/post/topic-teaser/index.ts
new file mode 100644
index 0000000000..3395e6cc06
--- /dev/null
+++ b/packages/styles/schematics/migrations/post/topic-teaser/index.ts
@@ -0,0 +1,47 @@
+import { Rule } from '@angular-devkit/schematics';
+import DomMigration from '../../../utils/dom/migration';
+import IDomUpdate from '../../../utils/dom/update';
+import type { Cheerio, AnyNode } from 'cheerio';
+
+export default function (): Rule {
+ return new DomMigration(
+ new TopicTeaserImageWidthAndHeightUpdate,
+ new TopicTeaserImageClassesUpdate,
+ new TopicTeaserContentClassesUpdate,
+ new TopicTeaserLinkListClassesUpdate
+ ).rule;
+}
+
+class TopicTeaserImageWidthAndHeightUpdate implements IDomUpdate {
+ selector = '.topic-teaser-image';
+
+ update ($elements: Cheerio) {
+ $elements
+ .attr('width', '100%')
+ .attr('height', '100%');
+ }
+}
+
+class TopicTeaserImageClassesUpdate implements IDomUpdate {
+ selector = '.topic-teaser-image-container';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('col-10 col-rg-8 col-lg-4');
+ }
+}
+
+class TopicTeaserContentClassesUpdate implements IDomUpdate {
+ selector = '.topic-teaser-content';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('col-12 col-lg-8');
+ }
+}
+
+class TopicTeaserLinkListClassesUpdate implements IDomUpdate {
+ selector = '.topic-teaser ul.link-list';
+
+ update ($elements: Cheerio) {
+ $elements.removeClass('font-curve-regular');
+ }
+}
diff --git a/packages/styles/schematics/migrations/rtl/index.ts b/packages/styles/schematics/migrations/rtl/index.ts
deleted file mode 100644
index 9b7007560c..0000000000
--- a/packages/styles/schematics/migrations/rtl/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {Rule} from '@angular-devkit/schematics';
-import {CssClassesUpdate} from "../../utils/css/css-classes-update";
-import {breakpoints, sizes} from "../../utils/css/constants";
-import {CssMigration} from "../../utils/css/css-migration";
-import {oneOf, optional} from "../../utils/regex";
-
-/** Entry point for the RTL migration. */
-export default function (): Rule {
- return new CssMigration(new MarginAndPaddingClassesUpdate, new TextAlignmentAndFloatClassesUpdate).rule;
-}
-
-// Horizontal direction have been renamed to use start and end in lieu of left and right
-class MarginAndPaddingClassesUpdate extends CssClassesUpdate {
- properties = ['m', 'p'];
- sides = ['l', 'r'];
-
- searchValue = oneOf(this.properties) + oneOf(this.sides) + optional('-' + oneOf(breakpoints)) + '-' + oneOf(sizes);
-
- replaceValue = (property: string, side: string, breakpoint: string | undefined, size: string) => {
- return property + update(side) + (breakpoint ? '-' + breakpoint : '') + '-' + size;
- };
-
-}
-
-class TextAlignmentAndFloatClassesUpdate extends CssClassesUpdate {
- properties = ['text', 'float'];
- sides = ['left', 'right'];
-
- searchValue = oneOf(this.properties) + optional('-' + oneOf(breakpoints)) + '-' + oneOf(this.sides);
-
- replaceValue = (property: string, breakpoint: string, side: string) => {
- return property + (breakpoint ? '-' + breakpoint : '') + '-' + update(side);
- };
-}
-
-function update(side: string): string | undefined {
- return new Map([['l', 's'], ['r', 'e'], ['left', 'start'], ['right', 'end']]).get(side);
-}
diff --git a/packages/styles/schematics/migrations/secondary-classes/index.ts b/packages/styles/schematics/migrations/secondary-classes/index.ts
deleted file mode 100644
index 1a7a28667f..0000000000
--- a/packages/styles/schematics/migrations/secondary-classes/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Rule } from '@angular-devkit/schematics';
-import { CssClassesUpdate } from '../../utils/css/css-classes-update';
-import { CssMigration } from '../../utils/css/css-migration';
-import { oneOf } from '../../utils/regex';
-
-/** Entry point for the backgrounds' migration. */
-export default function (): Rule {
- return new CssMigration(new SecondaryClassesUpdate).rule;
-}
-
-// The .text-auto class no longer exists
-class SecondaryClassesUpdate extends CssClassesUpdate {
- searchValue = oneOf(['bg-secondary', 'border-secondary', 'text-secondary']);
- replaceValue = '';
-}
diff --git a/packages/styles/schematics/utils/css/constants.ts b/packages/styles/schematics/utils/constants.ts
similarity index 93%
rename from packages/styles/schematics/utils/css/constants.ts
rename to packages/styles/schematics/utils/constants.ts
index b007b4e2d2..fcaa55817e 100644
--- a/packages/styles/schematics/utils/css/constants.ts
+++ b/packages/styles/schematics/utils/constants.ts
@@ -2,6 +2,7 @@ export const breakpoints = ['xs', 'sm', 'rg', 'md', 'lg', 'xl', 'xxl'];
const bootstrapSizes = ['0', '1', '2', '3', '4', '5', 'auto'];
const postSizes = ['hair', 'line', 'micro', 'mini', 'small-regular', 'regular', 'small-large', 'large', 'big', 'bigger-big', 'small-huge', 'huge', 'small-giant', 'giant', 'bigger-giant'];
+
export const sizes = [...bootstrapSizes, ...postSizes];
-export const themeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'active', 'white', 'light', 'dark', 'nightblue', 'nightblue-bright', 'petrol', 'petrol-bright', 'coral', 'coral-bright', 'olive', 'olive-bright', 'purple', 'purple-bright', 'aubergine', 'aubergine-bright',];
+export const themeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'active', 'white', 'light', 'dark', 'nightblue', 'nightblue-bright', 'petrol', 'petrol-bright', 'coral', 'coral-bright', 'olive', 'olive-bright', 'purple', 'purple-bright', 'aubergine', 'aubergine-bright'];
diff --git a/packages/styles/schematics/utils/dom/migration-rule.ts b/packages/styles/schematics/utils/dom/migration-rule.ts
index c455bf4cd6..e375869197 100644
--- a/packages/styles/schematics/utils/dom/migration-rule.ts
+++ b/packages/styles/schematics/utils/dom/migration-rule.ts
@@ -5,8 +5,41 @@ import { getProjectTsConfigPaths } from "@angular/core/schematics/utils/project_
import { canMigrateFile, createMigrationProgram } from "@angular/core/schematics/utils/typescript/compiler_host";
import { NgComponentTemplateVisitor } from "@angular/core/schematics/utils/ng_component_template";
import { SourceFile } from "typescript";
-import * as cheerio from 'cheerio';
import DomMigration from './migration';
+import * as cheerio from 'cheerio/lib/slim';
+import * as prettier from 'prettier';
+import * as htmlParser from 'prettier/parser-html';
+
+// cheerio/lib/slim export uses htmlparser2 to parse the html
+// this is why we can use htmlparser2 options here (instead of parse5 options)
+const CHEERIO_OPTIONS: cheerio.CheerioOptions = {
+ decodeEntities: false,
+ lowerCaseTags: false,
+ lowerCaseAttributeNames: false,
+ recognizeSelfClosing: true,
+ withStartIndices: true,
+ withEndIndices: true
+};
+
+const PRETTIER_OPTIONS: prettier.Options = {
+ parser: 'html',
+ plugins: [htmlParser],
+ printWidth: 10000,
+ tabWidth: 2,
+ useTabs: false,
+ semi: true,
+ singleQuote: false,
+ quoteProps: 'consistent',
+ jsxSingleQuote: false,
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ bracketSameLine: false,
+ arrowParens: "always",
+ htmlWhitespaceSensitivity: 'css',
+ endOfLine: 'lf',
+ embeddedLanguageFormatting: 'off',
+ singleAttributePerLine: true
+};
export default function DomMigrationRule (migration: DomMigration): Rule {
return async (tree: Tree, _context: SchematicContext) => {
@@ -31,47 +64,49 @@ export default function DomMigrationRule (migration: DomMigration): Rule {
if (!sourceCode) continue;
for (const [updateKey, { update, selector }] of Object.entries(migration.updates)) {
+ // get update class context
const context = migration.updates[Number(updateKey)];
// create cheerio dom tree
- const $ = cheerio.load(sourceCode, {
- xmlMode: true,
- decodeEntities: false,
- lowerCaseTags: false,
- lowerCaseAttributeNames: false,
- recognizeSelfClosing: true,
- withStartIndices: true,
- withEndIndices: true
- }, false);
+ const $ = cheerio.load(sourceCode, CHEERIO_OPTIONS, false);
- // get "cheerioElement[]" to mutate in "update" method
- const $elements = $(selector);
+ // get "cheerioElement[]" to mutate in "update" method and add cheerio-identifier to each of em
+ const $inputElements = $(selector).each((i, element) => { $(element).data('cheerio-id', i); });
// continue to next migration.update if no elements were found
- if ($elements.length <= 0) continue;
-
- // create "cheerioElement[]" out of cheerio elements
- const elementsArray = Array.from($elements);
- // map source elements as "string[]" for later comparison
- const sourceElements = elementsArray.map(element => $(element).toString());
- // start tree file recorder to update tree file later
- const treeUpdateRecorder = tree.beginUpdate(treeFilePath);
+ if ($inputElements.length <= 0) continue;
+ // map elements to sourceElements for later comparison
+ const sourceElements = Array.from($inputElements)
+ .map((element, index) => ({
+ id: index,
+ element: $(element).toString(),
+ start: element.startIndex ?? null,
+ end: element.endIndex ? element.endIndex + 1 : null
+ }));
+
// send "cheerioElement[]" and cheerio instance to the "update" method
// after this "cheerioElement[]" in "$elements" are updated
- update.bind(context)($elements, $);
+ update.bind(context)($inputElements, $);
+
+ // get updated elements from dom tree
+ const $outputElements = $('*').filter((_i, element) => $(element).data('cheerio-id') !== undefined);
+
+ // start tree file recorder to update tree file later
+ const treeUpdateRecorder = tree.beginUpdate(treeFilePath);
- elementsArray
- .forEach((element, index) => {
- const $element = $(element);
+ sourceElements
+ .forEach(source => {
+ // get corresponding outputelement by cheerio-id
+ const distElement = $outputElements.filter((_i, element) => $(element).data('cheerio-id') === source.id).first().toString();
// continue to next "element", if eighter "element" has not been updated or "element" has no indices
- if (sourceElements[index] === $element.toString() || element.startIndex === null || element.endIndex === null) return;
+ if (source.element === distElement || source.start === null || source.end === null) return;
// remove old "element" out of tree file
- treeUpdateRecorder.remove(element.startIndex, element.endIndex- element.startIndex + 1);
+ treeUpdateRecorder.remove(source.start, source.end - source.start);
// write new "element" into the tree file
- treeUpdateRecorder.insertLeft(element.startIndex, $element.toString());
+ treeUpdateRecorder.insertLeft(source.start, prettier.format(distElement, PRETTIER_OPTIONS).replace(/(\n|\r\n)$/, ''));
});
// commit changes in tree file to tree
@@ -82,4 +117,4 @@ export default function DomMigrationRule (migration: DomMigration): Rule {
}
}
};
-}
\ No newline at end of file
+}
diff --git a/packages/styles/src/components/button-overview.stories.mdx b/packages/styles/src/components/button-overview.stories.mdx
index 6afe29a7d8..9f7da59d35 100644
--- a/packages/styles/src/components/button-overview.stories.mdx
+++ b/packages/styles/src/components/button-overview.stories.mdx
@@ -3,20 +3,27 @@ import './icons.scss';
-
# Buttons
+
#### Using Bootstrap v5.0
+
[Bootstrap Documentation](https://getbootstrap.com/docs/5.0/components/buttons/)
-
-
+
+
+
### Default buttons
Use these buttons in most situations. If you don't want the animation, you can leave out the .btn-animated class.
+
- Primary
- Secondary
+
+ Primary
+
+
+ Secondary
+
Tertiary
@@ -40,14 +47,16 @@ Try to avoid disabled buttons by displaying an error message for invalid forms o
Secondary
- Tertiary
+
+ Tertiary
+
-
### Icon buttons with text
Icon buttons with text should not have the class .btn-icon, otherwise the horizontal padding is not correct.
+
-
### Inverted buttons
Inverted buttons don't need special classes anymore, just use any of the background classes to set the background and you're done for the day.
+