diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js
index ac3bf1da..7d5efaee 100644
--- a/blocks/accordion/accordion.js
+++ b/blocks/accordion/accordion.js
@@ -3,6 +3,16 @@ import ComponentBase from '../../scripts/component-base.js';
export default class Accordion extends ComponentBase {
dependencies = ['icon'];
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ innerComponents: null,
+ nestedComponentsPrefix: ':scope > ',
+ },
+ ];
+ }
+
extendNestedConfig() {
return [
...super.extendNestedConfig(),
diff --git a/blocks/breadcrumbs/breadcrumbs.js b/blocks/breadcrumbs/breadcrumbs.js
index 67a12886..c946a486 100644
--- a/blocks/breadcrumbs/breadcrumbs.js
+++ b/blocks/breadcrumbs/breadcrumbs.js
@@ -8,6 +8,12 @@ export default class Breadcrumbs extends ComponentBase {
targetsSelectorsLimit: 1,
};
+ attributesValues = {
+ all: {
+ class: 'full-width',
+ },
+ };
+
nestedComponentsConfig = {};
extendConfig() {
@@ -24,9 +30,7 @@ export default class Breadcrumbs extends ComponentBase {
];
}
- connected() {
- this.classList.add('full-width');
- this.classList.add('breadcrumbs');
+ addHtml() {
const { origin, pathname } = window.location;
let breadcrumbRoot = getMeta(metaTags.breadcrumbRoot.metaName);
breadcrumbRoot = breadcrumbRoot?.startsWith('/') ? breadcrumbRoot : `/${breadcrumbRoot}`;
diff --git a/blocks/button/button.js b/blocks/button/button.js
index cb3d1a5d..6f3d07ef 100644
--- a/blocks/button/button.js
+++ b/blocks/button/button.js
@@ -28,7 +28,7 @@ export default class Button extends ComponentBase {
];
}
- connected() {
+ addEDSHtml() {
this.initAsBlock();
this.queryElements();
this.wrapText();
diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css
index 9be87f1b..14d0c78d 100644
--- a/blocks/footer/footer.css
+++ b/blocks/footer/footer.css
@@ -1,12 +1,8 @@
-footer {
- background: var(--background-color);
- width: var(--max-width);
- margin: 0 auto;
-}
-
raqn-footer {
background: var(--background-color);
border-top: 1px solid var(--text);
+ padding: 20px 0;
+
}
raqn-footer ul {
@@ -22,8 +18,8 @@ raqn-footer ul li a {
@media screen and (min-width: 1024px) {
raqn-footer {
- display: grid;
- grid-template-columns: auto 20vw;
+ /* display: grid;
+ grid-template-columns: auto 20vw; */
}
raqn-footer ul li a {
@@ -40,10 +36,10 @@ raqn-footer ul li a {
margin: 2em 0;
}
- raqn-footer > *:last-child {
+ /* raqn-footer > *:last-child {
justify-self: end;
align-self: center;
- }
+ } */
}
raqn-footer ul li:last-child a {
diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js
index e7f78b56..a3b53776 100644
--- a/blocks/footer/footer.js
+++ b/blocks/footer/footer.js
@@ -6,6 +6,8 @@ const metaFooter = getMeta(metaTags.footer.metaName);
export default class Footer extends ComponentBase {
static loaderConfig = {
...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > footer',
+ targetsSelectorsLimit: 1,
loaderStopInit() {
return metaFooter === false;
},
@@ -17,18 +19,18 @@ export default class Footer extends ComponentBase {
return [
...super.extendConfig(),
{
+ contentFromTargets: false,
addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ contentFromTargets: false,
+ },
},
];
}
ready() {
- const child = this.children[0];
- if (!child) return;
- child.replaceWith(...child.children);
this.nav = this.querySelector('ul');
this.nav?.setAttribute('role', 'navigation');
- this.classList.add('full-width');
- this.classList.add('horizontal');
}
}
diff --git a/blocks/grid-item/grid-item.js b/blocks/grid-item/grid-item.js
index 312a2398..68bf62b0 100644
--- a/blocks/grid-item/grid-item.js
+++ b/blocks/grid-item/grid-item.js
@@ -1,4 +1,5 @@
import ComponentBase from '../../scripts/component-base.js';
+import { globalConfig } from '../../scripts/libs.js';
export default class Grid extends ComponentBase {
static observedAttributes = [
@@ -12,7 +13,7 @@ export default class Grid extends ComponentBase {
'data-align',
];
- nestedComponentsConfig = {};
+ // nestedComponentsConfig = {};
attributesValues = {
all: {
@@ -22,8 +23,18 @@ export default class Grid extends ComponentBase {
},
};
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ innerComponents: globalConfig.blockSelector,
+ },
+ ];
+ }
+
setDefaults() {
super.setDefaults();
+
this.gridParent = null;
}
@@ -48,7 +59,7 @@ export default class Grid extends ComponentBase {
}
}
- connected() {
+ ready() {
this.gridParent ??= this.parentElement;
}
@@ -87,4 +98,28 @@ export default class Grid extends ComponentBase {
this.style.removeProperty(prop);
}
}
+
+ addEDSHtml() {
+ if (!this.isInitAsBlock) return;
+
+ this.recursiveItems(this.previousElementSibling);
+ }
+
+ recursiveItems(elem) {
+ if (!elem) return;
+ if (this.isGridItem(elem)) return;
+ if (this.isRaqnGrid(elem)) return;
+
+ this.prepend(elem);
+
+ this.recursiveItems(this.previousElementSibling);
+ }
+
+ isGridItem(elem) {
+ return elem.tagName === 'DIV' && elem.classList.contains('grid-item');
+ }
+
+ isRaqnGrid(elem) {
+ return elem.tagName === 'RAQN-GRID-ITEM';
+ }
}
diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css
index ba44b35b..5a535ba9 100644
--- a/blocks/grid/grid.css
+++ b/blocks/grid/grid.css
@@ -1,7 +1,7 @@
raqn-grid {
/* Set to initial to prevent inheritance for nested grids */
--grid-height: initial;
- --grid-width: 100%;
+ --grid-width: initial;
--grid-justify-items: initial;
--grid-align-items: initial;
--grid-justify-content: initial;
diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js
index b46a68d5..a75b8094 100644
--- a/blocks/grid/grid.js
+++ b/blocks/grid/grid.js
@@ -1,6 +1,5 @@
import ComponentBase from '../../scripts/component-base.js';
import { stringToJsVal } from '../../scripts/libs.js';
-import component from '../../scripts/init.js';
export default class Grid extends ComponentBase {
static observedAttributes = [
@@ -29,12 +28,17 @@ export default class Grid extends ComponentBase {
},
};
- setDefaults() {
- super.setDefaults();
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ innerComponents: ':scope > .grid-item',
+ },
+ ];
}
get gridItems() {
- return [...this.children];
+ return [...this.children].filter((el) => el.tagName.toLowerCase() === 'raqn-grid-item');
}
onAttributeHeightChanged({ oldValue, newValue }) {
@@ -190,87 +194,20 @@ export default class Grid extends ComponentBase {
}
}
- async connected() {
- await this.collectGridItemsFromBlocks();
- }
-
- ready() {
- this.cleanGridItems();
- }
-
- cleanGridItems() {
- // Get all the grid items and remove any non grid item element.
- return [...this.children].filter((child) => child.matches('raqn-grid-item') || child.remove());
- }
-
- async collectGridItemsFromBlocks() {
+ async addEDSHtml() {
if (!this.isInitAsBlock) return;
- await this.recursiveItems(this.nextElementSibling);
- }
+ const elems = [...this.parentElement.children];
- async recursiveItems(elem, children = []) {
- if (!elem) return;
- if (this.isForbiddenGridItem(elem)) return;
- if (this.isForbiddenBlockGrid(elem)) return;
- if (this.isForbiddenRaqnGrid(elem)) return;
+ const gridIndex = elems.indexOf(this);
- if (this.isThisGridItem(elem)) {
- await this.createGridItem([...children], [...elem.classList]);
- await this.recursiveItems(elem.nextElementSibling, []);
- elem.remove();
- return;
- }
+ let children = elems.slice(gridIndex + 1);
- children.push(elem);
-
- await this.recursiveItems(elem.nextElementSibling, children);
- }
+ const lastItem = [...children].reverse().find((el) => el.matches('.grid-item'));
+ const lastItemIndex = children.indexOf(lastItem);
- getLevel(elem = this) {
- return Number(elem.dataset.level);
- }
-
- getLevelFromClass(elem) {
- const levelClass = [...elem.classList].find((cls) => cls.startsWith('data-level-')) || 'data-level-1';
- return Number(levelClass.slice('data-level-'.length));
- }
-
- isGridItem(elem) {
- return elem.tagName === 'DIV' && elem.classList.contains('grid-item');
- }
-
- isThisGridItem(elem) {
- return this.isGridItem(elem) && this.getLevelFromClass(elem) === this.getLevel();
- }
-
- isForbiddenGridItem(elem) {
- return this.isGridItem(elem) && this.getLevelFromClass(elem) > this.getLevel();
- }
-
- isBlockGrid(elem) {
- return elem.tagName === 'DIV' && elem.classList.contains('grid');
- }
-
- isRaqnGrid(elem) {
- return elem.tagName === 'RAQN-GRID';
- }
-
- isForbiddenRaqnGrid(elem) {
- return this.isRaqnGrid(elem) && this.getLevel() >= this.getLevel(elem);
- }
-
- isForbiddenBlockGrid(elem) {
- return this.isBlockGrid(elem) && this.getLevelFromClass(elem) <= this.getLevel();
- }
+ children = children.slice(0, lastItemIndex + 1);
- async createGridItem(children, configByClasses) {
- await component.loadAndDefine('grid-item');
- const tempGridItem = document.createElement('raqn-grid-item');
- tempGridItem.init({ configByClasses });
- tempGridItem.gridParent = this;
- tempGridItem.append(...children);
- this.gridItems.push(tempGridItem);
- this.append(tempGridItem);
+ this.append(...children);
}
}
diff --git a/blocks/header/header.js b/blocks/header/header.js
index 335f228c..1aaa30fa 100644
--- a/blocks/header/header.js
+++ b/blocks/header/header.js
@@ -6,6 +6,8 @@ const metaHeader = getMeta(metaTags.header.metaName);
export default class Header extends ComponentBase {
static loaderConfig = {
...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > header',
+ targetsSelectorsLimit: 1,
loaderStopInit() {
return metaHeader === false;
},
@@ -13,9 +15,7 @@ export default class Header extends ComponentBase {
attributesValues = {
all: {
- class: {
- color: 'primary',
- },
+ class: 'color-primary',
},
};
@@ -27,12 +27,17 @@ export default class Header extends ComponentBase {
return [
...super.extendConfig(),
{
+ contentFromTargets: false,
addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ contentFromTargets: false,
+ },
},
];
}
- connected() {
+ ready() {
eagerImage(this, 1);
}
}
diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js
index 6ad76e7e..cbf360b7 100644
--- a/blocks/icon/icon.js
+++ b/blocks/icon/icon.js
@@ -6,6 +6,14 @@ const metaIcons = getMeta(metaTags.icons.metaName);
export default class Icon extends ComponentBase {
static observedAttributes = ['data-active', 'data-icon'];
+ attributesValues = {
+ all: {
+ attribute: {
+ 'aria-hidden': 'true',
+ },
+ },
+ };
+
#initialIcon = null;
#activeIcon = null;
@@ -52,10 +60,6 @@ export default class Icon extends ComponentBase {
return `${path}/${iconName}.svg`;
}
- async connected() {
- this.setAttribute('aria-hidden', 'true');
- }
-
// ${viewport}-icon-${value} or icon-${value}
applyIcon(icon) {
this.dataset.icon = isObject(icon) ? flatAsValue(icon) : icon;
diff --git a/blocks/image/image.js b/blocks/image/image.js
index 74aa774a..21b89d6c 100644
--- a/blocks/image/image.js
+++ b/blocks/image/image.js
@@ -23,7 +23,7 @@ export default class Image extends ComponentBase {
];
}
- connected() {
+ addHtml() {
this.createLinkedImage();
}
@@ -31,8 +31,9 @@ export default class Image extends ComponentBase {
if (!this.children) return;
const em = this.firstElementChild;
const anchor = em.firstElementChild;
- const pictureParent = this.parentElement.previousElementSibling;
- const picture = pictureParent.firstElementChild;
+ const pictureParent = this.parentElement?.previousElementSibling;
+ const picture = pictureParent?.firstElementChild;
+ if (!picture) return;
anchor.setAttribute('aria-label', anchor.textContent);
anchor.innerHTML = '';
anchor.append(picture);
diff --git a/blocks/popup/popup.js b/blocks/popup/popup.js
index df7515e7..7162a423 100644
--- a/blocks/popup/popup.js
+++ b/blocks/popup/popup.js
@@ -1,6 +1,5 @@
import ComponentBase from '../../scripts/component-base.js';
import {
- globalConfig,
stringToJsVal,
popupState,
focusTrap,
@@ -28,6 +27,7 @@ export default class Popup extends ComponentBase {
return [
...super.extendConfig(),
{
+ innerComponents: '.popup__content > div',
contentFromTargets: false,
targetsAsContainers: {
contentFromTargets: false,
@@ -120,7 +120,7 @@ export default class Popup extends ComponentBase {
});
}
- connected() {
+ ready() {
this.activeOnConnect();
}
@@ -139,11 +139,6 @@ export default class Popup extends ComponentBase {
this.elements.popupContent.innerHTML = await this.fragmentContent;
}
- setInnerBlocks() {
- const innerBlocks = [...this.elements.popupContent.querySelectorAll(globalConfig.blockSelector)];
- this.innerBlocks = innerBlocks;
- }
-
onAttributeUrlChanged({ oldValue, newValue }) {
if (newValue === oldValue) return;
this.fragmentPath = `${newValue}.plain.html`;
diff --git a/blocks/section-metadata/section-metadata.css b/blocks/section-metadata/section-metadata.css
deleted file mode 100644
index 25579c74..00000000
--- a/blocks/section-metadata/section-metadata.css
+++ /dev/null
@@ -1,3 +0,0 @@
-raqn-section-metadata {
- display: none;
-}
diff --git a/blocks/section-metadata/section-metadata.js b/blocks/section-metadata/section-metadata.js
deleted file mode 100644
index 18994efc..00000000
--- a/blocks/section-metadata/section-metadata.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import ComponentBase from '../../scripts/component-base.js';
-import { stringToArray } from '../../scripts/libs.js';
-
-export default class SectionMetadata extends ComponentBase {
- static observedAttributes = ['class'];
-
- extendConfig() {
- return [
- ...super.extendConfig(),
- {
- classes: {
- section: 'section',
- },
- },
- ];
- }
-
- ready() {
- this.parentElement.classList.add(this.config.classes.section, ...this.classList.values());
- }
-
- onAttributeClassChanged({ oldValue, newValue }) {
- if (!this.initialized) return;
- if (oldValue === newValue) return;
-
- const opts = { divider: ' ' };
- this.parentElement.classList.remove(...stringToArray(oldValue, opts));
- this.parentElement.classList.add(...stringToArray(newValue, opts));
- }
-}
diff --git a/blocks/section/section.css b/blocks/section/section.css
new file mode 100644
index 00000000..354a0567
--- /dev/null
+++ b/blocks/section/section.css
@@ -0,0 +1 @@
+/* NOP */
\ No newline at end of file
diff --git a/blocks/section/section.js b/blocks/section/section.js
new file mode 100644
index 00000000..b206fd40
--- /dev/null
+++ b/blocks/section/section.js
@@ -0,0 +1,64 @@
+import component from '../../scripts/init.js';
+import ComponentBase from '../../scripts/component-base.js';
+import { globalConfig } from '../../scripts/libs.js';
+
+export default class Section extends ComponentBase {
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > div',
+ };
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ innerComponents: `:scope > ${globalConfig.blockSelector}`,
+ nestedComponentsPrefix: ':scope > ',
+ },
+ ];
+ }
+
+ buildExternalConfig() {
+ // get values from section metadata block;
+ const meta = this.querySelector(':scope > .section-metadata');
+ if (meta) {
+ meta.remove();
+ this.initOptions.configByClasses = [...meta.classList];
+ }
+ super.buildExternalConfig();
+ }
+
+ setInnerBlocks() {
+ if (!this.config.innerComponents) return;
+
+ const elems = [...this.querySelectorAll(this.config.innerComponents)];
+ const grid = elems.find((el) => el.matches('.grid'));
+ const gridIndex = elems.indexOf(grid);
+ const lastItem = [...elems].reverse().find((el) => el.matches('.grid-item'));
+ const lastItemIndex = elems.indexOf(lastItem);
+ const preGridChildren = [...elems].slice(0, gridIndex + 1);
+ const postGridChildren = [...elems].slice(lastItemIndex + 1);
+
+ this.innerBlocks = [...preGridChildren, ...postGridChildren].map((elem) => component.getBlockData(elem));
+ this.innerGrids = [];
+ }
+
+ addEDSHtml() {
+ const grids = this.querySelectorAll('.grid');
+ if (grids.length > 1) {
+ if (window.raqnIsPreview) {
+ this.innerHTML =
+ '
The content of this section is hidden because it contains more than 1 grid which is not supported. Please fix.
';
+ this.attributesValues.all ??= {};
+ this.attributesValues.all.class ??= '';
+
+ this.attributesValues.all.class += ' error-message';
+ this.classList.add('error-message-box');
+
+ // prevent hiding the component based on the throw error bellow
+ this.config.hideOnInitError = false;
+ }
+ throw new Error('More then 1 grid configured in this section. Please fix.');
+ }
+ }
+}
diff --git a/blocks/sidekick-tools-palette/sidekick-tools-palette.js b/blocks/sidekick-tools-palette/sidekick-tools-palette.js
index 783cb47a..ab51b6d4 100644
--- a/blocks/sidekick-tools-palette/sidekick-tools-palette.js
+++ b/blocks/sidekick-tools-palette/sidekick-tools-palette.js
@@ -376,7 +376,7 @@ export default class SidekickToolsPalette extends ComponentBase {
});
}
- connected() {
+ addHtml() {
this.initPalette();
}
}
diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js
index 6acd93d0..9928e0e0 100644
--- a/blocks/theming/theming.js
+++ b/blocks/theming/theming.js
@@ -13,12 +13,33 @@ import {
const k = Object.keys;
export default class Theming extends ComponentBase {
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ // add the component to the head. Target should be set as `document`
+ targetsSelectors: ':scope > head',
+ targetsSelectorsLimit: 1,
+ };
+
componentsConfig = {};
elements = {};
variations = {};
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ contentFromTargets: false,
+ addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ contentFromTargets: false,
+ },
+ },
+ ];
+ }
+
setDefaults() {
super.setDefaults();
this.scapeDiv = document.createElement('div');
diff --git a/head.html b/head.html
index a0c21054..cd6c22eb 100644
--- a/head.html
+++ b/head.html
@@ -19,4 +19,3 @@
-
diff --git a/scripts/component-base.js b/scripts/component-base.js
index a3b749dd..4cf7d530 100644
--- a/scripts/component-base.js
+++ b/scripts/component-base.js
@@ -1,21 +1,22 @@
import component from './init.js';
import {
- globalConfig,
getBreakPoints,
listenBreakpointChange,
camelCaseAttr,
capitalizeCaseAttr,
deepMerge,
+ deepMergeMethod,
classToFlat,
unFlat,
isObject,
flatAsValue,
flat,
mergeUniqueArrays,
- getBlocksAndGrids,
} from './libs.js';
import { externalConfig } from './libs/external-config.js';
+window.raqnInstances ??= {};
+
export default class ComponentBase extends HTMLElement {
// All supported data attributes must be added to observedAttributes
// The order of observedAttributes is the order in which the values from config are added.
@@ -33,7 +34,7 @@ export default class ComponentBase extends HTMLElement {
};
get Handler() {
- return window.raqnComponents[this.componentName];
+ return window.raqnComponentsHandlers[this.componentName];
}
get isInitAsBlock() {
@@ -43,6 +44,7 @@ export default class ComponentBase extends HTMLElement {
constructor() {
super();
this.setDefaults();
+ this.setInstance();
this.setInitializationPromise();
this.extendConfigRunner({ config: 'config', method: 'extendConfig' });
this.extendConfigRunner({ config: 'nestedComponentsConfig', method: 'extendNestedConfig' });
@@ -53,7 +55,9 @@ export default class ComponentBase extends HTMLElement {
this.uuid = `gen${crypto.randomUUID().split('-')[0]}`;
this.webComponentName = this.tagName.toLowerCase();
this.componentName = this.webComponentName.replace(/^raqn-/, '');
+ this.externalConfig = null;
this.overrideExternalConfig = false;
+ this.category = null;
this.wasInitBeforeConnected = false;
this.fragmentPath = null;
this.fragmentCache = 'default';
@@ -70,9 +74,8 @@ export default class ComponentBase extends HTMLElement {
// from inner html blocks
innerGrids: [],
};
- // set only if content is loaded externally
+ this.initializeInnerBlocks = true;
this.innerBlocks = [];
- // set only if content is loaded externally
this.innerGrids = [];
this.initError = null;
this.breakpoints = getBreakPoints();
@@ -80,7 +83,8 @@ export default class ComponentBase extends HTMLElement {
// use the this.extendConfig() method to extend the default config
this.config = {
- listenBreakpoints: false,
+ innerComponents: undefined,
+ nestedComponentsPrefix: ':scope > ',
hideOnInitError: true,
hideOnChildrenError: false,
addToTargetMethod: 'replaceWith',
@@ -100,6 +104,20 @@ export default class ComponentBase extends HTMLElement {
componentName: 'button',
},
};
+
+ this.attrMerge = {
+ '**.class': (a, b) => {
+ const haveLength = [a, b].every((c) => c?.length);
+
+ if (b && typeof a === 'string' && typeof b !== 'string') {
+ // eslint-disable-next-line no-console
+ console.error('Merge for css classes in attributeValues failed. Values are not strings');
+ return a;
+ }
+
+ return haveLength ? `${a} ${b}` : b;
+ },
+ };
}
setInitializationPromise() {
@@ -114,6 +132,11 @@ export default class ComponentBase extends HTMLElement {
// const { promise, resolve, reject } = Promise.withResolvers();
}
+ setInstance() {
+ window.raqnInstances[this.componentName] ??= [];
+ window.raqnInstances[this.componentName].push(this);
+ }
+
// Using the `method` which returns an array of objects it's easier to extend
// configs when the components are deeply extended with multiple levels of inheritance;
extendConfigRunner({ config, method }) {
@@ -153,6 +176,7 @@ export default class ComponentBase extends HTMLElement {
await this.Handler;
this.wasInitBeforeConnected = true;
this.initOptions = initOptions || {};
+ this.config = deepMerge(this.config, this.initOptions.componentConfig);
this.setInitialAttributesValues();
await this.buildExternalConfig();
this.runConfigsByViewport();
@@ -176,13 +200,16 @@ export default class ComponentBase extends HTMLElement {
* use the data attr values as default for attributesValues
*/
setInitialAttributesValues() {
- const initialAttributesValues = { all: { data: {} } };
+ const initialAttributesValues = {};
+
+ this.externalConfigName = this.getAttribute('config-name') || this.initOptions.externalConfigName;
this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => {
const value = this.dataset[noDataCamelCase];
if (typeof value === 'undefined') return {};
const initialValue = unFlat({ [noData]: value });
+ initialAttributesValues.all ??= { data: {} };
initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue);
return initialAttributesValues;
});
@@ -209,7 +236,7 @@ export default class ComponentBase extends HTMLElement {
// Build-in method called after the element is added to the DOM.
async connectedCallback() {
// Common identifier for raqn web components
- this.setAttribute('raqnWebComponent', '');
+ this.setAttribute('raqnwebcomponent', '');
this.setAttribute('isloading', '');
try {
this.initialized = this.getAttribute('initialized');
@@ -219,8 +246,14 @@ export default class ComponentBase extends HTMLElement {
this.setAttribute('id', this.uuid);
this.loadDependencies(); // do not wait for dependencies;
await this.loadFragment(this.fragmentPath);
- await this.connected(); // manipulate/create the html
+ // Add, create and manipulate only html containing EDS blocks/markup
+ // ! any element with a class will considered a block and transformed to a webComponent
+ await this.addEDSHtml();
+ this.setInnerBlocks();
await this.initChildComponents();
+ // Add, create and manipulate html after inner webComponents were initialized.
+ // Here normal html or webComponent can be created and added to the component.
+ await this.addHtml();
this.addListeners(); // html is ready add listeners
await this.ready(); // add extra functionality
this.setAttribute('initialized', true);
@@ -254,22 +287,51 @@ export default class ComponentBase extends HTMLElement {
}
async buildExternalConfig() {
- let configByClasses = mergeUniqueArrays(this.initOptions.configByClasses, this.classList);
+ let configByClasses = mergeUniqueArrays(
+ this.initOptions.configByClasses?.filter((c, index) => c.includes('-') && index !== 0),
+ [...this.classList].map((c) => `all-class-${c}`),
+ );
+
// normalize the configByClasses to serializable format
- const { byName } = getBreakPoints();
+ const { byName } = this.breakpoints;
configByClasses = configByClasses
// remove the first class which is the component name and keep only compound classes
- .filter((c, index) => c.includes('-') && index !== 0)
+ // .filter((c, index) => c.includes('-') && index !== 0)
// make sure break points are included in the config
.map((c) => {
- const exceptions = ['all', 'config'];
+ const breakpoints = ['all', 'config', ...Object.keys(byName)];
const firstClass = c.split('-')[0];
- const isBreakpoint = Object.keys(byName).includes(firstClass) || exceptions.includes(firstClass);
+ const isBreakpoint = breakpoints.includes(firstClass);
return isBreakpoint ? c : `all-${c}`;
});
+ const classesAndAttr = configByClasses.reduce(
+ (acc, c) => {
+ const breakpoints = ['all', ...Object.keys(byName)];
+ const classBreakpoint = breakpoints.find((b) => c.startsWith(`${b}-class-`));
+
+ if (c.startsWith('config-')) {
+ // allows to have external config names with hyphens.
+ acc.externalConfigName = c.slice('config-'.length);
+ }
+
+ if (classBreakpoint) {
+ acc.classes[classBreakpoint] ??= {};
+ const currentClasses = acc.classes[classBreakpoint].class;
+ const cls = c.slice(`${classBreakpoint}-class-`.length);
+ acc.classes[classBreakpoint].class = currentClasses?.length ? `${currentClasses} ${cls}` : cls;
+ } else {
+ acc.attrs.push(c);
+ }
+ return acc;
+ },
+ { classes: {}, attrs: [], externalConfigName: null },
+ );
+
// serialize the configByClasses into a flat object
- let values = classToFlat(configByClasses);
+ let values = deepMerge({}, classesAndAttr.classes, classToFlat(classesAndAttr.attrs));
+
+ this.externalConfigName ??= classesAndAttr.externalConfigName;
// get the external config
// TODO With the unFlatten approach of setting this.attributesValues there is an increased amount of processing
@@ -279,7 +341,7 @@ export default class ComponentBase extends HTMLElement {
// - data - as flatten values with camel case keys
// - attributes - as flatten values with hyphen separated keys.
// for anything else set them flatten as they come from from external config
- const configs = unFlat(await externalConfig.getConfig(this.componentName, values.config));
+ const configs = unFlat(await externalConfig.getConfig(this.componentName, this.externalConfigName, this.category));
if (this.overrideExternalConfig) {
// Used for preview functionality
@@ -288,8 +350,6 @@ export default class ComponentBase extends HTMLElement {
values = deepMerge({}, configs, values);
}
- delete values.config;
-
// add to attributesValues
this.attributesValues = deepMerge({}, this.attributesValues, values);
}
@@ -334,9 +394,13 @@ export default class ComponentBase extends HTMLElement {
}
}
+ get currentAttributesValues() {
+ const { name } = this.breakpoints.active;
+ return deepMergeMethod(this.attrMerge, {}, this.attributesValues.all, this.attributesValues[name]);
+ }
+
runConfigsByViewport() {
- const { name } = getBreakPoints().active;
- const current = deepMerge({}, this.attributesValues.all, this.attributesValues[name]);
+ const current = this.currentAttributesValues;
this.removeAttribute('class');
Object.keys(current).forEach((key) => {
const action = `apply${key.charAt(0).toUpperCase() + key.slice(1)}`;
@@ -375,7 +439,7 @@ export default class ComponentBase extends HTMLElement {
this.classList.add(...flatAsValue(className).split(' '));
} else if (className) {
// strings are added as is
- this.setAttribute('class', className);
+ this.classList.add(...className.split(' '));
}
}
@@ -391,18 +455,10 @@ export default class ComponentBase extends HTMLElement {
});
}
- // ${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);
- });
+ applySetting(config) {
+ // delete the setting to run only once on init
+ delete this.attributesValues.all.setting;
+ deepMerge(this.config, config);
}
/**
@@ -426,9 +482,9 @@ export default class ComponentBase extends HTMLElement {
getBreakpointAttrVal(attr) {
const { name: activeBrName } = this.breakpoints.active;
- const attribute = this.attributesValues?.[attr];
- if (!attribute) return undefined;
- return attribute?.[activeBrName] ?? attribute?.all;
+ const attributeAll = this.attributesValues?.all?.[attr];
+ const attributeBreakpoint = this.attributesValues?.[activeBrName]?.[attr];
+ return attributeBreakpoint ?? attributeAll;
}
addListeners() {
@@ -439,7 +495,7 @@ export default class ComponentBase extends HTMLElement {
async initChildComponents() {
await Promise.allSettled([this.initNestedComponents(), this.initInnerBlocks()]);
- await this.initInnerGrids();
+ // await this.initInnerGrids();
}
async initNestedComponents() {
@@ -450,7 +506,7 @@ export default class ComponentBase extends HTMLElement {
? deepMerge({}, setting, {
// Exclude nested components query from innerBlocks. Inner Components will query their own nested components.
loaderConfig: {
- targetsSelectorsPrefix: ':scope > div >', // Limit only to default content, exclude blocks.
+ targetsSelectorsPrefix: this.config.nestedComponentsPrefix, // Limit only to default content, exclude blocks.
},
})
: setting;
@@ -502,7 +558,9 @@ export default class ComponentBase extends HTMLElement {
if (response.ok) {
this.fragmentContent = response.text();
await this.addFragmentContent();
- this.setInnerBlocksAndGrids();
+ // When html is loaded externally it will contain sections and blocks
+ // Initialize inner components
+ this.config.innerComponents ??= ':scope > div';
}
}
@@ -510,13 +568,15 @@ export default class ComponentBase extends HTMLElement {
this.innerHTML = await this.fragmentContent;
}
- // Set only if content is loaded externally;
- setInnerBlocksAndGrids() {
- const { blocks, grids } = getBlocksAndGrids(
- [...this.querySelectorAll(globalConfig.blockSelector)].map((elem) => component.getBlockData(elem)),
+ setInnerBlocks() {
+ if (!this.config.innerComponents) return;
+ // const { blocks, grids } =
+ // ;
+
+ this.innerBlocks = [...this.querySelectorAll(this.config.innerComponents)].map((elem) =>
+ component.getBlockData(elem),
);
- this.innerBlocks = blocks;
- this.innerGrids = grids;
+ // this.innerGrids = grids;
}
queryElements() {
@@ -543,11 +603,17 @@ export default class ComponentBase extends HTMLElement {
initSubscriptions() {}
+ removeSubscriptions() {}
+
onInit() {}
- connected() {}
+ addEDSHtml() {}
+
+ addHtml() {}
ready() {}
- disconnectedCallback() {}
+ disconnectedCallback() {
+ this.removeSubscriptions();
+ }
}
diff --git a/scripts/component-loader.js b/scripts/component-loader.js
index 3d935868..f7bf7395 100644
--- a/scripts/component-loader.js
+++ b/scripts/component-loader.js
@@ -1,7 +1,5 @@
import { loadModule, deepMerge, mergeUniqueArrays, getBreakPoints } from './libs.js';
-window.raqnInstances = window.raqnInstances || {};
-
export default class ComponentLoader {
constructor({
componentName,
@@ -14,12 +12,12 @@ export default class ComponentLoader {
props,
nestedComponentsConfig,
active,
+ path,
}) {
- window.raqnComponents ??= {};
+ window.raqnComponentsHandlers ??= {};
if (!componentName) {
throw new Error('`componentName` is required');
}
- this.instances = window.raqnInstances || {};
this.componentName = componentName;
this.targets = targets.map((target) => ({ target }));
this.loaderConfig = loaderConfig;
@@ -29,7 +27,8 @@ export default class ComponentLoader {
this.breakpoints = getBreakPoints();
this.componentConfig = componentConfig;
this.nestedComponentsConfig = nestedComponentsConfig;
- this.pathWithoutExtension = `/blocks/${this.componentName}/${this.componentName}`;
+ this.path = path || '/blocks';
+ this.pathWithoutExtension = `${this.path}/${this.componentName}/${this.componentName}`;
this.props = props ?? {};
this.isWebComponent = null;
this.isClass = null;
@@ -38,11 +37,11 @@ export default class ComponentLoader {
}
get Handler() {
- return window.raqnComponents[this.componentName];
+ return window.raqnComponentsHandlers[this.componentName];
}
set Handler(handler) {
- window.raqnComponents[this.componentName] = handler;
+ window.raqnComponentsHandlers[this.componentName] = handler;
}
setHandlerType(handler = this.Handler) {
@@ -90,9 +89,6 @@ 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/init.js b/scripts/init.js
index fd905986..797d8846 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -4,9 +4,6 @@ import {
metaTags,
eagerImage,
getMeta,
- getMetaGroup,
- mergeUniqueArrays,
- getBlocksAndGrids,
} from './libs.js';
const component = {
@@ -90,127 +87,29 @@ const component = {
const lcp = block.classList.contains('lcp');
let componentName = tagName;
if (!globalConfig.semanticBlocks.includes(tagName)) {
- componentName = block.classList.item(0);
+ componentName = block.classList.item(0) || 'section';
}
return { targets: [block], componentName, lcp };
},
};
export const onLoadComponents = {
- // default content
- staticStructureComponents: [
- {
- componentName: 'image',
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- targetsSelectorsPrefix: 'main > div >',
- },
- },
- {
- componentName: 'button',
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- targetsSelectorsPrefix: 'main > div >',
- },
- },
- ],
-
async init() {
- this.setLcp();
- this.setStructure();
- this.queryAllBlocks();
- this.setBlocksData();
- this.setLcpBlocks();
- this.setLazyBlocks();
- this.initBlocks();
- },
-
- queryAllBlocks() {
- this.blocks = [
- document.body.querySelector(globalConfig.semanticBlocks[0]),
- ...document.querySelectorAll(globalConfig.blockSelector),
- ...document.body.querySelectorAll(globalConfig.semanticBlocks.slice(1).join(',')),
- ];
- },
-
- setBlocksData() {
- const structureData = this.structureComponents.map(({ componentName }) => ({
- componentName,
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- },
- }));
- structureData.push(...this.staticStructureComponents);
-
- const blocksData = this.blocks.map((block) => component.getBlockData(block));
- this.blocksData = [...structureData, ...blocksData];
- },
-
- setLcp() {
- const { metaName, fallbackContent } = metaTags.lcp;
- const lcpMeta = getMeta(metaName, { getArray: true });
- const defaultLcp = fallbackContent;
- const lcp = lcpMeta?.length ? lcpMeta : defaultLcp;
- // theming must be in LCP to prevent CLS
- this.lcp = mergeUniqueArrays(lcp, ['theming']).map((componentName) => ({
- componentName: componentName.trim(),
- }));
- },
-
- setStructure() {
- const structureComponents = getMetaGroup(metaTags.structure.metaNamePrefix, { getFallback: false });
- this.structureComponents = structureComponents.flatMap(({ name, content }) => {
- if (content !== true) return [];
- return {
- componentName: name.trim(),
- };
- });
const template = getMeta(metaTags.template.metaName);
- if(template) {
- this.structureComponents = [...this.structureComponents, {
- componentName: template,
- }];
- }
- },
-
- setLcpBlocks() {
- this.lcpBlocks = this.blocksData.filter((data) => !!this.findLcp(data));
- },
-
- setLazyBlocks() {
- const allLazy = this.blocksData.filter((data) => !this.findLcp(data));
- const { grids, blocks } = getBlocksAndGrids(allLazy);
-
- this.lazyBlocks = blocks;
- this.grids = grids;
- },
+ const templateConfig = getMeta(metaTags.templateConfig.metaName);
- findLcp(data) {
- return (
- this.lcp.find(({ componentName }) => componentName === data.componentName) || data.lcp /* ||
- [...document.querySelectorAll('main > div > [class]:nth-child(-n+1)')].find((el) => el === data?.targets?.[0]) */
- );
- },
-
- async initBlocks() {
- // Keep the page hidden until specific components are initialized to prevent CLS
- component.multiInit(this.lcpBlocks).then(() => {
- window.postMessage({ message: 'raqn:components:loaded' });
- document.body.style.setProperty('display', 'block');
+ component.init({
+ componentName: template,
+ path: '/templates',
+ externalConfigName: templateConfig,
+ targets: [document.querySelector('body > main')],
});
-
- await component.multiInit(this.lazyBlocks);
- // grids must be initialized sequentially starting from the deepest level.
- // all the blocks that will be contained by the grids must be already initialized before they are added to the grids.
- component.multiSequentialInit(this.grids);
},
};
export const globalInit = {
async init() {
+ this.isPreview();
this.setLang();
this.initEagerImages();
onLoadComponents.init();
@@ -228,6 +127,13 @@ export const globalInit = {
eagerImage(document.body, length);
}
},
+
+ isPreview() {
+ const { hostname } = window.location;
+ const previewHosts = ['localhost', '.aem.page'];
+
+ window.raqnIsPreview = previewHosts.some((host) => hostname.endsWith(host));
+ },
};
globalInit.init();
diff --git a/scripts/libs.js b/scripts/libs.js
index ecd0b011..e3aec77a 100644
--- a/scripts/libs.js
+++ b/scripts/libs.js
@@ -2,9 +2,10 @@ export const globalConfig = {
semanticBlocks: ['header', 'footer'],
blockSelector: `
[class]:not(
+ .section-metadata,
+ [raqnwebcomponent],
style,
- [class^="config-" i],
- [class^="grid-item" i]
+ [class^="config-" i]
)`,
breakpoints: {
xs: 0,
@@ -50,8 +51,13 @@ export const metaTags = {
},
template: {
metaName: 'template',
+ fallbackContent: 'template',
// contentType: 'string template name',
},
+ templateConfig: {
+ metaName: 'template-config',
+ // contentType: 'string template config name',
+ },
lcp: {
metaName: 'lcp',
fallbackContent: ['theming', 'header', 'breadcrumbs'],
@@ -319,6 +325,80 @@ export function deepMerge(origin, ...toMerge) {
return deepMerge(origin, ...toMerge);
}
+/**
+ * Helps handle the merging of non object prop values like string or arrays
+ * by providing a key path and a method which defines how to handle the merge.
+ * @example
+ * keyPathMethods: {
+ * '**.*': (a, b) => {}, // matches any key at any level. As an example by using a,b type checks can define general merging handling or all arrays properties.
+ * '**.class': (a, b) => {} // matches class key at any level
+ * '**.class|classes': (a, b) => {} // matches class or classes keys at any level
+ * '**.all|xs.class': (a, b) => {} // matches all.class or xs.class key paths at any level of nesting
+ * 'all.class': (a, b) => {} // matches the exact key path nesting
+ * 'all.class': (a, b) => {} // matches the exact key path nesting
+ * 'all|xs.*.settings|config.*.class': (a, b) => { // matches class key at 5th level of nesting where '*' can be any
+ * } // key and first level is all and 3rd level si settings
+ * '*.*.*.class': (a, b) => {} // matches class key at 4th level of nesting where '*' can be any key
+ * '*.*.*.class': (a, b) => {} // matches class key at 4th level of nesting where '*' can be any key
+ * }
+ */
+export function deepMergeMethod(keyPathMethods, origin, ...toMerge) {
+ if (!toMerge.length) return origin;
+ const merge = toMerge.shift();
+ const pathsArrays =
+ keyPathMethods?.pathsArrays ||
+ Object.entries(keyPathMethods).flatMap(([key, method]) => {
+ if (key === 'currentPath') return [];
+ return [[key.split('.').map((k) => k.split('|')), method]];
+ });
+ const { currentPath = [] } = keyPathMethods;
+
+ if (isOnlyObject(origin) && isOnlyObject(merge)) {
+ Object.keys(merge).forEach((key) => {
+ const localPath = [...currentPath, key];
+
+ if (isOnlyObject(merge[key])) {
+ const noKeyInOrigin = !origin[key];
+ // overwrite origin non object values with objects
+ const overwriteOriginWithObject = !isOnlyObject(origin[key]) && isOnlyObject(merge[key]);
+ if (noKeyInOrigin || overwriteOriginWithObject) {
+ Object.assign(origin, { [key]: {} });
+ }
+ deepMergeMethod({ pathsArrays, currentPath: localPath }, origin[key], merge[key]);
+ } else {
+ const extendByBath =
+ !!pathsArrays.length &&
+ pathsArrays.some(([keyPathPattern, method]) => {
+ const keyPathCheck = [...keyPathPattern];
+ const localPathCheck = [...localPath];
+
+ if (keyPathCheck.at(0).at(0) === '**') {
+ keyPathCheck.shift();
+ if (localPathCheck.length > keyPathCheck.length) {
+ localPathCheck.splice(0, localPathCheck.length - keyPathCheck.length);
+ }
+ }
+
+ if (localPathCheck.length !== keyPathCheck.length) return false;
+
+ const isPathMatch = localPathCheck.every((k, i) =>
+ keyPathCheck[i].some((check) => k === check || check === '*'),
+ );
+ if (!isPathMatch) return false;
+ Object.assign(origin, { [key]: method(origin[key], merge[key]) });
+ return true;
+ });
+
+ if (!extendByBath) {
+ Object.assign(origin, { [key]: merge[key] });
+ }
+ }
+ });
+ }
+
+ return deepMergeMethod({ pathsArrays, currentPath }, origin, ...toMerge);
+}
+
export function loadModule(urlWithoutExtension, loadCSS = true) {
try {
const js = import(`${urlWithoutExtension}.js`);
@@ -327,9 +407,15 @@ export function loadModule(urlWithoutExtension, loadCSS = true) {
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;
+ // make the css loading not be render blocking
+ link.rel = 'preload';
+ link.as = 'style';
+ link.onload = () => {
+ link.onload = null;
+ link.rel = 'stylesheet';
+ resolve();
+ };
link.onerror = reject;
document.head.append(link);
} else {
@@ -498,19 +584,22 @@ export function flatAsClasses(data, sep = '-') {
*
* @param {Object} obj - Object to unflatten
* */
-
-export function unFlat(f, sep = '-') {
+export function unFlat(f, settings = {}) {
+ const { separatorAliases = [['+', '-']], keySeparator = '-' } = settings;
const un = {};
// for each key create objects
Object.keys(f).forEach((key) => {
- const properties = key.split(sep);
+ const properties = key.split(keySeparator);
const value = f[key];
+
properties.reduce((unflating, prop, i) => {
- if (!unflating[prop]) {
- const step = i < properties.length - 1 ? { [prop]: {} } : { [prop]: value };
+ const newProp = separatorAliases.reduce((acc, p) => acc.replaceAll(...p), prop);
+ if (!unflating[newProp]) {
+ const step = i < properties.length - 1 ? { [newProp]: {} } : { [newProp]: value };
Object.assign(unflating, step);
}
- return unflating[prop];
+
+ return unflating[newProp];
}, un);
});
return un;
diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js
index ad494878..54ab103f 100644
--- a/scripts/libs/external-config.js
+++ b/scripts/libs/external-config.js
@@ -4,10 +4,12 @@ window.raqnComponentsMasterConfig = window.raqnComponentsMasterConfig || null;
// eslint-disable-next-line import/prefer-default-export
export const externalConfig = {
- async getConfig(componentName, configName = 'default') {
+ async getConfig(componentName, configName, category = '') {
+ const defaultConfig = configName ?? 'default';
+ const componentNameCategory = `${category ? `${category}-` : ''}${componentName}`;
window.raqnComponentsMasterConfig ??= await this.loadConfig();
- const componentConfig = window.raqnComponentsMasterConfig?.[componentName];
- const parsedConfig = componentConfig?.[configName];
+ const componentConfig = window.raqnComponentsMasterConfig?.[componentNameCategory];
+ const parsedConfig = componentConfig?.[defaultConfig];
// return copy of object to prevent mutation of raqnComponentsMasterConfig;
if (parsedConfig) return deepMerge({}, parsedConfig);
diff --git a/styles/styles.css b/styles/styles.css
index afb13051..5cef2d78 100644
--- a/styles/styles.css
+++ b/styles/styles.css
@@ -188,7 +188,7 @@ img {
/* Set default block style to all raqn web components
Use :where() to give lower specificity in order to not overwrite any display option set on the web component tag
*/
-:where([raqnWebComponent]) {
+:where([raqnwebcomponent]) {
display: block;
}
@@ -253,6 +253,18 @@ button {
pointer-events: none;
}
+.error-message-box {
+ background: red;
+ padding-block: 20px;
+ padding-inline: 20px;
+ border: 5px solid #000;
+ margin-block: 20px;
+
+ & * {
+ color: white;
+ }
+}
+
#franklin-svg-sprite {
display: none;
}
diff --git a/templates/dynamic-sidebar/dynamic-sidebar.css b/templates/dynamic-sidebar/dynamic-sidebar.css
new file mode 100644
index 00000000..56e268dc
--- /dev/null
+++ b/templates/dynamic-sidebar/dynamic-sidebar.css
@@ -0,0 +1,3 @@
+:where(raqn-header, raqn-footer) > raqn-section > *:not(.full-width) {
+ margin-inline: var(--container-width);
+}
\ No newline at end of file
diff --git a/templates/dynamic-sidebar/dynamic-sidebar.js b/templates/dynamic-sidebar/dynamic-sidebar.js
new file mode 100644
index 00000000..b44ed982
--- /dev/null
+++ b/templates/dynamic-sidebar/dynamic-sidebar.js
@@ -0,0 +1,227 @@
+import Grid from '../../blocks/grid/grid.js';
+import ComponentBase from '../../scripts/component-base.js';
+import component from '../../scripts/init.js';
+import { globalConfig, metaTags, getMetaGroup, stringToArray, camelCaseAttr } from '../../scripts/libs.js';
+
+export default class DynamicSidebar extends ComponentBase {
+ static observedAttributes = [...Grid.observedAttributes];
+
+ #templateColumns = ['template-sidebar-one', 'template-main', 'template-sidebar-two'];
+
+ attributesValues = {
+ all: {
+ templateColumns: 'template-sidebar-one, template-main, template-sidebar-two',
+ },
+ l: {
+ templateColumns: 'template-sidebar-one, template-main, template-sidebar-two',
+ },
+ m: {
+ templateColumns: 'template-main, template-sidebar-one, template-sidebar-two',
+ },
+ grid: {
+ all: {
+ class: 'full-width',
+ data: {
+ columns: '300px 1fr',
+ },
+ },
+ m: {
+ columns: '1fr',
+ },
+ s: {
+ columns: '1fr',
+ },
+ xs: {
+ columns: '1fr',
+ },
+ },
+ templateTwo: {},
+ templateMain: {},
+ templateOne: {},
+ };
+
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > main',
+ };
+
+ nestedComponentsConfig = {};
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ innerComponents: `
+ .template-sidebar-one ${globalConfig.blockSelector},
+ .template-main > div,
+ .template-sidebar-two ${globalConfig.blockSelector}`,
+ addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ },
+ structureComponents: '',
+ structureAddToTargetMethod: 'append',
+ structure: {
+ breadcrumbs: {
+ targetsSelectors: 'main',
+ },
+ },
+ },
+ ];
+ }
+
+ setDefaults() {
+ super.setDefaults();
+ this.category = 'template';
+ }
+
+ async onInit() {
+ this.initLCP();
+ }
+
+ async addEDSHtml() {
+ await this.createTemplateGrid();
+ }
+
+ async addHtml() {
+ await this.initStructure();
+ }
+
+ ready() {
+ this.initFooter();
+ }
+
+ initLCP() {
+ component
+ .multiInit([
+ {
+ componentName: 'theming',
+ targets: [document.head],
+ loaderConfig: {
+ addToTargetMethod: 'append',
+ },
+ },
+ {
+ componentName: 'header',
+ targets: [document.body],
+ loaderConfig: {
+ targetsSelectors: ':scope > header',
+ targetsAsContainers: true,
+ },
+ },
+ ])
+ .then(() => {
+ window.postMessage({ message: 'raqn:components:loaded' });
+ document.body.style.setProperty('display', 'block');
+ });
+ }
+
+ async createTemplateGrid() {
+ await component.multiLoadAndDefine(['grid', 'grid-item']);
+ const content = [...this.children];
+ const contentByItem = content.reduce(
+ (acc, item) => {
+ const meta = item.querySelector(':scope > .section-metadata');
+ if (!meta) {
+ acc.templateMain.push(item);
+ return acc;
+ };
+
+ const allowedCols = this.#templateColumns.find(
+ (tc) => meta.classList.contains(tc) && (meta.classList.remove(tc) || true),
+ );
+
+ if (allowedCols) {
+ acc[camelCaseAttr(allowedCols)].push(item);
+ } else {
+ acc.templateMain.push(item);
+ }
+ return acc;
+ },
+ { templateSidebarOne: [], templateSidebarTwo: [], templateMain: [] },
+ );
+
+ this.innerHTML = '';
+ this.tplGrid = document.createElement('raqn-grid');
+ this.tplGrid.attributesValues = this.attributesValues.grid;
+ this.tplGrid.config.innerComponents = null;
+
+ const itemsTpl = stringToArray(this.currentAttributesValues.templateColumns).filter((col) =>
+ this.#templateColumns.includes(col),
+ );
+ const itemElemsItems = itemsTpl.map((item) => this.createGridItems(item));
+
+ this.append(this.tplGrid);
+ await this.tplGrid.initialization;
+
+ this.tplGrid.append(...itemElemsItems);
+
+ // To prevent errors initialization needs to be ready before appending content to new elements.
+ await Promise.allSettled(Object.keys(contentByItem).map((c) => this[c]?.initialization));
+ Object.keys(contentByItem).forEach((c) => this[c] && this[c].append(...contentByItem[c]));
+ }
+
+ createGridItems(item) {
+ const camelCaseItem = camelCaseAttr(item);
+ this[camelCaseItem] = document.createElement('raqn-grid-item');
+ this[camelCaseItem].classList.add(item);
+ this[camelCaseItem].attributesValues = this.attributesValues[camelCaseItem];
+ this[camelCaseItem].config.innerComponents = null;
+
+ return this[camelCaseItem];
+ }
+
+ applyTemplateColumns(val) {
+ if (!this.initialized) return;
+ const itemsTpl = stringToArray(val).filter((col) => this.#templateColumns.includes(col));
+ const itemElemsItems = itemsTpl.map((itemTpl) =>
+ [...this.tplGrid.children].find((item) => item.matches(`.${itemTpl}`)),
+ );
+ this.tplGrid.append(...itemElemsItems);
+ }
+
+ async initStructure() {
+ let structureComponents = getMetaGroup(metaTags.structure.metaNamePrefix, { getFallback: false });
+ const localStructure = stringToArray(this.config.structureComponents).flatMap((c) => {
+ const name = c.trim();
+ const structure = structureComponents.find((sc) => sc.name === name);
+ if (structure) return structure;
+ return { name, content: true };
+ });
+ structureComponents = structureComponents.filter((c) => !localStructure.some((lc) => c.name === lc.name));
+ structureComponents = [...localStructure, ...structureComponents];
+
+ this.structureComponents = structureComponents.flatMap(({ name, content }) => {
+ if (content !== true) return [];
+ const { structureAddToTargetMethod } = this.config;
+ const { targetsSelectors, addToTargetMethod } = this.config.structure[name] || {};
+
+ return {
+ componentName: name.trim(),
+ targets: [document],
+ loaderConfig: {
+ ...(targetsSelectors && { targetsSelectors }),
+ targetsAsContainers: true,
+ },
+ componentConfig: {
+ targetsAsContainers: {
+ addToTargetMethod: addToTargetMethod || structureAddToTargetMethod,
+ },
+ },
+ };
+ });
+
+ await component.multiInit(this.structureComponents);
+ }
+
+ initFooter() {
+ return component.init({
+ componentName: 'footer',
+ targets: [document.body],
+ loaderConfig: {
+ targetsSelectors: ':scope > footer',
+ targetsAsContainers: true,
+ },
+ });
+ }
+}
diff --git a/templates/template/template.css b/templates/template/template.css
new file mode 100644
index 00000000..627a8f12
--- /dev/null
+++ b/templates/template/template.css
@@ -0,0 +1,3 @@
+:where(raqn-template, raqn-header, raqn-footer) > raqn-section > *:not(.full-width) {
+ margin-inline: var(--container-width);
+}
diff --git a/templates/template/template.js b/templates/template/template.js
new file mode 100644
index 00000000..9fa9b6f7
--- /dev/null
+++ b/templates/template/template.js
@@ -0,0 +1,117 @@
+import ComponentBase from '../../scripts/component-base.js';
+import component from '../../scripts/init.js';
+import { metaTags, getMetaGroup, stringToArray } from '../../scripts/libs.js';
+
+export default class Template extends ComponentBase {
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > main',
+ };
+
+ nestedComponentsConfig = {};
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ // only init EDS sections
+ innerComponents: ':scope > div',
+ addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ },
+ structureComponents: '',
+ structureAddToTargetMethod: 'prepend',
+ structure: {
+ breadcrumbs: {
+ targetsSelectors: 'main',
+ },
+ },
+ },
+ ];
+ }
+
+ setDefaults() {
+ super.setDefaults();
+ this.category = 'template';
+ }
+
+ async onInit() {
+ this.initLCP();
+ await this.initStructure();
+ }
+
+ initLCP() {
+ component
+ .multiInit([
+ {
+ componentName: 'theming',
+ targets: [document.head],
+ loaderConfig: {
+ addToTargetMethod: 'append',
+ },
+ },
+ {
+ componentName: 'header',
+ targets: [document.body],
+ loaderConfig: {
+ targetsSelectors: ':scope > header',
+ targetsAsContainers: true,
+ },
+ },
+ ])
+ .then(() => {
+ window.postMessage({ message: 'raqn:components:loaded' });
+ document.body.style.setProperty('display', 'block');
+ });
+ }
+
+ async initStructure() {
+ let structureComponents = getMetaGroup(metaTags.structure.metaNamePrefix, { getFallback: false });
+ const localStructure = stringToArray(this.config.structureComponents).flatMap((c) => {
+ const name = c.trim();
+ const structure = structureComponents.find((sc) => sc.name === name);
+ if (structure) return structure;
+ return { name, content: true };
+ });
+ structureComponents = structureComponents.filter((c) => !localStructure.some((lc) => c.name === lc.name));
+ structureComponents = [...localStructure, ...structureComponents];
+
+ this.structureComponents = structureComponents.flatMap(({ name, content }) => {
+ if (content !== true) return [];
+ const { structureAddToTargetMethod } = this.config;
+ const { targetsSelectors, addToTargetMethod } = this.config.structure[name] || {};
+
+ return {
+ componentName: name.trim(),
+ targets: [document],
+ loaderConfig: {
+ ...(targetsSelectors && { targetsSelectors }),
+ targetsAsContainers: true,
+ },
+ componentConfig: {
+ targetsAsContainers: {
+ addToTargetMethod: addToTargetMethod || structureAddToTargetMethod,
+ },
+ },
+ };
+ });
+
+ await component.multiInit(this.structureComponents);
+ }
+
+ initFooter() {
+ return component.init({
+ componentName: 'footer',
+ targets: [document.body],
+ loaderConfig: {
+ targetsSelectors: ':scope > footer',
+ targetsAsContainers: true,
+ },
+ });
+ }
+
+ ready() {
+ this.initFooter();
+ }
+}