diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 644bc2e3..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -helix-importer-ui \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json index 0d1a584f..e1a82932 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -2,6 +2,7 @@ "extends": ["stylelint-config-standard"], "rules": { "no-descending-specificity": null, + "custom-property-pattern": null, "selector-class-pattern": [ "^[a-z]([-]?[a-z0-9]+)*(__[a-z0-9]([-]?[a-z0-9]+)*)?(--[a-z0-9]([-]?[a-z0-9]+)*)?$", { @@ -9,4 +10,4 @@ } ] } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 903379e0..cea91b96 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Henkel RAQN Guide + PoC / Migration project to experiment with EDS in the RAQN world. ## Environments -- Preview: https://main--henkel-raqn-guide--hlxsites.hlx.page/ -- Live: https://main--henkel-raqn-guide--hlxsites.hlx.live/ + +- Preview: https://main--raqn-docs-sharepoint--henkel.aem.page/ +- Live: https://main--raqn-docs-sharepoint--henkel.aem.live/ ## Installation @@ -26,4 +28,4 @@ npm run lint:fix ## Documentation -[Documentation](docs/readme.md) \ No newline at end of file +[Documentation](docs/readme.md) diff --git a/blocks/accordion/accordion.css b/blocks/accordion/accordion.css index 9774cf74..84a24023 100644 --- a/blocks/accordion/accordion.css +++ b/blocks/accordion/accordion.css @@ -1,12 +1,12 @@ raqn-accordion { - --scope-icon-size: 1em; - --accordion-background: var(--scope-background, black); - --accordion-color: var(--scope-color, white); + --icon-size: 1em; + --accordion-background: var(--background, black); + --accordion-color: var(--title, white); background: var(--accordion-background); color: var(--accordion-color); - margin: var(--scope-margin, 0); - padding: var(--scope-padding, 0); + margin: var(--margin, 0); + padding: var(--padding, 0); display: grid; } @@ -21,9 +21,9 @@ raqn-accordion accordion-control.active raqn-icon { } .accordion-control { - border-block-start: var(--scope-border-block-start, none); - border-inline-start: var(--scope-border-inline-start, none); - border-inline-end: var(--scope-border-inline-end, none); + border-block-start: var(--border-block-start, none); + border-inline-start: var(--border-inline-start, none); + border-inline-end: var(--border-inline-end, none); cursor: pointer; display: flex; align-items: center; @@ -36,8 +36,8 @@ raqn-accordion accordion-control.active raqn-icon { } .accordion-control > * { - --scope-headings-color: var(--scope-color, black); - --scope-hover-color: var(--scope-accent-color, gray); + --headings-color: var(--title, black); + --hover-background-color: var(--accent-background, gray); width: 100%; display: flex; @@ -46,7 +46,7 @@ raqn-accordion accordion-control.active raqn-icon { } .accordion-control:hover { - --scope-color: var(--scope-headings-color); + color: var(--headings-color); } .accordion-content { @@ -54,8 +54,8 @@ raqn-accordion accordion-control.active raqn-icon { max-height: 0; overflow: hidden; opacity: 0; - border-block-end: var(--scope-border-block-end, none); - border-block-start: var(--scope-border-block-start, none); + border-block-end: var(--border-block-end, none); + border-block-start: var(--border-block-start, none); margin-block-end: -1px; transition: max-height 0.5s ease-in-out, diff --git a/blocks/breadcrumbs/breadcrumbs.css b/blocks/breadcrumbs/breadcrumbs.css index f53ea6f9..be04d905 100644 --- a/blocks/breadcrumbs/breadcrumbs.css +++ b/blocks/breadcrumbs/breadcrumbs.css @@ -4,8 +4,8 @@ raqn-breadcrumbs { gap: 10px; align-items: center; padding: 10px 0; - background: var(--scope-background, transparent); - color: var(--scope-color, #000); + background: var(--background, transparent); + color: var(--color, #000); } raqn-breadcrumbs ul { @@ -21,7 +21,7 @@ raqn-breadcrumbs ul li { } raqn-breadcrumbs ul li a { - color: var(--scope-color); + color: var(--text); font-weight: normal; } diff --git a/blocks/button/button.css b/blocks/button/button.css index 73c998ee..e5111379 100644 --- a/blocks/button/button.css +++ b/blocks/button/button.css @@ -5,30 +5,38 @@ raqn-button { display: grid; align-content: center; align-items: center; - justify-items: var(--scope-justify, start); + justify-items: var(--justify, start); + + --border-radius: 0; + --border-block-start: 1px solid transparent; + --border-block-end: 1px solid transparent; + --border-inline-start: 1px solid transparent; + --border-inline-end: 1px solid transparent; } raqn-button :where(a, button) { display: inline-flex; - line-height: var(--scope-icon-size, 1em); - background: var(--scope-accent-background, #000); - color: var(--scope-accent-color, #fff); + line-height: var(--icon-size, 1em); + background: var(--accent-background, #000); + color: var(--accent-text, #fff); text-transform: none; - border-radius: var(--scope-border-radius, 0); - border-block-start: var(--scope-border-block-start, 1px solid transparent); - border-block-end: var(--scope-border-block-end, 1px solid transparent); - border-inline-start: var(--scope-border-inline-start, 1px solid transparent); - border-inline-end: var(--scope-border-inline-end, 1px solid transparent); - padding: 10px 20px; + border-radius: var(--border-radius, 0); + border-block-start: var(--border-block-start, 1px solid transparent); + border-block-end: var(--border-block-end, 1px solid transparent); + border-inline-start: var(--border-inline-start, 1px solid transparent); + border-inline-end: var(--border-inline-end, 1px solid transparent); + border-color: var(--accent-border, none); + padding-block: var(--button-padding-block, 10px); + padding-inline: var(--button-padding-inline, 20px); overflow: hidden; text-decoration: none; text-align: start; } raqn-button :where(a, button):hover { - background: var(--scope-accent-background-hover, #fff); - color: var(--scope-accent-color-hover, #fff); - border-color: currentcolor; + background: var(--hover-background, #fff); + color: var(--hover-text, #fff); + border-color: var(--hover-border, #fff); cursor: pointer; } diff --git a/blocks/button/button.editor.js b/blocks/button/button.editor.js new file mode 100644 index 00000000..1bb63567 --- /dev/null +++ b/blocks/button/button.editor.js @@ -0,0 +1,127 @@ +export default function config() { + return { + variables: { + '--accent-background': { + type: 'text', + label: 'Background', + helpText: 'The background color of the button.', + }, + '--accent-color': { + type: 'text', + label: 'Color', + helpText: 'The text color of the button.', + }, + '--button-padding-block': { + type: 'text', + label: 'Padding Block', + helpText: 'The padding block of the button.', + }, + '--button-padding-inline': { + type: 'text', + label: 'Padding Inline', + helpText: 'The padding inline of the button.', + }, + '--border-block-end': { + type: 'text', + label: 'Border Block End', + helpText: 'The border block end of the button.', + }, + '--border-radius': { + type: 'text', + label: 'Border Radius', + helpText: 'The border radius of the button.', + }, + '--border-block-start': { + type: 'text', + label: 'Border Block Start', + helpText: 'The border block start of the button.', + }, + '--border-inline-end': { + type: 'text', + label: 'Border Inline End', + helpText: 'The border inline end of the button.', + }, + '--border-inline-start': { + type: 'text', + label: 'Border Inline Start', + helpText: 'The border inline start of the button.', + }, + '--box-shadow': { + type: 'text', + label: 'Box Shadow', + helpText: 'The box shadow of the button.', + }, + '--accent-background-hover': { + type: 'text', + label: 'Background Hover', + helpText: 'The background color of the button when hovered.', + }, + '--accent-color-hover': { + type: 'text', + label: 'Color Hover', + helpText: 'The text color of the button when hovered.', + }, + '--justify': { + type: 'text', + label: 'Justify', + helpText: 'The justify of the button.', + }, + }, + selection: { + Blue: { + descritpion: { + label: 'Regular Blue Button', + preview: 'http://localhost:8888/@henkel/theme-interface/assets/previews/button/blue.png', + }, + variables: { + '--accent-background': '#007bff', + '--accent-color': '#fff', + '--border-block-end': '0', + '--border-block-start': '0', + '--border-inline-end': '0', + '--border-inline-start': '0', + '--box-shadow': 'none', + '--accent-background-hover': '#0056b3', + '--accent-color-hover': '#fff', + '--justify': 'start', + }, + }, + Red: { + descritpion: { + label: 'Regular Red Button', + preview: 'http://localhost:8888/@henkel/theme-interface/assets/previews/button/red.png', + }, + variables: { + '--accent-background': 'red', + '--accent-color': 'white', + '--border-block-end': '1px', + '--border-block-start': '1px', + '--border-inline-end': '1px', + '--border-inline-start': '1px', + '--box-shadow': '1px 1px 1px 1px rgba(0, 0, 0, 0.1)', + '--accent-background-hover': 'white', + '--accent-color-hover': 'red', + '--justify': 'start', + }, + }, + White: { + descritpion: { + label: 'Regular white Button', + preview: 'http://localhost:8888/@henkel/theme-interface/assets/previews/button/white.png', + }, + variables: { + '--accent-background': 'white', + '--accent-color': 'black', + '--border-block-end': '10px', + '--border-block-start': '10px', + '--border-inline-end': '1px', + '--border-inline-start': '1px', + '--box-shadow': '1px 1px 1px 1px rgba(0, 0, 0, 0.1)', + '--accent-background-hover': 'white', + '--accent-color-hover': 'red', + '--justify': 'start', + }, + }, + }, + }; +} diff --git a/blocks/card/card.css b/blocks/card/card.css index 3d61bfeb..edf3a13d 100644 --- a/blocks/card/card.css +++ b/blocks/card/card.css @@ -1,23 +1,23 @@ raqn-card { - background: var(--scope-background, transparent); - color: var(--scope-color, #fff); + background: var(--background, transparent); + color: var(--text, #fff); display: grid; position: relative; grid-template-columns: var(--card-columns, 1fr); - gap: var(--scope-gap, 20px); - padding: var(--scope-padding, 20px 0); + gap: var(--gap, 20px); + padding: var(--padding, 20px 0); } raqn-card > div { display: flex; - gap: var(--scope-gap, 20px); + gap: var(--gap, 20px); position: relative; - background: var(--scope-inner-background, transparent); - padding: var(--scope-inner-padding, 20px); - border-block-start: var(--scope-border-block-start, none); - border-block-end: var(--scope-border-block-end, none); - border-inline-start: var(--scope-border-inline-start, none); - border-inline-end: var(--scope-border-inline-end, none); + background: var(--inner-background, transparent); + padding: var(--inner-padding, 20px); + border-block-start: var(--border-block-start, none); + border-block-end: var(--border-block-end, none); + border-inline-start: var(--border-inline-start, none); + border-inline-end: var(--border-inline-end, none); } raqn-card :where(a, button) { @@ -41,7 +41,6 @@ raqn-card div > div:first-child > p:has(> em:only-child > a:only-child) { margin: 0; } - raqn-card div > div { display: flex; flex-direction: column; diff --git a/blocks/card/card.editor.js b/blocks/card/card.editor.js new file mode 100644 index 00000000..562d46f4 --- /dev/null +++ b/blocks/card/card.editor.js @@ -0,0 +1,61 @@ +export default function config() { + return { + sets: { + '--background': { + type: 'text', + label: 'Background', + helpText: 'The background color of the card.', + }, + '--color': { + type: 'text', + label: 'Color', + helpText: 'The text color of the card.', + }, + '--gap': { + type: 'text', + label: 'Gap', + helpText: 'The gap between cards.', + }, + '--padding': { + type: 'text', + label: 'Padding', + helpText: 'The padding of the card.', + }, + }, + attributes: { + 'data-columns': { + type: 'text', + label: 'Number of Columns', + helpText: 'The number of columns in the card grid.', + }, + 'data-eager': { + type: 'text', + label: 'Eager Loading', + helpText: 'The number of images to load eagerly.', + }, + }, + selection: { + variant1: { + attributes: { + 'data-columns': '2', + 'data-ratio': '4/3', + 'data-eager': '0', + }, + }, + variant2: { + attributes: { + 'data-columns': '3', + 'data-ratio': '4/3', + 'data-eager': '0', + }, + }, + variant3: { + attributes: { + 'data-columns': '4', + 'data-ratio': '4/3', + 'data-eager': '0', + }, + }, + }, + }; +} diff --git a/blocks/card/card.js b/blocks/card/card.js index c5728ee3..43f90108 100644 --- a/blocks/card/card.js +++ b/blocks/card/card.js @@ -4,6 +4,27 @@ import { eagerImage } from '../../scripts/libs.js'; export default class Card extends ComponentBase { static observedAttributes = ['data-columns', 'data-ratio', 'data-eager']; + // Default values for the attributes except for class + attributesValues = { + all: { + data: { + columns: '4', + ratio: '4/3', + eager: '0', + }, + }, + xs: { + data: { + columns: '1', + }, + }, + s: { + data: { + columns: '1', + }, + }, + }; + ready() { this.eager = parseInt(this.dataset.eager || 0, 10); this.classList.add('inner'); diff --git a/blocks/column/column.css b/blocks/column/column.css index 002ec5ac..f24646d4 100644 --- a/blocks/column/column.css +++ b/blocks/column/column.css @@ -1,5 +1,5 @@ raqn-column { - margin: var(--scope-margin, 0); + margin: var(--margin, 0); width: 100%; display: grid; } diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 7aa0fb17..9be87f1b 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,12 +1,12 @@ footer { - background: var(--scope-background-color); - width: var(--scope-max-width); + background: var(--background-color); + width: var(--max-width); margin: 0 auto; } raqn-footer { - background: var(--scope-background-color); - border-top: 1px solid var(--scope-color); + background: var(--background-color); + border-top: 1px solid var(--text); } raqn-footer ul { @@ -17,7 +17,7 @@ raqn-footer ul { } raqn-footer ul li a { - color: var(--scope-color); + color: var(--text); } @media screen and (min-width: 1024px) { @@ -28,7 +28,7 @@ raqn-footer ul li a { raqn-footer ul li a { padding: 10px 1.2em; - border-inline-end: 1px solid var(--scope-color); + border-inline-end: 1px solid var(--text); } raqn-footer ul { diff --git a/blocks/footer/footer.editor.js b/blocks/footer/footer.editor.js new file mode 100644 index 00000000..4010848f --- /dev/null +++ b/blocks/footer/footer.editor.js @@ -0,0 +1,25 @@ +export default function config() { + return { + variables: { + '--background-color': { + type: 'text', + label: 'Background Color', + scope: 'page', + helpText: 'The background color of the footer.', + }, + '--color': { + type: 'text', + label: 'Color', + scope: 'global', + helpText: 'The text color of the footer.', + }, + }, + attributes: { + class: { + type: 'text', + label: 'Class', + helpText: 'The class of the footer.', + }, + }, + }; +} diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index a5cbcf9f..ec0a61a1 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -25,6 +25,7 @@ export default class Footer extends ComponentBase { ready() { const child = this.children[0]; + if (!child) return; child.replaceWith(...child.children); this.nav = this.querySelector('ul'); this.nav.setAttribute('role', 'navigation'); diff --git a/blocks/header/header.css b/blocks/header/header.css index 0f9948f8..c8806448 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,14 +1,14 @@ raqn-header { - --scope-background: var(--scope-header-background, #fff); - --scope-color: var(--scope-header-color, #000); - --scope-top: var(--scope-header-top, 0); + --header-background: var(--background, #fff); + --color: var(--text, #000); + --top: var(--header-top, 0); position: fixed; - top: var(--scope-top); + top: var(--top); width: 100%; - min-height: var(--scope-header-height, 64px); + min-height: var(--header-height, 110px); display: grid; - background: var(--scope-header-background, #fff); + background: var(--header-background, #fff); align-items: center; z-index: 100; } diff --git a/blocks/header/header.js b/blocks/header/header.js index 3f15faf1..24565244 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -12,6 +12,14 @@ export default class Header extends ComponentBase { }, }; + attributesValues = { + all: { + class: { + color: 'primary', + }, + }, + }; + fragmentPath = metaFragment || `${fallbackContent}.plain.html`; dependencies = ['navigation', 'image']; diff --git a/blocks/hero/hero.css b/blocks/hero/hero.css index 822306c0..987e0d33 100644 --- a/blocks/hero/hero.css +++ b/blocks/hero/hero.css @@ -1,14 +1,15 @@ /* block specific CSS goes here */ raqn-hero { - --hero-background: var(--scope-background, black); - --hero-color: var(--scope-color, white); - --hero-grid-template-columns: var(--scope-hero-columns, 1fr); + --hero-background: var(--background, black); + --hero-color: var(--text, white); + --hero-grid-template-columns: var(--hero-columns, 1fr); --hero-hero-order: 0; - --hero-padding-block: var(--scope-hero-padding-block, 40px); + --hero-padding-block: var(--hero-padding-block, 40px); background: var(--hero-background); color: var(--hero-color); + display: grid; align-items: center; grid-template-columns: var(--hero-grid-template-columns, 1fr); padding-block: var(--hero-padding-block); diff --git a/blocks/hero/hero.editor.js b/blocks/hero/hero.editor.js new file mode 100644 index 00000000..7b596f46 --- /dev/null +++ b/blocks/hero/hero.editor.js @@ -0,0 +1,29 @@ +export default function config() { + return { + attributes: { + data: { + order: { + type: 'select', + options: [ + { + label: 'Image on the right', + value: '0', + }, + { + label: 'Image on the left', + value: '1', + }, + ], + label: 'Order', + helpText: 'Order of the columns.', + }, + width: { + type: 'text', + label: 'Width', + value: '100%', + helpText: 'Width of the hero.', + }, + }, + }, + }; +} diff --git a/blocks/hero/hero.js b/blocks/hero/hero.js index 809615ac..3398759f 100644 --- a/blocks/hero/hero.js +++ b/blocks/hero/hero.js @@ -3,12 +3,26 @@ import ComponentBase from '../../scripts/component-base.js'; export default class Hero extends ComponentBase { static observedAttributes = ['data-order']; + dependencies = ['icon', 'button', 'image']; + + // Default values for the attributes + attributesValues = { + all: { + class: { + full: 'width', + }, + attribute: { + role: 'banner', + 'aria-label': 'hero', + }, + }, + }; + ready() { - const child = this.children[0]; + const child = this.querySelector(':has( div + div)'); + + if (!child) return; child.replaceWith(...child.children); - this.classList.add('full-width'); - this.setAttribute('role', 'banner'); - this.setAttribute('aria-label', 'hero'); } onAttributeOrderChanged({ newValue }) { diff --git a/blocks/icon/icon.css b/blocks/icon/icon.css index b34e808b..72ea9a03 100644 --- a/blocks/icon/icon.css +++ b/blocks/icon/icon.css @@ -1,11 +1,11 @@ raqn-icon { display: inline-flex; - font-size: 1em; - line-height: 1em; text-align: center; - min-width: var(--scope-icon-size, 1em); - min-height: var(--scope-icon-size, 1em); - justify-content: var(--scope-icon-align, start); + font-size: 1.2em; + line-height: 1.2em; + width: var(--icon-size, 1.2em); + height: var(--icon-size, 1.2em); + justify-content: var(--icon-align, start); text-transform: none; vertical-align: middle; -webkit-font-smoothing: antialiased; @@ -14,8 +14,8 @@ raqn-icon { raqn-icon svg { display: inline-block; - width: var(--scope-icon-size, 1em); - height: var(--scope-icon-size, 1em); + width: var(--icon-size, 1.2em); + height: var(--icon-size, 1.2em); fill: currentcolor; overflow: hidden; vertical-align: middle; diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js index 0c07e7f9..920c096a 100644 --- a/blocks/icon/icon.js +++ b/blocks/icon/icon.js @@ -1,5 +1,5 @@ import ComponentBase from '../../scripts/component-base.js'; -import { stringToJsVal } from '../../scripts/libs.js'; +import { flatAsValue, isObject, stringToJsVal } from '../../scripts/libs.js'; export default class Icon extends ComponentBase { static observedAttributes = ['data-active', 'data-icon']; @@ -53,12 +53,17 @@ export default class Icon extends ComponentBase { this.setAttribute('aria-hidden', 'true'); } + // ${viewport}-icon-${value} or icon-${value} + applyIcon(icon) { + this.dataset.icon = isObject(icon) ? flatAsValue(icon) : icon; + } + // Same icon component can be reused with any other icons just by changing the attribute async onAttributeIconChanged({ oldValue, newValue }) { if (oldValue === newValue) return; // ! The initial and active icon names are separated with a double underline - // ! The active icon is optional; + // ! The active icon is optional; const [initial, active] = newValue.split('__'); this.#initialIcon = initial; this.#activeIcon = active || null; diff --git a/blocks/navigation/navigation.css b/blocks/navigation/navigation.css index 6c700922..89ad6984 100644 --- a/blocks/navigation/navigation.css +++ b/blocks/navigation/navigation.css @@ -1,11 +1,11 @@ /* stylelint-disable CssSyntaxError */ raqn-navigation { - --raqn-navigation-background: var(--scope-background, #fff); - --raqn-navigation-color: var(--scope-color, #000); + --raqn-navigation-background: var(--background, #fff); + --raqn-navigation-color: var(--text, #000); --raqn-navigation-level-1: var(--raqn-font-size-4, 1.25rem); --raqn-navigation-level-2: var(--raqn-font-size-5, 1rem); - margin: var(--scope-margin); + margin: var(--margin); width: 100%; display: grid; justify-content: center; @@ -18,12 +18,16 @@ raqn-navigation > nav p { } raqn-navigation .level-1 a:not(:hover) { - color: var(--scope-accent-background, #000); + color: var(--accent-background, #000); +} + +raqn-navigation .level-1 a:hover { + color: var(--highlight, #000); } raqn-navigation > nav > ul { overflow-y: auto; - max-height: calc(100vh - var(--scope-header-height)); + max-height: calc(100vh - var(--header-height)); } raqn-navigation.active > nav ul, @@ -54,8 +58,8 @@ raqn-navigation > button { justify-self: end; align-items: center; justify-content: center; - background: var(--scope-background, #fff); - color: var(--scope-color, #000); + background: var(--accent-background, #fff); + color: var(--accent-text, #000); border: none; border-radius: var(--border-radius); padding: var(--padding-vertical, 10px) var(--padding-horizontal, 10px); @@ -63,8 +67,8 @@ raqn-navigation > button { } raqn-navigation.active button { - background: var(--scope-background-hover, #000); - color: var(--scope-color-hover, #fff); + background: var(--hover-background, #000); + color: var(--hover-text, #fff); } raqn-navigation.active > nav > ul { @@ -72,18 +76,18 @@ raqn-navigation.active > nav > ul { display: block; list-style: none; max-width: 0; - background: var(--scope-background, #fff); + background: var(--background, #fff); min-width: 100%; inset-inline-start: 0; - inset-block-start: var(--scope-header-height, 64px); + inset-block-start: var(--header-height, 64px); height: 100%; - max-height: calc(100vh - var(--scope-header-height, 64px)); + max-height: calc(100vh - var(--header-height, 64px)); margin: 0 auto; padding: 0; } raqn-navigation.active > nav > ul li { - max-width: var(--scope-max-width, 100%); + max-width: var(--max-width, 100%); margin: 0 auto; } @@ -96,7 +100,7 @@ raqn-navigation .accordion-content-wrapper { } raqn-navigation:not([data-compact='true']) > nav a { - line-height: var(--scope-icon-size, 24px); + line-height: var(--icon-size, 24px); } raqn-navigation:not([data-compact='true']) > nav ul { @@ -105,8 +109,8 @@ raqn-navigation:not([data-compact='true']) > nav ul { } raqn-navigation:not([data-compact='true']) > nav > ul { - inset-inline-start: calc((100vw - var(--scope-max-width)) / 2); - inset-block-start: var(--scope-header-height, 64px); + inset-inline-start: calc((100vw - var(--max-width)) / 2); + inset-block-start: var(--header-height, 64px); } raqn-navigation:not([data-compact='true']) > nav > p { @@ -118,16 +122,16 @@ raqn-navigation:not([data-compact='true']) > nav [data-icon='chevron-right'] { } raqn-navigation:not([data-compact='true']) > nav .level-1 a { - padding: var(--scope-padding-vertical, 10px) var(--scope-padding-horizontal, 20px); + padding: var(--padding-vertical, 10px) var(--padding-horizontal, 20px); } raqn-navigation:not([data-compact='true']) > nav .level-2 > a { - color: var(--scope-link-color-hover); + color: var(--highlight, #000); font-size: 1.2em; } raqn-navigation:not([data-compact='true']) > nav .level-2 > a:hover { - color: var(--scope-color, #fff); + color: var(--highlight); } raqn-navigation:not([data-compact='true']) > nav .level-2, @@ -146,8 +150,8 @@ raqn-navigation:not([data-compact='true']) > nav .level-1 > ul { clip-path: inset(0% -100vw 100% -100vw); position: absolute; padding: 0; - inset-block-start: var(--scope-header-height, 64px); - inset-inline-start: calc((100vw - var(--scope-max-width)) / 2); + inset-block-start: var(--header-height, 64px); + inset-inline-start: calc((100vw - var(--max-width)) / 2); transition: clip-path 0.4s ease-in-out; overflow: visible; } @@ -161,13 +165,13 @@ raqn-navigation:not([data-compact='true']) > nav .level-1 > ul .level-2 { raqn-navigation:not([data-compact='true']) > nav .level-1 > ul::after { content: ' '; - margin-inline: calc(-1 * ((100vw - var(--scope-max-width)) / 2)); + margin-inline: calc(-1 * ((100vw - var(--max-width)) / 2)); position: absolute; height: 100%; width: 100vw; inset-inline-start: 0; - background: var(--scope-background, #fff); - border-block-start: 1px solid var(--scope-color, #000); + background: var(--background, #fff); + border-block-start: 1px solid var(--accent-background, #000); box-shadow: 0 0 30px #000; z-index: 1; } diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index 7d17a753..7085b1fe 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -1,12 +1,12 @@ import component from '../../scripts/init.js'; -import ComponentBase from '../../scripts/component-base.js'; -export default class Navigation extends ComponentBase { - static observedAttributes = ['data-icon', 'data-compact']; +import Column from '../column/column.js'; + +export default class Navigation extends Column { + static observedAttributes = ['data-icon', 'data-compact', ...Column.observedAttributes]; static loaderConfig = { - ...ComponentBase.loaderConfig, - targetsSelectors: ':scope > :is(:first-child)', + ...Column.loaderConfig, }; dependencies = ['icon']; diff --git a/blocks/router/router.css b/blocks/router/router.css index 1aca89f2..89061fe7 100644 --- a/blocks/router/router.css +++ b/blocks/router/router.css @@ -1,3 +1,3 @@ raqn-router { - background-color: var(--scope-background, transparent); + background-color: var(--background, transparent); } diff --git a/blocks/theming/theming.editor.js b/blocks/theming/theming.editor.js new file mode 100644 index 00000000..b0104c86 --- /dev/null +++ b/blocks/theming/theming.editor.js @@ -0,0 +1,38 @@ +import { MessagesEvents } from '../../scripts/editor.js'; +import { readValue } from '../../scripts/libs.js'; +import { publish } from '../../scripts/pubsub.js'; +import Theming from './theming.js'; + +let listener = false; +let themeInstance = null; + +export default function config() { + // init editor if message from parent + if (!listener) { + [themeInstance] = window.raqnInstances[Theming.name.toLowerCase()]; + + publish( + MessagesEvents.theme, + { name: 'theme', data: themeInstance.themeJson }, + { usePostMessage: true, targetOrigin: '*' }, + ); + + listener = true; + window.addEventListener('message', (e) => { + if (e && e.data) { + const { message, params } = e.data; + if (message && message === MessagesEvents.themeUpdate) { + [themeInstance] = window.raqnInstances[Theming.name.toLowerCase()]; + const { data } = params; + const row = Object.keys(data).map((key) => data[key]); + readValue(row, themeInstance.variations); + themeInstance.defineVariations(readValue(row, themeInstance.variations)); + themeInstance.styles(); + } + } + }); + } + return { + variables: {}, + }; +} diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index c01e2155..d30e7604 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -1,63 +1,47 @@ import ComponentBase from '../../scripts/component-base.js'; -import { globalConfig, metaTags, getMeta } from '../../scripts/libs.js'; +import { flat, getBreakPoints, getMediaQuery, getMeta, metaTags, readValue, unflat } from '../../scripts/libs.js'; -const { theming, theme } = metaTags; -const metaTheming = getMeta(theming.metaName); -const metaFragment = metaTheming && `${metaTheming}.json`; const k = Object.keys; export default class Theming extends ComponentBase { - nestedComponentsConfig = {}; + componentsConfig = {}; + + elements = {}; + + variations = {}; setDefaults() { super.setDefaults(); this.scapeDiv = document.createElement('div'); - // keep as it is - this.fragmentPath = metaFragment || theming.fallbackContent; - this.skip = ['tags']; - this.toTags = ['font-size', 'font-weight', 'font-family', 'line-height', 'font-style', 'font-margin-block']; - this.transform = { 'font-margin-block': 'margin-block' }; + this.themeJson = {}; + + this.globalsVar = ['c-', 'global']; + this.toTags = []; + this.transform = {}; this.tags = ''; this.fontFace = ''; this.atomic = ''; } - fontFaceTemplate(fontFace) { - if (fontFace.indexOf('-') > -1) { - const [name, ...rest] = fontFace.split('-'); - const params = rest.pop().split('.'); - const format = params.pop(); - const lastBit = params.pop(); - const fontWeight = globalConfig.fontWeights[lastBit] || 'regular'; - const fontStyle = lastBit === 'italic' ? lastBit : 'normal'; - // eslint-disable-next-line max-len - return `@font-face {font-family: ${name};font-weight: ${fontWeight};font-display: swap;font-style: ${fontStyle};src: url('/fonts/${fontFace}') format(${format});}`; - } - return ''; - } + fontFaceTemplate(data) { + const names = Object.keys(data); - fontTags(t, index) { - const tag = t.tags[index]; - const values = this.toTags.reduce((acc, key) => { - if (t[key][index]) { - if (acc[tag]) { - acc[tag][key] = t[key][index]; - } else { - acc[tag] = { [key]: t[key][index] }; - } - } - return acc; - }, {}); - return k(values).map((value) => { - const val = values[value]; - return `${tag} {${k(val) - .map((v) => `${this.getKey(v)}: var(--scope-${this.getKey(v)}, ${val[v]});`) - .join('')}}`; - }); - } - - getKey(key) { - return this.transform[key] ? this.transform[key] : key; + this.fontFace = names + .map((key) => { + // files + const types = Object.keys(data[key].options); + return types + .map( + (type) => `@font-face { + font-family: '${key}'; + src: url('${window.location.origin}/fonts/${data[key].options[type]}'); + ${type === 'italic' ? 'font-style' : 'font-weight'}: ${type}; + } + `, + ) + .join(''); + }) + .join(''); } escapeHtml(unsafe) { @@ -65,83 +49,124 @@ export default class Theming extends ComponentBase { return this.scapeDiv.innerHTML; } - renderVariables(key, row, t) { - const value = t[key][row]; - let variable = ''; - if (value) { - if (key === 'font-face') { - this.fontFace += this.fontFaceTemplate(value); - } else { - variable = `\n--raqn-${this.getKey(key)}-${row}: ${this.escapeHtml(value).trim()};`; - this.atomic += `body .${this.getKey(key)}-${row} {--scope-${this.getKey(key)}: var(--raqn-${this.getKey( - key, - )}-${row});}\n`; - } - } - return variable; - } - - readValue() { - const { data } = this.themeJson; - const keys = data.map((item) => item.key); - const t = data.reduce( - (ac, item, i) => - keys.reduce((acc, key) => { - delete item.key; - if (!this.themesKeys) { - this.themesKeys = k(item); - } - const ind = keys.indexOf(key); - if (i === ind) { - acc[key] = item; - } - return acc; - }, ac), - {}, - ); - // font tags - if (t.tags) { - this.tags = k(t.tags) - .map((index) => this.fontTags(t, index)) - .join('\n'); - } - // full scoped theme classes - this.themes = this.themesKeys - .map( - (themeItem) => `.theme-${themeItem} {${k(t) - .filter((key) => ![...this.skip, ...this.toTags].includes(key)) - .map((key) => (t[key][themeItem] ? `--scope-${key}: var(--raqn-${key}-${themeItem});` : '')) - .filter((v) => v !== '') - .join('')} - }`, - ) - .join(''); - - this.variables = `body{${k(t) - .filter((key) => ![...this.skip].includes(key)) - .map((key) => { - const rows = k(t[key]); - return rows.map((row) => this.renderVariables(key, row, t)).join(''); + reduceViewports(obj, callback) { + const breakpoints = Object.keys(obj); + return breakpoints + .map((bp) => { + const options = getBreakPoints(); + if (options.byName[bp]) { + const { min, max } = options.byName[bp]; + const query = getMediaQuery(min, max); + return ` +@media ${query} { + ${callback(obj[bp])} + } + `; + } + // regular + return callback(obj[bp]); }) - .join('')}}`; + .join('\n'); } styles() { - ['variables', 'tags', 'atomic', 'themes'].forEach((cssSegment) => { - const style = document.createElement('style'); + ['variables', 'tags', 'fontFace'].forEach((cssSegment) => { + const style = document.querySelector(`style.${cssSegment}`) || document.createElement('style'); style.innerHTML = this[cssSegment]; style.classList.add(cssSegment); document.head.appendChild(style); }); - const themeMeta = getMeta(theme.metaName); - document.body.classList.add(themeMeta || theme.fallbackContent); + const themeMeta = getMeta('theme'); + document.body.classList.add(themeMeta, 'color-default', 'font-default'); } - async processFragment(response) { + async processFragment(response, type = 'color') { if (response.ok) { - this.themeJson = await response.json(); - this.readValue(); - this.styles(); + const responseData = await response.json(); + this.themeJson[type] = responseData; + if (type === 'fontface') { + this.fontFaceTemplate(responseData); + } else if (type === 'component') { + Object.keys(responseData).forEach((key) => { + if (key.indexOf(':') === 0 || responseData[key].data.length === 0) return; + this.componentsConfig[key] = this.componentsConfig[key] || {}; + this.componentsConfig[key] = readValue(responseData[key].data, this.componentsConfig[key]); + }); + } else { + this.variations = readValue(responseData.data, this.variations); + this.defineVariations(); + } } } + + defineVariations() { + const names = k(this.variations); + const result = names.reduce((a, name) => { + const unflatted = unflat(this.variations[name]); + return ( + a + + this.reduceViewports(unflatted, (actionData) => { + const actions = k(actionData); + return actions.reduce((b, action) => { + const actionName = `render${action.charAt(0).toUpperCase()}${action.slice(1)}`; + if (this[actionName]) { + return b + this[actionName](actionData[action], name); + } + return b; + }, ''); + }) + ); + }, ''); + this.variables = result; + } + + renderColor(data, name) { + return this.variablesValues(data, name, '.color-'); + } + + variablesValues(data, name, prepend = '.') { + const f = flat(data); + return `${prepend || '.'}${name} { + ${k(f) + .map((key) => `\n--${key}: ${f[key]};`) + .join('')} + } + `; + } + + variablesScopes(data, name, prepend = '.') { + const f = flat(data); + return `${prepend}${name} { + ${k(f) + .map((key) => `\n${key}: var(--${name}-${key}, ${f[key]});`) + .join('')} + } + `; + } + + renderFont(data, name) { + const elements = k(data); + const flattened = flat(data); + this.tags = elements.reduce((a, key) => { + const props = flat(data[key]); + return a + this.variablesScopes(props, key, ''); + }, ''); + return this.variablesValues(flattened, name, '.font-'); + } + + async loadFragment() { + Promise.all( + ['color', 'font', 'layout', 'component'].map(async (fragment) => { + const metaKey = `theme${fragment}`; + + const path = getMeta(metaTags[metaKey].metaName) || metaTags[metaKey].fallbackContent; + return fetch(`${path}.json`).then((response) => this.processFragment(response, fragment)); + }), + ); + // + await fetch('color.json').then((response) => this.processFragment(response, 'color')); + await fetch('font.json').then((response) => this.processFragment(response, 'font')); + await fetch('/fonts/index.json').then((response) => this.processFragment(response, 'fontface')); + this.styles(); + } } diff --git a/docs/raqn/components.md b/docs/raqn/components.md index c3991092..b269e90e 100644 --- a/docs/raqn/components.md +++ b/docs/raqn/components.md @@ -83,8 +83,8 @@ With the component loader, it will be rendered as:

