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;
}