Get started

- Learn the basics: how to best get started and create a page. And how to - transfer your brand theme to the new capabilities of RAQN web. + Learn the basics: how to best get started and create a page. And how to transfer your brand theme to the new + capabilities of RAQN web.

@@ -141,8 +141,8 @@ Now, let's add a little style at `hero.css`: ```css /* Block-specific CSS goes here */ raqn-hero { - --hero-background-color: var(--scope-background, black); - --hero-color: var(--scope-color, white); + --hero-background-color: var(--background, black); + --hero-color: var(--text, white); --hero-grid-template-columns: 0.6fr 0.4fr; --hero-hero-order: 0; @@ -186,10 +186,10 @@ To set a param only to a specific viewport, prefix it with the viewport key: 1. **xs**: 0 to 479, 1. **s**: 480 to 767, -2. **m**: 768 to 1023, -3. **l**: 1024 to 1279, -4. **xl**: 1280 to 1919, -5. **xxl**: 1920. +1. **m**: 768 to 1023, +1. **l**: 1024 to 1279, +1. **xl**: 1280 to 1919, +1. **xxl**: 1920. Let's set the order param to apply only on the S (0 to 767) viewport: @@ -202,4 +202,4 @@ Now, the param is only set on S viewports: Where: 1. Regular params will be set to all viewports. -2. Prefixed params will be applied only to the specific viewport, overriding the general one. \ No newline at end of file +2. Prefixed params will be applied only to the specific viewport, overriding the general one. diff --git a/docs/raqn/theming.md b/docs/raqn/theming.md index 43b5744f..abf3e2fa 100644 --- a/docs/raqn/theming.md +++ b/docs/raqn/theming.md @@ -9,11 +9,11 @@ To enhance future developments, we aim to introduce theme capabilities within th ## CSS variables for theme - Leveraging EDS capabilities for delivering a spreadsheet as JSON, we'll employ a `theme.xls` as a theme storage. The following example illustrates the structure: +Leveraging EDS capabilities for delivering a spreadsheet as JSON, we'll employ a `theme.xls` as a theme storage. The following example illustrates the structure: ![Theme concept](../assets/theme-concept-excel.png) -- The first row defines the name of the theme, which can be expressed as strings (e.g., primary, secondary) or numbers for simplicity. *(Note: The A1 cell is illustrative, and its value is ignored.)* +- The first row defines the name of the theme, which can be expressed as strings (e.g., primary, secondary) or numbers for simplicity. _(Note: The A1 cell is illustrative, and its value is ignored.)_ - The first column outlines the property/variable names. For effective theme application, we require: @@ -31,44 +31,44 @@ ${property}-${columnName}: ${value}; ```css /* Global CSS variables */ body { - --raqn-color-1: red; - --raqn-color-2: blue; - --raqn-color-default: black; - --raqn-background-1: #eee; - --raqn-background-2: #ddd; - --raqn-background-default: #fff; + --raqn-color-1: red; + --raqn-color-2: blue; + --raqn-color-default: black; + --raqn-background-1: #eee; + --raqn-background-2: #ddd; + --raqn-background-default: #fff; } /* Atomic classes with specificity of 2 */ body .color-1 { - --scope-color: var(--raqn-color-1); + --color: var(--raqn-color-1); } body .color-2 { - --scope-color: var(--raqn-color-2); + --color: var(--raqn-color-2); } body .color-default { - --scope-color: var(--raqn-color-default); + --color: var(--raqn-color-default); } body .background-1 { - --scope-background: var(--raqn-background-1); + --background: var(--raqn-background-1); } body .background-2 { - --scope-background: var(--raqn-background-2); + --background: var(--raqn-background-2); } body .background-default { - --scope-background: var(--raqn-background-default); + --background: var(--raqn-background-default); } /* Theme classes to apply all scopes */ .theme-1 { - --scope-color: var(--raqn-color-1); - --scope-background: var(--raqn-background-1); + --color: var(--raqn-color-1); + --background: var(--raqn-background-1); } .theme-2 { - --scope-color: var(--raqn-color-2); - --scope-background: var(--raqn-background-2); + --color: var(--raqn-color-2); + --background: var(--raqn-background-2); } .theme-default { - --scope-color: var(--raqn-color-default); - --scope-background: var(--raqn-background-default); + --color: var(--raqn-color-default); + --background: var(--raqn-background-default); } ``` @@ -86,35 +86,43 @@ ${tags} { CSS output: ```css -h1, .heading1 { - font-size: 40px; - font-weight: bold; - line-height: 1.4em; -} -h2, .heading2 { - font-size: 30px; - font-weight: 600; - line-height: 1em; - font-style: italic; -} -h3, .heading3 { - font-size: 25px; - font-weight: bold; -} -h4, .heading4 { - font-size: 20px; - font-weight: bold; -} -h5, .heading5 { - font-size: 18px; - font-weight: bold; -} -p,body,pre,input { - font-size: 12px; - font-weight: normal; - font-family: Roboto,Arial, sans-serif; - line-height: 1.2em; - font-style: normal; +h1, +.heading1 { + font-size: 40px; + font-weight: bold; + line-height: 1.4em; +} +h2, +.heading2 { + font-size: 30px; + font-weight: 600; + line-height: 1em; + font-style: italic; +} +h3, +.heading3 { + font-size: 25px; + font-weight: bold; +} +h4, +.heading4 { + font-size: 20px; + font-weight: bold; +} +h5, +.heading5 { + font-size: 18px; + font-weight: bold; +} +p, +body, +pre, +input { + font-size: 12px; + font-weight: normal; + font-family: Roboto, Arial, sans-serif; + line-height: 1.2em; + font-style: normal; } ``` @@ -143,28 +151,28 @@ The theme and fonts are applied by default to the site: You can set up additional variables for general purposes. Here are some examples: 1. **Colors Variables** - - `background`: Change general background - - `inner-background`: Change a child element background, e.g., card backgrounds - - `link-color`: Link colors - - `link-color-hover`: Link hover and active color - - `accent-color`: Buttons and CTAs color - - `accent-background`: Buttons and CTAs background - - `accent-color-hover`: Buttons and CTAs hover and active color - - `accent-background-hover`: Buttons and CTAs hover and active background - - `header-background`: Header background - - `header-color`: Header text color - - `headings-color`: Headings color (h1 to h3) - - `footer-background`: Footer background color + - `background`: Change general background + - `inner-background`: Change a child element background, e.g., card backgrounds + - `link-color`: Link colors + - `link-color-hover`: Link hover and active color + - `accent-color`: Buttons and CTAs color + - `accent-background`: Buttons and CTAs background + - `accent-color-hover`: Buttons and CTAs hover and active color + - `accent-background-hover`: Buttons and CTAs hover and active background + - `header-background`: Header background + - `header-color`: Header text color + - `headings-color`: Headings color (h1 to h3) + - `footer-background`: Footer background color 2. **Block Model** - - `max-width`: Full width / max container (preferably using vw unit) - - `padding`: Padding of an element - - `inner-padding`: Padding of a child element, e.g., cards - - `gap`: Grid gap between columns - - `margin`: Margin of an element - - `icon-size`: Icon size (square) + - `max-width`: Full width / max container (preferably using vw unit) + - `padding`: Padding of an element + - `inner-padding`: Padding of a child element, e.g., cards + - `gap`: Grid gap between columns + - `margin`: Margin of an element + - `icon-size`: Icon size (square) 3. **Alignment** - - `align`: Vertical alignment of elements - - `justify`: Horizontal alignment of elements + - `align`: Vertical alignment of elements + - `justify`: Horizontal alignment of elements ## Example of Theme spreadsheet @@ -211,7 +219,7 @@ And its corresponding documentation: A special block named **Style** allows the use of only theme and atomic classes, without loading additional features. Here's an example: Wherer: -1 - You don't need to add `class=` just the classname +1 - You don't need to add `class=` just the classname 2 - No other feature or block is loaded ![Style Example](../assets/style-example.png) @@ -220,4 +228,4 @@ Wherer: Although we have developed a font-face theme definition, the current EDGE delivery lacks the capability to maintain fonts in drive or serve them: -![Font Limitation](../assets/font-limitation.png) \ No newline at end of file +![Font Limitation](../assets/font-limitation.png) diff --git a/fstab.yaml b/fstab.yaml index 80077e2a..e319ab7c 100644 --- a/fstab.yaml +++ b/fstab.yaml @@ -1,2 +1,2 @@ mountpoints: - /: https://henkelgroup.sharepoint.com/teams/guideraqnio/Shared%20Documents/website + /: https://becauseof666.sharepoint.com/sites/raqn/Shared%20Documents/web-guide diff --git a/scripts/component-base.js b/scripts/component-base.js index 918280d8..bd7805e1 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -6,7 +6,12 @@ import { camelCaseAttr, capitalizeCaseAttr, deepMerge, - buildConfig, + classToFlat, + externalConfig, + unflat, + isObject, + flatAsValue, + flat, } from './libs.js'; export default class ComponentBase extends HTMLElement { @@ -28,7 +33,7 @@ export default class ComponentBase extends HTMLElement { } get isInitAsBlock() { - return this.initOptions.target.classList.contains(this.componentName); + return this.initOptions?.target?.classList?.contains(this.componentName); } constructor() { @@ -87,12 +92,15 @@ export default class ComponentBase extends HTMLElement { } setInitializationPromise() { - const { promise, resolve, reject } = Promise.withResolvers(); - this.initialization = promise; // useful to wait on this prop for initialization after the element is created, - this.initResolvers = { - resolve, - reject, - }; + this.initialization = new Promise((resolve, reject) => { + this.initResolvers = { + resolve, + reject, + }; + }); + // Promise.withResolvers don't fullfill last 2 versions of Safari + // eg this breaks everything in Safari < 17.4, we need to support. + // const { promise, resolve, reject } = Promise.withResolvers(); } // Using the `method` which returns an array of objects it's easier to extend @@ -119,13 +127,9 @@ export default class ComponentBase extends HTMLElement { async init(initOptions) { try { this.wasInitBeforeConnected = true; - this.initOptions = initOptions || {}; - const { externalConfigName, configByClasses = [] } = this.initOptions; - - await this.buildExternalConfig(externalConfigName, configByClasses); - this.mergeConfigs(); - this.setAttributesClassesAndProps(); + await this.buildExternalConfig(); + this.runConfigsByViewport(); this.addDefaultsToNestedConfig(); // Add extra functionality to be run on init. await this.onInit(); @@ -188,68 +192,45 @@ export default class ComponentBase extends HTMLElement { async initOnConnected() { if (this.wasInitBeforeConnected) return; - const configByClasses = this.dataset.configByClasses?.trim?.().split?.(' ') || []; - await this.buildExternalConfig(this.dataset.configName, configByClasses); + await this.buildExternalConfig(); + this.runConfigsByViewport(); delete this.dataset.configName; delete this.dataset.configByClasses; - this.mergeConfigs(); - this.setAttributesClassesAndProps(); this.addDefaultsToNestedConfig(); // Add extra functionality to be run on init. await this.onInit(); } - async buildExternalConfig(externalConfigName, configByClasses, knownAttr) { - this.externalOptions = await buildConfig( - this.componentName, - externalConfigName, - configByClasses, - knownAttr || this.Handler.observedAttributes, - ); - } - - mergeConfigs() { - this.initOptions.loaderConfig = deepMerge({}, this.Handler.loaderConfig, this.initOptions.loaderConfig); - this.props = deepMerge({}, this.initOptions.props, this.externalOptions.props); - - this.config = deepMerge({}, this.config, this.initOptions.componentConfig, this.externalOptions.config); + async buildExternalConfig() { + let configByClasses = this.initOptions.configByClasses || []; + // normalize the configByClasses to serializable format + const { byName } = getBreakPoints(); + configByClasses = configByClasses + // remove the first class which is the component name and keep only compound classes + .filter((c, index) => c.includes('-') && index !== 0) + // make sure break points are included in the config + .map((c) => { + const exceptions = ['all', 'config']; + const firstClass = c.split('-')[0]; + const isBreakpoint = Object.keys(byName).includes(firstClass) || exceptions.includes(firstClass); + return isBreakpoint ? c : `all-${c}`; + }); - this.attributesValues = deepMerge( - this.attributesValues, - this.initOptions.attributesValues, - this.externalOptions.attributesValues, - ); + // serialize the configByClasses into a flat object + let values = classToFlat(configByClasses); - this.nestedComponentsConfig = deepMerge( - this.nestedComponentsConfig, - this.initOptions.nestedComponentsConfig, - this.externalOptions.nestedComponentsConfig, - ); - } + // get the external config + if (values.config) { + const configs = unflat(await externalConfig.getConfig(this.webComponentName, values.config)); + values = deepMerge({}, values, configs); + delete values.config; + } - setAttributesClassesAndProps() { - Object.entries(this.props).forEach(([prop, value]) => { - this[prop] = value; - }); - // Set attributes based on attributesValues - this.sortedAttributes.forEach(([attr, attrValues]) => { - const isClass = attr === 'class'; - const val = attrValues[this.breakpoints.active.name] ?? attrValues.all; - if (isClass) { - const classes = (attrValues.all ? `${attrValues.all} ` : '') + (attrValues[this.breakpoints.active.name] ?? ''); - const classesArr = classes.split(' ').flatMap((cls) => { - if (cls) return cls.trim(); - return []; - }); - if (!classesArr.length) return; - this.classList.add(...classesArr); - } else { - this.dataset[attr] = val; - } - }); + // add to attributesValues + this.attributesValues = deepMerge({}, this.attributesValues, values); } get sortedAttributes() { @@ -270,7 +251,7 @@ export default class ComponentBase extends HTMLElement { targetsAsContainers: true, }, }; - this.nestedComponentsConfig[key] = deepMerge(defaults, this.nestedComponentsConfig[key]); + this.nestedComponentsConfig[key] = deepMerge({}, defaults, this.nestedComponentsConfig[key]); }); } @@ -290,38 +271,86 @@ export default class ComponentBase extends HTMLElement { addContentFromTarget() { const { target } = this.initOptions; - + const { contentFromTargets } = this.config; + if (!contentFromTargets) return; this.append(...target.childNodes); } onBreakpointChange(e) { if (e.matches) { - this.setBreakpointAttributesValues(e); + this.runConfigsByViewport(); } } - setBreakpointAttributesValues(e) { - this.sortedAttributes.forEach(([attribute, breakpointsValues]) => { - const isAttribute = attribute !== 'class'; - if (isAttribute) { - const newValue = breakpointsValues[e.raqnBreakpoint.name] ?? breakpointsValues.all; - // this will trigger the `attributeChangedCallback` and a `onAttribute${capitalizedAttr}Changed` method - // should be defined to handle the attribute value change - if (newValue ?? false) { - if (this.dataset[attribute] === newValue) return; - this.dataset[attribute] = newValue; - } else { - delete this.dataset[attribute]; - } - } else { - const prevClasses = (breakpointsValues[e.previousRaqnBreakpoint.name] ?? '').split(' ').filter((x) => x); - const newClasses = (breakpointsValues[e.raqnBreakpoint.name] ?? '').split(' ').filter((x) => x); - const removeClasses = prevClasses.filter((prevClass) => !newClasses.includes(prevClass)); - const addClasses = newClasses.filter((newClass) => !prevClasses.includes(newClass)); + runConfigsByViewport() { + const { name } = getBreakPoints().active; + const current = deepMerge({}, this.attributesValues.all, this.attributesValues[name]); + this.className = ''; + this.cleanDataset(); + Object.keys(current).forEach((key) => { + const action = `apply${key.charAt(0).toUpperCase() + key.slice(1)}`; - if (removeClasses.length) this.classList.remove(...removeClasses); - if (addClasses.length) this.classList.add(...addClasses); + if (typeof this[action] === 'function') { + return this[action]?.(current[key]); } + return this.applyClass(current[key]); + }); + } + + // ${viewport}-data-${attr}-"${value}" + applyData(entries) { + // received as {col:{ direction:2 }, columns: 2} + const values = flat(entries); + // transformed into values as {col-direction: 2, columns: 2} + Object.keys(values).forEach((key) => { + // camelCaseAttr converst col-direction into colDirection + this.dataset[camelCaseAttr(key)] = values[key]; + }); + } + + // ${viewport}-class-${value} + applyClass(className) { + // {'color':'primary', 'max':'width'} -> 'color-primary max-width' + + // classes can be serialized as a string or an object + if (isObject(className)) { + // if an object is passed, it's flat and splited + this.classList.add(...flatAsValue(className).split(' ')); + } else { + // strings are added as is + this.classList.add(className); + } + } + + // ${viewport}-attribute-${value} + + applyAttribute(entries) { + // received as {col:{ direction:2 }, columns: 2} + const values = flat(entries); + // transformed into values as {col-direction: 2, columns: 2} + Object.keys(values).forEach((key) => { + // camelCaseAttr converst col-direction into colDirection + this.setAttribute(key, values[key]); + }); + } + + // ${viewport}-nest-${value} + + applyNest(config) { + const names = Object.keys(config); + names.map((key) => { + const instance = document.createElement(`raqn-${key}`); + instance.initOptions.configByClasses = [config[key]]; + + this.cachedChildren = Array.from(this.initOptions.target.children); + this.cachedChildren.forEach((child) => instance.append(child)); + this.append(instance); + }); + } + + cleanDataset() { + Object.keys(this.dataset).forEach((key) => { + delete this.dataset[key]; }); } @@ -352,7 +381,7 @@ export default class ComponentBase extends HTMLElement { } addListeners() { - if (this.externalOptions.hasBreakpointsValues || this.config.listenBreakpoints) { + if (Object.keys(this.attributesValues).length > 1) { listenBreakpointChange(this.onBreakpointChange); } } diff --git a/scripts/component-loader.js b/scripts/component-loader.js index eaeaa4a7..3d935868 100644 --- a/scripts/component-loader.js +++ b/scripts/component-loader.js @@ -1,5 +1,7 @@ import { loadModule, deepMerge, mergeUniqueArrays, getBreakPoints } from './libs.js'; +window.raqnInstances = window.raqnInstances || {}; + export default class ComponentLoader { constructor({ componentName, @@ -17,6 +19,7 @@ export default class ComponentLoader { if (!componentName) { throw new Error('`componentName` is required'); } + this.instances = window.raqnInstances || {}; this.componentName = componentName; this.targets = targets.map((target) => ({ target })); this.loaderConfig = loaderConfig; @@ -87,6 +90,9 @@ export default class ComponentLoader { let elem = null; try { elem = await this.createElementAndConfigure(data); + elem.webComponentName = this.webComponentName; + this.instances[elem.componentName] = this.instances[elem.componentName] || []; + this.instances[elem.componentName].push(elem); } catch (error) { error.elem ??= elem; elem?.classList.add('hide-with-error'); diff --git a/scripts/editor-preview.js b/scripts/editor-preview.js new file mode 100644 index 00000000..451e56a6 --- /dev/null +++ b/scripts/editor-preview.js @@ -0,0 +1,44 @@ +import ComponentLoader from './component-loader.js'; +import { deepMerge } from './libs.js'; +import { publish } from './pubsub.js'; + +export default async function preview(component, classes, uuid) { + const { componentName } = component; + const header = document.querySelector('header'); + const footer = document.querySelector('footer'); + const main = document.querySelector('main'); + main.innerHTML = ''; + + if (header) { + header.parentNode.removeChild(header); + } + if (footer) { + footer.parentNode.removeChild(footer); + } + const loader = new ComponentLoader({ componentName }); + await loader.init(); + const webComponent = document.createElement(component.webComponentName); + webComponent.innerHTML = component.html; + webComponent.attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues); + main.appendChild(webComponent); + + window.addEventListener( + 'click', + (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + }, + true, + ); + + webComponent.style.display = 'inline-grid'; + webComponent.style.width = 'auto'; + webComponent.style.marginInlineStart = '0px'; + webComponent.runConfigsByViewport(); + document.body.style.setProperty('display', 'block'); + main.style.setProperty('display', 'block'); + setTimeout(() => { + const bodyRect = webComponent.getBoundingClientRect(); + publish('raqn:editor:preview:render', { bodyRect, uuid }, { usePostMessage: true, targetOrigin: '*' }); + }, 100); +} diff --git a/scripts/editor.js b/scripts/editor.js new file mode 100644 index 00000000..a53e6914 --- /dev/null +++ b/scripts/editor.js @@ -0,0 +1,131 @@ +import { deepMerge, loadModule } from './libs.js'; +import { publish } from './pubsub.js'; + +window.raqnEditor = window.raqnEditor || {}; +let watcher = false; + +export const MessagesEvents = { + init: 'raqn:editor:start', + loaded: 'raqn:editor:loaded', + active: 'raqn:editor:active', + disabled: 'raqn:editor:disabled', + render: 'raqn:editor:render', + select: 'raqn:editor:select', + updateComponent: 'raqn:editor:select:update', + theme: 'raqn:editor:theme', + themeUpdate: 'raqn:editor:theme:update', +}; + +export function refresh(id) { + Object.keys(window.raqnEditor).forEach((name) => { + window.raqnEditor[name].instances = window.raqnInstances[name].map((item) => + // eslint-disable-next-line no-use-before-define + getComponentValues(window.raqnEditor[name].dialog, item), + ); + }); + const bodyRect = window.document.body.getBoundingClientRect(); + publish( + MessagesEvents.render, + { components: window.raqnEditor, bodyRect, uuid: id }, + { usePostMessage: true, targetOrigin: '*' }, + ); +} + +export function updateComponent(component) { + const { componentName, uuid } = component; + const instance = window.raqnInstances[componentName].find((element) => element.uuid === uuid); + if (!instance) return; + + instance.attributesValues = deepMerge({}, instance.attributesValues, component.attributesValues); + instance.runConfigsByViewport(); + refresh(uuid); +} + +export function getComponentValues(dialog, element) { + const html = element.innerHTML; + window.document.body.style.height = 'auto'; + const domRect = element.getBoundingClientRect(); + let { variables = {}, attributes = {} } = dialog; + const { selection = {} } = dialog; + variables = Object.keys(variables).reduce((data, variable) => { + const value = getComputedStyle(element).getPropertyValue(variable); + + data[variable] = { ...variables[variable], value }; + + return data; + }, {}); + attributes = Object.keys(attributes).reduce((data, attribute) => { + const value = element.getAttribute(attribute); + + data[attribute] = { ...attributes[attribute], value }; + return data; + }, {}); + const cleanData = Object.fromEntries(Object.entries(element)); + delete cleanData.initOptions; + delete cleanData.childComponents; + delete cleanData.nestedComponents; + delete cleanData.nestedComponentsConfig; + return { ...cleanData, domRect, editor: { variables, attributes, selection }, html }; +} + +export default function initEditor(listeners = true) { + Promise.all( + Object.keys(window.raqnComponents).map( + (componentName) => + new Promise((resolve) => { + setTimeout(async () => { + try { + const component = await loadModule(`/blocks/${componentName}/${componentName}.editor`, false); + const mod = await component.js; + if (mod && mod.default) { + const dialog = await mod.default(); + // available dialog and component instances + window.raqnEditor[componentName] = { dialog, instances: [], name: componentName }; + window.raqnEditor[componentName].instances = window.raqnInstances[componentName].map((item) => + getComponentValues(dialog, item), + ); + } + resolve(); + } catch (error) { + resolve(); + } + }); + }), + ), + ).finally(() => { + const bodyRect = window.document.body.getBoundingClientRect(); + + publish( + MessagesEvents.loaded, + { components: window.raqnEditor, bodyRect }, + { usePostMessage: true, targetOrigin: '*' }, + ); + + if (!watcher) { + window.addEventListener('resize', () => { + refresh(); + }); + watcher = true; + } + }); + if (listeners) { + // init editor if message from parent + window.addEventListener('message', async (e) => { + if (e && e.data) { + const { message, params } = e.data; + switch (message) { + case MessagesEvents.select: + updateComponent(params); + break; + + case MessagesEvents.updateComponent: + updateComponent(params); + break; + + default: + break; + } + } + }); + } +} diff --git a/scripts/init.js b/scripts/init.js index 90970b9e..d4371b2d 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -3,6 +3,7 @@ import { globalConfig, metaTags, eagerImage, getMeta, getMetaGroup, mergeUniqueA const component = { async init(settings) { + // some components may have multiple targets const { componentName = this.getBlockData(settings?.targets?.[0]).componentName } = settings || {}; try { const loader = new ComponentLoader({ @@ -10,7 +11,6 @@ const component = { componentName, }); const instances = await loader.init(); - const init = { componentName, instances: [], @@ -70,7 +70,7 @@ const component = { }, }; -const onLoadComponents = { +export const onLoadComponents = { // default content staticStructureComponents: [ { @@ -162,13 +162,14 @@ const onLoadComponents = { initBlocks() { // Keep the page hidden until specific components are initialized to prevent CLS component.multiInit(this.lcpBlocks).then(() => { - document.body.style.setProperty('display', 'unset'); + window.postMessage({ message: 'raqn:components:loaded' }); + document.body.style.setProperty('display', 'block'); }); component.multiInit(this.lazyBlocks); }, }; -const globalInit = { +export const globalInit = { async init() { this.setLang(); this.initEagerImages(); @@ -191,4 +192,42 @@ const globalInit = { globalInit.init(); +// init editor if message from parent +window.addEventListener('message', async (e) => { + if (e && e.data) { + const { message, params } = e.data; + if (!Array.isArray(params)) { + const query = new URLSearchParams(window.location.search); + switch (message) { + case 'raqn:editor:start': + (async function startEditor() { + const editor = await import('./editor.js'); + const { origin, target, preview = false } = params; + setTimeout(() => { + editor.default(origin, target, preview); + }, 2000); + })(); + break; + // other cases? + case 'raqn:editor:preview:component': + // preview editor with only a component + if (query.has('preview')) { + (async function startEditor() { + const preview = query.get('preview'); + const win = await import('./editor-preview.js'); + const { uuid } = params; + + if (uuid === preview) { + win.default(params.component, params.classes, uuid); + } + })(); + } + break; + default: + break; + } + } + } +}); + export default component; diff --git a/scripts/libs.js b/scripts/libs.js index d4efd414..ae549f34 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -3,7 +3,7 @@ export const globalConfig = { blockSelector: '[class]:not(style, [class^="config-" i])', breakpoints: { xs: 0, - s: 480, + s: 320, m: 768, l: 1024, xl: 1280, @@ -49,14 +49,29 @@ export const metaTags = { metaName: 'eager-images', // contentType: 'number string', }, - theming: { - metaName: 'theming', - fallbackContent: 'theming.json', + themecolor: { + metaName: 'color', + fallbackContent: 'color', + // contentType: 'path without extension', + }, + themefont: { + metaName: 'color', + fallbackContent: 'font', + // contentType: 'path without extension', + }, + themelayout: { + metaName: 'layout', + fallbackContent: 'layout', + // contentType: 'path without extension', + }, + themecomponent: { + metaName: 'component', + fallbackContent: 'components-config', // contentType: 'path without extension', }, theme: { metaName: 'theme', - fallbackContent: 'theme-default', + fallbackContent: 'color-default font-default', // contentType: 'string theme name', }, }; @@ -64,11 +79,14 @@ export const metaTags = { export const camelCaseAttr = (val) => val.replace(/-([a-z])/g, (k) => k[1].toUpperCase()); export const capitalizeCaseAttr = (val) => camelCaseAttr(val.replace(/^[a-z]/g, (k) => k.toUpperCase())); -export function matchMediaQuery(breakpointMin, breakpointMax) { +export function getMediaQuery(breakpointMin, breakpointMax) { const min = `(min-width: ${breakpointMin}px)`; const max = breakpointMax ? ` and (max-width: ${breakpointMax}px)` : ''; + return `${min}${max}`; +} - return window.matchMedia(`${min}${max}`); +export function matchMediaQuery(breakpointMin, breakpointMax) { + return window.matchMedia(getMediaQuery(breakpointMin, breakpointMax)); } export function getBreakPoints() { @@ -189,6 +207,25 @@ export function stringToArray(val, options) { }); } +// retrive data from excel json format +export function readValue(data, extend = {}) { + const k = Object.keys; + const keys = k(data[0]).filter((item) => item !== 'key'); + return data.reduce((acc, row) => { + const mainKey = row.key; + keys.reduce((a, key) => { + if (!row[key]) return a; + if (!a[key]) { + a[key] = { [mainKey]: row[key] }; + } else { + a[key][mainKey] = row[key]; + } + return a; + }, acc); + return acc; + }, extend); +} + export function getMeta(name, settings) { const { getArray = false } = settings || {}; const meta = document.querySelector(`meta[name="${name}"]`); @@ -249,24 +286,14 @@ export const externalConfig = { }; }, - async getConfig(componentName, configName, knownAttributes) { + async getConfig(componentName, configName) { if (!configName) return this.defaultConfig(); // to be removed in the feature and fallback to 'default' const masterConfig = await this.loadConfig(); const componentConfig = masterConfig?.[componentName]; - let parsedConfig = componentConfig?.parsed?.[configName]; + const parsedConfig = componentConfig?.[configName]; if (parsedConfig) return parsedConfig; - const rawConfig = componentConfig?.data.filter((conf) => conf.configName?.trim() === configName /* ?? 'default' */); - if (!rawConfig?.length) { - // eslint-disable-next-line no-console - console.error(`The config named '${configName}' for '${componentName}' webComponent is not valid.`); - return this.defaultConfig(); - } - const safeConfig = JSON.parse(JSON.stringify(rawConfig)); - parsedConfig = this.parseRawConfig(safeConfig, knownAttributes); - componentConfig.parsed ??= {}; - componentConfig.parsed[configName] = parsedConfig; - return parsedConfig; + return {}; }, async loadConfig() { @@ -289,214 +316,51 @@ export const externalConfig = { window.raqnComponentsConfig = await window.raqnComponentsConfig; - return window.raqnComponentsConfig; + return this.simplifiedConfig(); }, - parseRawConfig(configArr, knownAttributes) { - const parsedConfig = configArr?.reduce((acc, breakpointConfig) => { - const breakpoint = breakpointConfig.viewport.toLowerCase(); - const isMainConfig = breakpoint === 'all'; - - Object.entries(breakpointConfig).forEach(([key, val]) => { - if (val.trim() === '') return; - if (![...Object.keys(globalConfig.breakpoints), 'all'].includes(breakpoint)) return; - if (!isMainConfig) acc.hasBreakpointsValues = true; - - const parsedVal = stringToJsVal(val, { trim: true }); - - if (knownAttributes.includes(key) || key === 'class') { - this.parseAttrValues(parsedVal, acc, key, breakpoint); - } else if (isMainConfig) { - const configPrefix = 'config-'; - const propPrefix = 'prop-'; - if (key.startsWith(configPrefix)) { - this.parseConfig(parsedVal, acc, key, configPrefix); - } else if (key.startsWith(propPrefix)) { - acc.props[key.slice(propPrefix.length)] = parsedVal; - } else if (key === 'nest') { - this.parseNestedConfig(val, acc); - } + simplifiedConfig() { + window.raqnParsedConfigs = window.raqnParsedConfigs || {}; + if (window.raqnComponentsConfig) { + Object.keys(window.raqnComponentsConfig).forEach((key) => { + if (!window.raqnComponentsConfig[key]) return; + const { data } = window.raqnComponentsConfig[key]; + if (data && data.length > 0) { + window.raqnParsedConfigs[key] = window.raqnParsedConfigs[key] || {}; + window.raqnParsedConfigs[key] = readValue(data, window.raqnParsedConfigs[key]); } }); - return acc; - }, this.defaultConfig(configArr)); - - return parsedConfig; - }, - - parseAttrValues(parsedVal, acc, key, breakpoint) { - const keyProp = key.replace(/^data-/, ''); - const camelAttr = camelCaseAttr(keyProp); - acc.attributesValues[camelAttr] ??= {}; - acc.attributesValues[camelAttr][breakpoint] = parsedVal; - }, - - parseConfig(parsedVal, acc, key, configPrefix) { - const configKeys = key.slice(configPrefix.length).split('.'); - const indexLength = configKeys.length - 1; - configKeys.reduce((cof, confKey, index) => { - cof[confKey] = index < indexLength ? {} : parsedVal; - return cof[confKey]; - }, acc.config); - }, - - parseNestedConfig(val, acc) { - const parsedVal = stringToArray(val).reduce((nestConf, confVal) => { - const [componentName, activeOrConfigName] = confVal.split('='); - const parsedActiveOrConfigName = stringToJsVal(activeOrConfigName); - const isString = typeof parsedActiveOrConfigName === 'string'; - nestConf[componentName] ??= { - componentName, - externalConfigName: isString ? parsedActiveOrConfigName : null, - active: isString || parsedActiveOrConfigName, - }; - return nestConf; - }, {}); - acc.nestedComponentsConfig = parsedVal; + } + return window.raqnParsedConfigs; }, }; -export const configFromClasses = { - getConfig(componentName, configByClasses, knownAttributes) { - const nestedComponentsConfig = this.nestedConfigFromClasses(configByClasses); - const { attributesValues, hasBreakpointsValues } = this.attributeValuesFromClasses( - componentName, - configByClasses, - knownAttributes, - ); - return { - attributesValues, - nestedComponentsConfig, - hasBreakpointsValues, - }; - }, - - nestedComponentsNames(configByClasses) { - const nestPrefix = 'nest-'; // - - return configByClasses.flatMap((c) => (c.startsWith(nestPrefix) ? [c.slice(nestPrefix.length)] : [])); - }, - - nestedConfigFromClasses(configByClasses) { - const nestedComponentsNames = this.nestedComponentsNames(configByClasses); - const nestedComponentsConfig = configByClasses.reduce((acc, c) => { - let value = c; - - const classBreakpoint = this.classBreakpoint(c); - const isBreakpoint = this.isBreakpoint(classBreakpoint); - - if (isBreakpoint) value = value.slice(classBreakpoint.length + 1); - - const componentName = nestedComponentsNames.find((prefix) => value.startsWith(prefix)); - if (componentName) { - acc[componentName] ??= { componentName, active: true }; - const val = value.slice(componentName.length + 1); - const active = 'active-'; - if (val.startsWith(active)) { - acc[componentName].active = stringToJsVal(val.slice(active.length)); - } else { - acc[componentName].configByClasses ??= ''; - acc[componentName].configByClasses += `${isBreakpoint ? `${classBreakpoint}-` : ''}${val} `; - } +export function loadModule(urlWithoutExtension, loadCSS = true) { + try { + const js = import(`${urlWithoutExtension}.js`); + if (!loadCSS) return { js, css: Promise.resolve() }; + const css = new Promise((resolve, reject) => { + const cssHref = `${urlWithoutExtension}.css`; + if (!document.querySelector(`head > link[href="${cssHref}"]`)) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = cssHref; + link.onload = resolve; + link.onerror = reject; + document.head.append(link); + } else { + resolve(); } - return acc; - }, {}); - return nestedComponentsConfig; - }, - - attributeValuesFromClasses(componentName, configByClasses, knownAttributes) { - let hasBreakpointsValues = false; - const nestedComponentsNames = this.nestedComponentsNames(configByClasses); - const onlyKnownAttributes = knownAttributes.filter((a) => a !== 'class'); - const attributesValues = configByClasses - .filter((c) => c !== componentName && c !== 'block') - .reduce((acc, c) => { - let value = c; - let isKnownAttribute = null; - - const classBreakpoint = this.classBreakpoint(c); - const isBreakpoint = this.isBreakpoint(classBreakpoint); - - if (isBreakpoint) { - hasBreakpointsValues = true; - value = value.slice(classBreakpoint.length + 1); - } - - const excludeNested = nestedComponentsNames.find((prefix) => value.startsWith(prefix)); - if (excludeNested) return acc; - - let key = 'class'; - const isClassValue = value.startsWith(key); - if (isClassValue) { - value = value.slice(key.length + 1); - } else { - [isKnownAttribute] = onlyKnownAttributes.flatMap((attribute) => { - const noDataPrefix = attribute.replace(/^data-/, ''); - if (!value.startsWith(`${noDataPrefix}-`)) return []; - return noDataPrefix; - }); - if (isKnownAttribute) { - key = isKnownAttribute; - value = value.slice(isKnownAttribute.length + 1); - } - } - - const isClass = key === 'class'; - const camelCaseKey = camelCaseAttr(key); - if (isKnownAttribute || isClass) acc[camelCaseKey] ??= {}; - if (isKnownAttribute) acc[camelCaseKey][classBreakpoint] = value; - if (isClass) { - acc[camelCaseKey][classBreakpoint] ??= ''; - acc[camelCaseKey][classBreakpoint] += `${value} `; - } - return acc; - }, {}); - - return { attributesValues, hasBreakpointsValues }; - }, - classBreakpoint(c) { - return Object.keys(globalConfig.breakpoints).find((b) => c.startsWith(`${b}-`)) || 'all'; - }, - isBreakpoint(classBreakpoint) { - return classBreakpoint !== 'all'; - }, -}; - -export async function buildConfig(componentName, externalConf, configByClasses, knownAttributes = []) { - const configPrefix = 'config-'; - let config; - const externalConfigName = - configByClasses.find((c) => c.startsWith(configPrefix))?.slice?.(configPrefix.length) || externalConf; + }).catch((error) => + // eslint-disable-next-line no-console + console.log('could not load module style', urlWithoutExtension, error), + ); - if (externalConfigName) { - config = await externalConfig.getConfig(componentName, externalConfigName, knownAttributes); - } else { - config = configFromClasses.getConfig(componentName, configByClasses, knownAttributes); + return { css, js }; + } catch (error) { + console.log('could not load module', urlWithoutExtension, error); } - - return config; -} - -export function loadModule(urlWithoutExtension) { - const js = import(`${urlWithoutExtension}.js`); - const css = new Promise((resolve, reject) => { - const cssHref = `${urlWithoutExtension}.css`; - if (!document.querySelector(`head > link[href="${cssHref}"]`)) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = cssHref; - link.onload = resolve; - link.onerror = reject; - document.head.append(link); - } else { - resolve(); - } - }).catch((error) => - // eslint-disable-next-line no-console - console.error('could not load module style', urlWithoutExtension, error), - ); - - return { css, js }; + return { css: Promise.resolve(), js: Promise.resolve() }; } export function mergeUniqueArrays(...arrays) { @@ -591,3 +455,75 @@ export const focusTrap = (elem, { dynamicContent } = { dynamicContent: false }) } }); }; + +/** + * flattenProperties: convert objects from {a:{b:{c:{d:1}}}} to all subkeys as strings {'a-b-c-d':1} + * + * @param {Object} obj - Object to flatten + * @param {String} alreadyFlat - prefix or recursive keys. + * */ + +export function flat(obj = {}, alreadyFlat = '', sep = '-', maxDepth = 10) { + const f = {}; + // check if its a object + Object.keys(obj).forEach((k) => { + // get the value + const value = obj[k].valueOf() || obj[k]; + // append key to already flatten Keys + const key = `${alreadyFlat ? `${alreadyFlat}${sep}` : ''}${k}`; + // if still a object fo recursive + if (isObject(value) && maxDepth > 0) { + Object.assign(f, flat(value, key, sep, maxDepth - 1)); + } else { + // there is a real value so add key to flat object + f[key] = value; + } + }); + return f; +} + +export function flatAsValue(data, sep = '-') { + return Object.entries(data) + .reduce((acc, [key, value]) => { + if (isObject(value)) { + return flatAsValue(value, acc); + } + return `${acc} ${key}${sep}${value}`; + }, '') + .trim(); +} + +/** + * unFlattenProperties: convert objects from subkeys as strings {'a-b-c-d':1} to tree {a:{b:{c:{d:1}}}} + * + * @param {Object} obj - Object to unflatten + * */ + +export function unflat(f, sep = '-') { + const un = {}; + // for each key create objects + Object.keys(f).forEach((key) => { + const properties = key.split(sep); + const value = f[key]; + properties.reduce((unflating, prop, i) => { + if (!unflating[prop]) { + const step = i < properties.length - 1 ? { [prop]: {} } : { [prop]: value }; + Object.assign(unflating, step); + } + return unflating[prop]; + }, un); + }); + return un; +} + +export const classToFlat = (classes = [], valueLength = 1, extend = {}) => + unflat( + classes.reduce((acc, c) => { + const length = c.split('-').length - valueLength; + const key = c.split('-').slice(0, length).join('-'); + const value = c.split('-').slice(length).join('-'); + if (!acc[key]) acc[key] = {}; + acc[key] = value; + return acc; + }, extend), + ); diff --git a/scripts/pubsub.js b/scripts/pubsub.js index 4f21b20f..5dfd25c3 100644 --- a/scripts/pubsub.js +++ b/scripts/pubsub.js @@ -53,12 +53,11 @@ export const unsubscribeAll = (options = {}) => { return; } - Object.keys(actions) - .forEach((key) => { - if (exactFit ? key === message : key.includes(message)) { - delete actions[key]; - } - }); + Object.keys(actions).forEach((key) => { + if (exactFit ? key === message : key.includes(message)) { + delete actions[key]; + } + }); }; export const callStack = (message, params, options) => { @@ -69,19 +68,9 @@ export const callStack = (message, params, options) => { if (actions[message]) { const messageCallStack = Array.from(actions[message]); // copy array // call all actions by last one registered - let prevent = false; - - // Some current usages of `publish` are not passing `params` as an object. - // For these cases the option to `stopImmediatePropagation` will not be available. - if (params && typeof params === 'object' && !Array.isArray(params)) { - params.stopImmediatePropagation = () => { - prevent = true; - }; - } - // run the call stack unless `stopImmediatePropagation()` was called in previous action (prevent further actions to run) const callStackMethod = callStackAscending ? 'shift' : 'pop'; - while (!prevent && messageCallStack.length > 0) { + while (messageCallStack.length > 0) { const action = messageCallStack[callStackMethod](); action(params); } @@ -94,14 +83,16 @@ export const postMessage = (message, params, options = {}) => { let data = { message }; try { - data = JSON.parse(JSON.stringify({ message, params })); + data = { message, params: JSON.parse(JSON.stringify(params)) }; } catch (error) { // some objects cannot be passed by post messages like when passing htmlElements. // for those that can be published but are not compatible with postMessages we don't send params // eslint-disable-next-line no-console console.warn(error); } - + // upward message + window.parent.postMessage(data, targetOrigin); + // downward message window.postMessage(data, targetOrigin); }; @@ -111,7 +102,6 @@ export const publish = (message, params, options = {}) => { callStack(message, params, options); return; } - postMessage(message, params, options); }; @@ -125,4 +115,4 @@ if (!window.messageListenerAdded) { } } }); -} \ No newline at end of file +} diff --git a/styles/styles.css b/styles/styles.css index 2caeb902..b54c13c9 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -1,6 +1,7 @@ @media screen and (max-width: 768px) { body { - --scope-max-width: 100vw; + --raqn-max-width-default: var(--max-width, 80vw); + --max-width: 100vw; } } @@ -11,15 +12,35 @@ img { html, body { + --raqn-max-width-default: 80vw; + --max-width: var(--raqn-max-width-default, 80vw); + --header-height: 110px; + width: 100%; height: 100%; margin: 0; padding: 0; + color: var(--text, #000); + font-family: var(--p-font-family, roboto); + font-size: var(--p-font-size, 16px); + font-weight: var(--p-font-weight, normal); + font-style: var(--p-font-style, normal); + line-height: var(--p-line-height, 1.2em); } body { display: none; - background: var(--scope-background, #fff); + background: var(--background, #fff); + padding: 0; + margin: 0; + width: 100%; +} + +main { + background: var(--background, #fff); + padding: 0; + margin: 0; + width: 100%; position: relative; min-height: 100%; } @@ -69,16 +90,19 @@ legend, caption { font-size: 100%; vertical-align: baseline; - color: currentcolor; } -header { - --scope-background: var(--scope-header-background, #fff); - --scope-color: var(--scope-header-color, #000); +h1, +h2, +h3, +h4 { + color: var(--title, #000); +} - min-height: var(--scope-header-height, 64px); +header { + min-height: var(--header-height, 64px); display: grid; - background: var(--scope-header-background, #fff); + background: var(--header-background, #fff); } head:has(meta[name='header'][content='false' i]) + body > header, @@ -87,15 +111,16 @@ head:has(meta[name='footer'][content='false' i]) + body > footer { } main > div { - max-width: var(--scope-max-width, 100%); + max-width: var(--max-width, 100%); margin: 0 auto; } main > div > * { - max-width: var(--scope-max-width, 100%); - min-width: var(--scope-max-width, 100%); + max-width: var(--max-width, 100%); + min-width: var(--max-width, 100%); margin-inline: auto; box-sizing: border-box; + background-color: var(--background, #fff); } main > .raqn-grid > * { @@ -104,48 +129,48 @@ main > .raqn-grid > * { } .full-width { - --scope-outer-gap: calc((var(--raqn-max-width-default) - 100vw) / 2); - --scope-inner-gap: calc((100vw - var(--scope-max-width)) / 2); + --outer-gap: calc((var(--raqn-max-width-default) - 100vw) / 2); + --inner-gap: calc((100vw - var(--max-width)) / 2); display: grid; min-width: 100vw; max-width: none; - margin-inline-start: var(--scope-outer-gap); - padding-inline: var(--scope-inner-gap); + margin-inline-start: var(--outer-gap); + padding-inline: var(--inner-gap); box-sizing: border-box; } main > div > div { - background: var(--scope-background, #fff); - color: var(--scope-color, #000); - padding: var(--scope-padding, 0); + background: var(--background, #fff); + color: var(--text, #000); + padding: var(--padding, 0); } main > div > div > div { - max-width: var(--scope-max-width, 100%); - margin: var(--scope-margin, 0 auto); + max-width: var(--max-width, 100%); + margin: var(--margin, 0 auto); width: 100%; } .breadcrumbs { display: grid; - grid-template-columns: var(--scope-grid-template-columns, 1fr); - gap: var(--scope-gap, 20px); + grid-template-columns: var(--grid-template-columns, 1fr); + gap: var(--gap, 20px); align-items: center; justify-items: start; - min-height: var(--scope-font-size, 1.2em); + min-height: var(--font-size, 1.2em); } a { line-height: 1em; align-items: center; - color: var(--scope-link-color, inherit); + color: var(--highlight, inherit); text-decoration: none; - font-size: var(--scope-font-size, 1em); + font-size: var(--font-size, 1em); } a:hover { - color: var(--scope-link-color-hover, inherit); + color: var(--text, inherit); } button { @@ -163,7 +188,7 @@ button:hover { } .raqn-grid { - width: var(--scope-max-width, 100%); + width: var(--max-width, 100%); margin: 0 auto; display: grid; grid-template-columns: var(--grid-template-columns, 1fr); @@ -185,40 +210,6 @@ img { pointer-events: none; } -p, -h1, -h2, -h3, -h4, -h5, -h6 { - margin-block: var(--scope-margin-block, 1em); -} - -@media screen and (min-width: 1024px) { - p, - ul, - ol, - pre, - h1, - h2, - h3, - h4, - h5, - h6 { - max-width: 50vw; - } -} - -h1, -h2, -h3, -h4, -h5, -h6 { - color: var(--scope-headings-color, var(--scope-color, currentColor)); -} - #franklin-svg-sprite { display: none; }