diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js
index 2cc41191..1705e08a 100644
--- a/blocks/accordion/accordion.js
+++ b/blocks/accordion/accordion.js
@@ -3,6 +3,20 @@ import ComponentBase from '../../scripts/component-base.js';
export default class Accordion extends ComponentBase {
dependencies = ['icon'];
+ extendNestedConfig() {
+ return [
+ ...super.extendNestedConfig(),
+ {
+ button: {
+ componentName: 'button',
+ loaderConfig: {
+ targetsSelectorsPrefix: ':scope > :is(:nth-child(even)) >',
+ },
+ },
+ },
+ ];
+ }
+
ready() {
this.setAttribute('role', 'navigation');
let children = Array.from(this.children);
@@ -15,7 +29,7 @@ export default class Accordion extends ComponentBase {
}
return child;
});
- // console.log(children)
+
this.setupControls(children.filter((_, ind) => ind % 2 === 0));
this.setupContent(children.filter((_, ind) => ind % 2 === 1));
}
diff --git a/blocks/breadcrumbs/breadcrumbs.js b/blocks/breadcrumbs/breadcrumbs.js
index fe3b3083..a6c49918 100644
--- a/blocks/breadcrumbs/breadcrumbs.js
+++ b/blocks/breadcrumbs/breadcrumbs.js
@@ -1,17 +1,33 @@
import ComponentBase from '../../scripts/component-base.js';
+import { getBaseUrl } from '../../scripts/libs.js';
-export default class BreadCrumbs extends ComponentBase {
- capitalize(string) {
- return string
- .split('-')
- .map((str) => str.charAt(0).toUpperCase() + str.slice(1))
- .join(' ');
+export default class Breadcrumbs extends ComponentBase {
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: 'main > div',
+ targetsSelectorsLimit: 1,
+ targetsAsContainers: true,
+ };
+
+ nestedComponentsConfig = {};
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ contentFromTargets: false,
+ addToTargetMethod: 'replaceWith',
+ targetsAsContainers: {
+ addToTargetMethod: 'prepend',
+ },
+ },
+ ];
}
- ready() {
+ connected() {
this.classList.add('full-width');
this.classList.add('breadcrumbs');
- this.path = window.location.pathname.split('/');
+ this.path = window.location.href.split(getBaseUrl()).join('/').split('/');
this.innerHTML = `
${this.path
@@ -28,4 +44,11 @@ export default class BreadCrumbs extends ComponentBase {
.join('- ›
')}
`;
}
+
+ capitalize(string) {
+ return string
+ .split('-')
+ .map((str) => str.charAt(0).toUpperCase() + str.slice(1))
+ .join(' ');
+ }
}
diff --git a/blocks/button/button.css b/blocks/button/button.css
index b3b31b2d..c1e60516 100644
--- a/blocks/button/button.css
+++ b/blocks/button/button.css
@@ -2,7 +2,6 @@ raqn-button {
width: 100%;
display: grid;
align-content: center;
- justify-content: center;
align-items: center;
justify-items: var(--scope-justify, start);
}
diff --git a/blocks/button/button.js b/blocks/button/button.js
index 0647e3d3..ca0cc23f 100644
--- a/blocks/button/button.js
+++ b/blocks/button/button.js
@@ -1,8 +1,30 @@
import ComponentBase from '../../scripts/component-base.js';
export default class Button extends ComponentBase {
- ready() {
- this.setAttribute('role', 'button');
- this.setAttribute('tabindex', '0');
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: ':is(p,div):has(> a:only-child)',
+ selectorTest: (el) => el.childNodes.length === 1,
+ };
+
+ nestedComponentsConfig = {
+ columns: {
+ componentName: 'columns',
+ active: false,
+ loaderConfig: {
+ targetsAsContainers: false,
+ },
+ },
+ };
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ },
+ },
+ ];
}
}
diff --git a/blocks/card/card.css b/blocks/card/card.css
index b3f2c32f..3d61bfeb 100644
--- a/blocks/card/card.css
+++ b/blocks/card/card.css
@@ -8,20 +8,9 @@ raqn-card {
padding: var(--scope-padding, 20px 0);
}
-raqn-card a {
- font-size: var(--raqn-font-size-3, 1.2em);
- font-weight: bold;
-}
-
-raqn-card p a {
- margin-block: 0;
-}
-
-raqn-card > picture {
- grid-column: span var(--card-columns, 1fr);
-}
-
raqn-card > div {
+ display: flex;
+ gap: var(--scope-gap, 20px);
position: relative;
background: var(--scope-inner-background, transparent);
padding: var(--scope-inner-padding, 20px);
@@ -31,33 +20,42 @@ raqn-card > div {
border-inline-end: var(--scope-border-inline-end, none);
}
-raqn-card > div div:last-child > a {
+raqn-card :where(a, button) {
+ position: relative;
+ z-index: 2;
+}
+
+/* Make entire item clickable */
+raqn-card div > div:first-child > p > em:only-child > a:only-child {
position: absolute;
- inset-inline-start: 0;
- inset-block-start: 0;
+ inset-block-end: 0;
+ inset-inline-end: 0;
width: 100%;
height: 100%;
- cursor: pointer;
- text-indent: -10000px;
- margin: 0;
- padding: 0;
+ color: transparent;
+ user-select: none;
+ z-index: 1;
}
-raqn-card > div:has(raqn-icon) {
- padding-block-end: 0;
- padding-block-end: var(--scope-icon-size, 20px);
+raqn-card div > div:first-child > p:has(> em:only-child > a:only-child) {
+ margin: 0;
}
-raqn-card p:has(raqn-icon) {
- display: inline-grid;
-}
-raqn-card raqn-icon {
- width: 100%;
+raqn-card div > div {
display: flex;
- position: absolute;
+ flex-direction: column;
+ height: 100%;
inset-block-end: 0;
inset-inline-end: 0;
- box-sizing: border-box;
- padding: var(--scope-inner-padding, 20px);
+}
+
+raqn-card div > div p:last-child:has(> raqn-button, raqn-icon) {
+ flex-grow: 1;
+ display: flex;
+ align-items: flex-end;
+}
+
+raqn-card div > div p:last-child:has(> raqn-icon) {
+ justify-content: flex-end;
}
diff --git a/blocks/card/card.js b/blocks/card/card.js
index b2a26d1c..84f76707 100644
--- a/blocks/card/card.js
+++ b/blocks/card/card.js
@@ -5,9 +5,6 @@ export default class Card extends ComponentBase {
static observedAttributes = ['columns', 'ratio', 'eager', 'background', 'button'];
ready() {
- if (this.getAttribute('button') === 'true') {
- Array.from(this.querySelectorAll('a')).forEach((a) => this.convertLink(a));
- }
this.eager = parseInt(this.getAttribute('eager') || 0, 10);
this.classList.add('inner');
if (this.eager) {
diff --git a/blocks/column/column.css b/blocks/column/column.css
new file mode 100644
index 00000000..002ec5ac
--- /dev/null
+++ b/blocks/column/column.css
@@ -0,0 +1,5 @@
+raqn-column {
+ margin: var(--scope-margin, 0);
+ width: 100%;
+ display: grid;
+}
diff --git a/blocks/column/column.js b/blocks/column/column.js
new file mode 100644
index 00000000..18fc90ed
--- /dev/null
+++ b/blocks/column/column.js
@@ -0,0 +1,70 @@
+import ComponentBase from '../../scripts/component-base.js';
+
+export default class Column extends ComponentBase {
+ static observedAttributes = ['position', 'size', 'justify'];
+
+ connected() {
+ this.position = parseInt(this.getAttribute('position'), 10);
+ this.size = this.getAttribute('size');
+ this.justify = this.getAttribute('justify') || 'stretch';
+ this.calculateGridTemplateColumns();
+ }
+
+ calculateGridTemplateColumns() {
+ this.style.setProperty('justify-content', this.justify);
+ if (this.position) {
+ const parent = this.parentElement;
+ const children = Array.from(parent.children);
+ parent.classList.add('raqn-grid');
+ let parentGridTemplateColumns = parent.style.getPropertyValue(
+ '--grid-template-columns',
+ );
+ if (!parentGridTemplateColumns) {
+ // we have no grid template columns yet
+ parentGridTemplateColumns = children
+ .map((child, index) => {
+ if (this.position === index + 1) {
+ return this.size || 'auto';
+ }
+ return 'auto';
+ })
+ .join(' ');
+ // set the new grid template columns
+ parent.style.setProperty(
+ '--grid-template-columns',
+ parentGridTemplateColumns,
+ );
+ } else {
+ const { position } = this;
+ const prio = children.indexOf(this) + 1;
+ parentGridTemplateColumns = parentGridTemplateColumns
+ .split(' ')
+ .map((size, i) => {
+ // we have a non standard value for this position
+ const hasValue = size !== 'auto';
+ // we are at the position
+ const isPosition = i + 1 === position;
+ // we are at a position before the prio
+ const isBeforePrio = i + 1 <= prio;
+ // we have a non standard value for this position and we are at the position
+ if (!hasValue && isPosition) {
+ return this.size || 'auto';
+ }
+ // we have a non standard value for this position and we are at a position before the prio
+ if (hasValue && isPosition && isBeforePrio) {
+ return this.size || size;
+ }
+ return size;
+ })
+ .join(' ');
+ // set the new grid template columns
+ parent.style.setProperty(
+ '--grid-template-columns',
+ parentGridTemplateColumns,
+ );
+ }
+ this.style.gridColumn = this.position;
+ this.style.gridRow = 1;
+ }
+ }
+}
diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css
new file mode 100644
index 00000000..1879b40e
--- /dev/null
+++ b/blocks/columns/columns.css
@@ -0,0 +1,3 @@
+raqn-column {
+ margin: var(--scope-margin, 0);
+}
diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js
new file mode 100644
index 00000000..b99948a4
--- /dev/null
+++ b/blocks/columns/columns.js
@@ -0,0 +1,93 @@
+import { collectAttributes } from '../../scripts/libs.js';
+
+export default class Columns {
+ static observedAttributes = ['position', 'size', 'justify'];
+
+ constructor(data) {
+ this.element = data.target;
+
+ const { currentAttributes } = collectAttributes(
+ data.componentName,
+ data.rawClasses,
+ [],
+ Columns.observedAttributes,
+ this.element,
+ );
+
+ Object.keys(currentAttributes).forEach((key) => {
+ this.element.setAttribute(key, currentAttributes[key]);
+ });
+
+ this.position = parseInt(this.getAttribute('position'), 10);
+ this.size = this.getAttribute('size');
+ this.justify = this.getAttribute('justify') || 'stretch';
+ this.calculateGridTemplateColumns();
+ }
+
+ calculateGridTemplateColumns() {
+ if (this.justify) {
+ this.element.style.justifyContent = this.justify;
+ }
+ if (this.position) {
+ const parent = this.element.parentElement;
+ const children = Array.from(parent.children);
+ parent.classList.add('raqn-grid');
+ let parentGridTemplateColumns = parent.style.getPropertyValue(
+ '--grid-template-columns',
+ );
+ if (!parentGridTemplateColumns) {
+ // we have no grid template columns yet
+ parentGridTemplateColumns = children
+ .map((child, index) => {
+ if (this.position === index + 1) {
+ return this.size || 'auto';
+ }
+ return 'auto';
+ })
+ .join(' ');
+ // set the new grid template columns
+ parent.style.setProperty(
+ '--grid-template-columns',
+ parentGridTemplateColumns,
+ );
+ } else {
+ const { position } = this;
+ const prio = children.indexOf(this.element) + 1;
+ parentGridTemplateColumns = parentGridTemplateColumns
+ .split(' ')
+ .map((size, i) => {
+ // we have a non standard value for this position
+ const hasValue = size !== 'auto';
+ // we are at the position
+ const isPosition = i + 1 === position;
+ // we are at a position before the prio
+ const isBeforePrio = i + 1 <= prio;
+ // we have a non standard value for this position and we are at the position
+ if (!hasValue && isPosition) {
+ return this.size || 'auto';
+ }
+ // we have a non standard value for this position and we are at a position before the prio
+ if (hasValue && isPosition && isBeforePrio) {
+ return this.size || size;
+ }
+ return size;
+ })
+ .join(' ');
+ // set the new grid template columns
+ parent.style.setProperty(
+ '--grid-template-columns',
+ parentGridTemplateColumns,
+ );
+ }
+ this.element.style.gridColumn = this.position;
+ this.element.style.gridRow = 1;
+ }
+ }
+
+ getAttribute(name) {
+ return (
+ this.element.getAttribute(name) ||
+ this.element.getAttribute(`data-${name}`)
+ );
+ }
+}
diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js
index 32ce6b8c..0d24d733 100644
--- a/blocks/footer/footer.js
+++ b/blocks/footer/footer.js
@@ -1,8 +1,23 @@
import ComponentBase from '../../scripts/component-base.js';
+import { getMeta } from '../../scripts/libs.js';
+const metaFooter = getMeta('footer');
+const metaFragment = !!metaFooter && `${metaFooter}.plain.html`;
export default class Footer extends ComponentBase {
- // keep as it is
- fragment = 'footer.plain.html';
+ fragment = metaFragment || 'footer.plain.html';
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ addToTargetMethod: 'append',
+ },
+ ];
+ }
+
+ static earlyStopRender() {
+ return metaFragment === false;
+ }
ready() {
const child = this.children[0];
diff --git a/blocks/header/header.js b/blocks/header/header.js
index 9d1e29ae..b1e400f3 100644
--- a/blocks/header/header.js
+++ b/blocks/header/header.js
@@ -1,14 +1,27 @@
import ComponentBase from '../../scripts/component-base.js';
-import { eagerImage } from '../../scripts/libs.js';
+import { eagerImage, getMeta } from '../../scripts/libs.js';
+const metaHeader = getMeta('header');
+const metaFragment = !!metaHeader && `${metaHeader}.plain.html`;
export default class Header extends ComponentBase {
- // keep as it is
- fragment = 'header.plain.html';
+ fragment = metaFragment || 'header.plain.html';
dependencies = ['navigation'];
- async processFragment(response) {
- await super.processFragment(response);
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ addToTargetMethod: 'append',
+ },
+ ];
+ }
+
+ static earlyStopRender() {
+ return metaFragment === false;
+ }
+
+ connected() {
eagerImage(this, 1);
}
}
diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js
index 90bacd1f..72d8a4e5 100644
--- a/blocks/icon/icon.js
+++ b/blocks/icon/icon.js
@@ -3,6 +3,8 @@ import ComponentBase from '../../scripts/component-base.js';
export default class Icon extends ComponentBase {
static observedAttributes = ['icon'];
+ nestedComponentsConfig = {};
+
constructor() {
super();
this.setupSprite();
@@ -18,7 +20,6 @@ export default class Icon extends ComponentBase {
}
get iconUrl() {
- // keep as it is
return `assets/icons/${this.iconName}.svg`;
}
diff --git a/blocks/image/image.css b/blocks/image/image.css
new file mode 100644
index 00000000..e34c3df2
--- /dev/null
+++ b/blocks/image/image.css
@@ -0,0 +1,3 @@
+raqn-image a {
+ display: block;
+}
\ No newline at end of file
diff --git a/blocks/image/image.js b/blocks/image/image.js
new file mode 100644
index 00000000..24a5f0ca
--- /dev/null
+++ b/blocks/image/image.js
@@ -0,0 +1,43 @@
+import ComponentBase from '../../scripts/component-base.js';
+
+// Not supported as a block
+export default class Image extends ComponentBase {
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: 'p:has(>picture:only-child) + p:has(> em:only-child > a:only-child)',
+ selectorTest: (el) => [el.childNodes.length, el.childNodes[0].childNodes.length].every(len => len === 1),
+ targetsAsContainers: true,
+ };
+
+ nestedComponentsConfig = {};
+
+ extendConfig() {
+ return [
+ ...super.extendConfig(),
+ {
+ addToTargetMethod: 'append',
+ targetsAsContainers: {
+ addToTargetMethod: 'append',
+ },
+ },
+ ];
+ }
+
+ connected() {
+ this.createLinkedImage();
+ }
+
+ createLinkedImage() {
+ if (!this.children) return;
+ const em = this.firstElementChild;
+ const anchor = em.firstElementChild;
+ const pictureParent = this.parentElement.previousElementSibling;
+ const picture = pictureParent.firstElementChild;
+ anchor.setAttribute('aria-label', anchor.textContent);
+ anchor.innerHTML = '';
+ anchor.append(picture);
+ this.append(anchor);
+ em.remove();
+ pictureParent.remove();
+ }
+}
diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js
index 56edfb06..b6874590 100644
--- a/blocks/navigation/navigation.js
+++ b/blocks/navigation/navigation.js
@@ -1,9 +1,14 @@
-import { start } from '../../scripts/init.js';
+import component from '../../scripts/init.js';
import ComponentBase from '../../scripts/component-base.js';
export default class Navigation extends ComponentBase {
static observedAttributes = ['icon', 'compact'];
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ targetsSelectors: ':scope > :is(:first-child)',
+ };
+
attributesValues = {
compact: {
xs: 'true',
@@ -20,7 +25,6 @@ export default class Navigation extends ComponentBase {
this.navButton.setAttribute('aria-controls', 'navigation');
this.navButton.setAttribute('aria-haspopup', 'true');
this.navButton.setAttribute('type', 'button');
- // this.navButton.setAttribute('tabindex', '0');
this.navButton.innerHTML = '';
this.navButton.addEventListener('click', () => {
this.classList.toggle('active');
@@ -29,7 +33,7 @@ export default class Navigation extends ComponentBase {
return this.navButton;
}
- ready() {
+ async ready() {
this.active = {};
this.navContent = this.querySelector('ul');
this.innerHTML = '';
@@ -45,7 +49,7 @@ export default class Navigation extends ComponentBase {
this.isCompact = this.getAttribute('compact') === 'true';
if (this.isCompact) {
- this.setupCompactedNav();
+ await this.setupCompactedNav();
} else {
this.setupNav();
}
@@ -60,10 +64,11 @@ export default class Navigation extends ComponentBase {
this.nav.append(this.navContent);
}
- setupCompactedNav() {
+ async setupCompactedNav() {
if (!this.navCompactedContentInit) {
this.navCompactedContentInit = true;
- start({ name: 'accordion' });
+ await Promise.all([component.loadAndDefine('accordion'), component.loadAndDefine('icon')]);
+
this.setupClasses(this.navCompactedContent, true);
this.navCompactedContent.addEventListener('click', (e) => this.activate(e));
}
@@ -81,7 +86,7 @@ export default class Navigation extends ComponentBase {
this.setupCompactedNav();
} else {
this.classList.remove('active');
- this.navButton.removeAttribute('aria-expanded');
+ this.navButton?.removeAttribute('aria-expanded');
this.setupNav();
}
}
@@ -92,11 +97,28 @@ export default class Navigation extends ComponentBase {
return icon;
}
- createAccordion(replaceChildrenElement) {
- const accordion = document.createElement('raqn-accordion');
- const children = Array.from(replaceChildrenElement.children);
- accordion.append(...children);
- replaceChildrenElement.append(accordion);
+ addIcon(elem) {
+ component.init({
+ componentName: 'icon',
+ targets: [elem],
+ rawClasses: 'icon-chevron-right',
+ config: {
+ addToTargetMethod: 'append',
+ },
+ });
+ }
+
+ createAccordion(elem) {
+ component.init({
+ componentName: 'accordion',
+ targets: [elem],
+ config: {
+ addToTargetMethod: 'append',
+ },
+ nestedComponentsConfig: {
+ button: { active: false },
+ },
+ });
}
setupClasses(ul, isCompact, level = 1) {
@@ -112,7 +134,7 @@ export default class Navigation extends ComponentBase {
if (isCompact) {
this.createAccordion(child);
} else if (level === 1) {
- anchor.append(this.createIcon('chevron-right'));
+ this.addIcon(anchor);
}
child.classList.add('has-children');
this.setupClasses(hasChildren, isCompact, level + 1);
diff --git a/blocks/section-metadata/section-metadata.js b/blocks/section-metadata/section-metadata.js
index a3066de9..76675076 100644
--- a/blocks/section-metadata/section-metadata.js
+++ b/blocks/section-metadata/section-metadata.js
@@ -2,6 +2,8 @@ import { collectAttributes } from '../../scripts/libs.js';
import ComponentBase from '../../scripts/component-base.js';
import ComponentMixin from '../../scripts/component-mixin.js';
+// TODO the block for this component should not have content, the values should come only form class attribute as for any other component
+// as for any other block. should replace the this.parentElement
export default class SectionMetadata extends ComponentBase {
async ready() {
const classes = [...this.querySelectorAll(':scope > div > div:first-child')]
diff --git a/blocks/theme/theme.css b/blocks/theming/theming.css
similarity index 55%
rename from blocks/theme/theme.css
rename to blocks/theming/theming.css
index bad16572..2b4e4e1d 100644
--- a/blocks/theme/theme.css
+++ b/blocks/theming/theming.css
@@ -1,3 +1,3 @@
-raqn-theme {
+raqn-theming {
display: none;
}
diff --git a/blocks/theme/theme.js b/blocks/theming/theming.js
similarity index 91%
rename from blocks/theme/theme.js
rename to blocks/theming/theming.js
index 6d7c59fe..dc2fcd4f 100644
--- a/blocks/theme/theme.js
+++ b/blocks/theming/theming.js
@@ -1,14 +1,19 @@
import ComponentBase from '../../scripts/component-base.js';
-import { config, getMeta } from '../../scripts/libs.js';
+import { globalConfig, getMeta } from '../../scripts/libs.js';
// minify alias
+const metaTheming = getMeta('theming');
+const metaFragment = metaTheming && `${metaTheming}.json`;
const k = Object.keys;
-export default class Theme extends ComponentBase {
+export default class Theming extends ComponentBase {
+
+ nestedComponentsConfig = {};
+
constructor() {
super();
this.scapeDiv = document.createElement('div');
// keep as it is
- this.fragment = 'theme.json';
+ this.fragment = metaFragment || 'theming.json';
this.skip = ['tags'];
this.toTags = [
'font-size',
@@ -30,7 +35,7 @@ export default class Theme extends ComponentBase {
const params = rest.pop().split('.');
const format = params.pop();
const lastBit = params.pop();
- const fontWeight = config.fontWeights[lastBit] || 'regular';
+ 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});}`;
diff --git a/head.html b/head.html
index 78d2fe4d..e4f50f0c 100644
--- a/head.html
+++ b/head.html
@@ -1,18 +1,26 @@
+
-
-
@@ -20,4 +28,4 @@
-
+
diff --git a/scripts/component-base.js b/scripts/component-base.js
index ddc79f10..c461fe43 100644
--- a/scripts/component-base.js
+++ b/scripts/component-base.js
@@ -1,5 +1,6 @@
-import { start, startBlock } from './init.js';
-import { getBreakPoints, listenBreakpointChange, camelCaseAttr, capitalizeCaseAttr } from './libs.js';
+import component from './init.js';
+
+import { getBreakPoints, listenBreakpointChange, camelCaseAttr, capitalizeCaseAttr, deepMerge } from './libs.js';
export default class ComponentBase extends HTMLElement {
static get knownAttributes() {
@@ -8,17 +9,70 @@ export default class ComponentBase extends HTMLElement {
constructor() {
super();
- this.blockName = null; // set by component loader
+ this.componentName = null; // set by component loader
this.webComponentName = null; // set by component loader
this.fragment = false;
this.dependencies = [];
this.breakpoints = getBreakPoints();
this.uuid = `gen${crypto.randomUUID().split('-')[0]}`;
this.attributesValues = {}; // the values are set by the component loader
- this.config = {};
+ this.setConfig('config', 'extendConfig');
+ this.setConfig('nestedComponentsConfig', 'extendNestedConfig');
this.setBinds();
}
+ static loaderConfig = {
+ targetsSelectorsPrefix: null,
+ targetsSelectors: null,
+ selectorTest: null, // a function to filter elements matched by targetsSelectors
+ targetsSelectorsLimit: null,
+ targetsAsContainers: false,
+ };
+
+ static async earlyStopRender() {
+ return false;
+ }
+
+ attributesValues = {}; // the values are set by the component loader
+
+ config = {
+ addToTargetMethod: 'replaceWith',
+ contentFromTargets: true,
+ targetsAsContainers: {
+ addToTargetMethod: 'replaceWith',
+ },
+ };
+
+ nestedComponentsConfig = {
+ image: {
+ componentName: 'image',
+ },
+ button: {
+ componentName: 'button',
+ },
+ columns: {
+ componentName: 'columns',
+ active: false,
+ loaderConfig: {
+ targetsAsContainers: false,
+ },
+ },
+ };
+
+ setConfig(config, method) {
+ const conf = this[method]?.();
+ if (!conf.length) return;
+ this[config] = deepMerge({}, ...conf);
+ }
+
+ extendConfig() {
+ return [...(super.extendConfig?.() || []), this.config];
+ }
+
+ extendNestedConfig() {
+ return [...(super.extendNestedConfig?.() || []), this.nestedComponentsConfig];
+ }
+
setBinds() {
this.onBreakpointChange = this.onBreakpointChange.bind(this);
}
@@ -29,6 +83,7 @@ export default class ComponentBase extends HTMLElement {
}
}
+ // TODO change to dataset attributes
setBreakpointAttributesValues(e) {
Object.entries(this.attributesValues).forEach(([attribute, breakpointsValues]) => {
const isAttribute = attribute !== 'class';
@@ -83,33 +138,56 @@ export default class ComponentBase extends HTMLElement {
this.initSubscriptions(); // must subscribe each time the element is added to the document
if (!this.initialized) {
this.setAttribute('id', this.uuid);
- if (this.fragment) {
- await this.loadFragment(this.fragment);
- }
- if (this.dependencies.length > 0) {
- await Promise.all(this.dependencies.map((dep) => start({ name: dep })));
- }
- this.connected(); // manipulate the html
+ await Promise.all([this.loadFragment(this.fragment), this.loadDependencies()]);
+ await this.connected(); // manipulate/create the html
+ await this.initNestedComponents();
this.addListeners(); // html is ready add listeners
- this.ready(); // add extra functionality
+ await this.ready(); // add extra functionality
this.setAttribute('initialized', true);
this.initialized = true;
- this.dispatchEvent(new CustomEvent('initialized', { detail: { block: this } }));
+ this.dispatchEvent(new CustomEvent('initialized', { detail: { element: this } }));
}
}
+ async initNestedComponents() {
+ const nested = await Promise.all(
+ Object.values(this.nestedComponentsConfig).flatMap(async (setting) => {
+ if (!setting.active) return [];
+ const s = this.fragment
+ ? deepMerge({}, setting, {
+ // Content can contain blocks which are going to init their own nestedComponents.
+ loaderConfig: {
+ targetsSelectorsPrefix: ':scope > div >', // Limit only to default content, exclude blocks.
+ },
+ })
+ : setting;
+ return component.init(s);
+ }),
+ );
+ this.nestedElements = nested.flat();
+ }
+
+ async loadDependencies() {
+ if (!this.dependencies.length) return;
+ await Promise.all(this.dependencies.map((dep) => component.loadAndDefine(dep)));
+ }
+
async loadFragment(path) {
- const response = await fetch(`${path}`, window.location.pathname.endsWith(path) ? { cache: 'reload' } : {});
- return this.processFragment(response);
+ if (!path) return;
+ const response = await this.getFragment(path);
+ await this.processFragment(response);
+ }
+
+ getFragment(path) {
+ return fetch(`${path}`, window.location.pathname.endsWith(path) ? { cache: 'reload' } : {});
}
async processFragment(response) {
if (response.ok) {
const html = await response.text();
this.innerHTML = html;
- return this.querySelectorAll(':scope > div > div').forEach((block) => startBlock(block));
+ await Promise.all([...this.querySelectorAll('div[class]')].map((block) => component.init({ targets: [block] })));
}
- return response;
}
initSubscriptions() {}
diff --git a/scripts/component-loader.js b/scripts/component-loader.js
index 097c64fd..3db8d095 100644
--- a/scripts/component-loader.js
+++ b/scripts/component-loader.js
@@ -1,91 +1,213 @@
-import { config, collectAttributes, loadModule } from './libs.js';
+import { collectAttributes, loadModule, deepMerge, mergeUniqueArrays } from './libs.js';
+// import ComponentBase from './component-base.js';
+
import ComponentMixin from './component-mixin.js';
export default class ComponentLoader {
- constructor(blockName, element) {
- window.raqnComponents = window.raqnComponents || {};
- this.blockName = blockName;
- this.pathWithoutExtension = `/blocks/${this.blockName}/${this.blockName}`;
- this.block = element;
- if (this.block) {
- this.content = this.block.children;
+ constructor({ componentName, targets = [], loaderConfig, rawClasses, config, nestedComponentsConfig, active }) {
+ window.raqnComponents ??= {};
+ if (!componentName) {
+ // eslint-disable-next-line no-console
+ console.error('`componentName` is required');
+ return;
}
+ this.componentName = componentName;
+ this.targets = targets.map((target) => ({ target }));
+ this.loaderConfig = loaderConfig;
+ this.rawClasses = rawClasses?.trim?.().split?.(' ') || [];
+ this.config = config;
+ this.nestedComponentsConfig = nestedComponentsConfig;
+ this.pathWithoutExtension = `/blocks/${this.componentName}/${this.componentName}`;
+ this.isWebComponent = null;
+ this.isClass = null;
+ this.isFn = null;
+ this.active = active;
}
- get handler() {
- return window.raqnComponents[this.blockName];
+ get Handler() {
+ return window.raqnComponents[this.componentName];
}
- set handler(handler) {
- window.raqnComponents[this.blockName] = handler;
+ set Handler(handler) {
+ window.raqnComponents[this.componentName] = handler;
}
- isWebComponentClass(clazz = this.handler) {
- return clazz.toString().startsWith('class');
+ setHandlerType(handler = this.Handler) {
+ this.isWebComponent = handler.prototype instanceof HTMLElement;
+ this.isClass = !this.isWebComponent && handler.toString().startsWith('class');
+ this.isFn = !this.isWebComponent && !this.isClass && typeof handler === 'function';
}
get webComponentName() {
- return `raqn-${this.blockName.toLowerCase()}`;
+ return `raqn-${this.componentName.toLowerCase()}`;
}
- async setupElement() {
- const element = document.createElement(this.webComponentName);
- element.blockName = this.blockName;
- element.webComponentName = this.webComponentName;
- element.append(...this.block.children);
- const { currentAttributes } = collectAttributes(
- this.blockName,
- this.block.classList,
+ async init() {
+ if (this.active === false) return null;
+ if (!this.componentName) return null;
+ const loaded = await this.loadAndDefine();
+ if (!loaded) return null;
+ this.setHandlerType();
+ if (await this.Handler?.earlyStopRender?.()) return this.Handler;
+ if (!this.targets?.length) return this.Handler;
+
+ this.setTargets();
+ return Promise.all(
+ this.targets.map(async (target) => {
+ const data = this.getTargetData(target);
+ if (this.isWebComponent) {
+ const elem = await this.createElementAndConfigure(data);
+ data.componentElem = elem;
+ this.addContentFromTarget(data);
+ await this.connectComponent(data);
+ return elem;
+ }
+
+ if (this.isClass) {
+ return new this.Handler({
+ componentName: this.componentName,
+ ...data,
+ });
+ }
+
+ if (this.isFn) {
+ return this.Handler(data);
+ }
+ return null;
+ }),
+ );
+ }
+
+ getTargetData({ target, container }) {
+ return {
+ target,
+ container,
+ rawClasses: !container ? mergeUniqueArrays(this.rawClasses, target.classList) : this.rawClasses,
+ // content: target?.childNodes,
+ };
+ }
+
+ setTargets() {
+ this.loaderConfig = deepMerge({}, this.Handler.loaderConfig, this.loaderConfig);
+ const { targetsSelectorsPrefix, targetsSelectors, targetsSelectorsLimit, targetsAsContainers, selectorTest } =
+ this.loaderConfig;
+ const selector = `${targetsSelectorsPrefix || ''} ${targetsSelectors}`;
+ if (targetsAsContainers) {
+ this.targets = this.targets.flatMap(({ target: container }) => {
+ const targetsFromContainer = this.getTargets(container, selector, selectorTest, targetsSelectorsLimit);
+ return targetsFromContainer.map((target) => ({
+ target,
+ container,
+ }));
+ });
+ }
+ }
+
+ getTargets(container, selector, selectorTest, length = 1) {
+ const queryType = length && length <= 1 ? 'querySelector' : 'querySelectorAll';
+ let elements = container[queryType](selector);
+
+ if (length === null) elements = [...elements];
+ if (length > 1) elements = [...elements].slice(0, length);
+ if (length === 1) elements = [elements];
+
+ if (typeof selectorTest === 'function') {
+ elements = elements.filter((el) => selectorTest(el));
+ }
+
+ return elements;
+ }
+
+ async createElementAndConfigure(data) {
+ const componentElem = document.createElement(this.webComponentName);
+
+ componentElem.componentName = this.componentName;
+ componentElem.webComponentName = this.webComponentName;
+ componentElem.config = deepMerge({}, componentElem.config, this.config);
+ const { nestedComponentsConfig } = componentElem;
+ const { currentAttributes, nestedComponents } = collectAttributes(
+ this.componentName,
+ data.rawClasses,
await ComponentMixin.getMixins(),
- this?.handler?.knownAttributes,
- element,
+ this?.Handler?.knownAttributes,
+ componentElem,
);
+
Object.keys(currentAttributes).forEach((key) => {
- element.setAttribute(key, currentAttributes[key]);
+ componentElem.setAttribute(key, currentAttributes[key].trim());
+ });
+
+ componentElem.nestedComponentsConfig = deepMerge(
+ componentElem.nestedComponentsConfig,
+ this.nestedComponentsConfig,
+ nestedComponents,
+ );
+
+ Object.keys(nestedComponentsConfig).forEach((key) => {
+ const defaults = {
+ targets: [componentElem],
+ active: true,
+ loaderConfig: {
+ targetsAsContainers: true,
+ },
+ };
+ nestedComponentsConfig[key] = deepMerge(defaults, nestedComponentsConfig[key]);
});
+ return componentElem;
+ }
+
+ addContentFromTarget(data) {
+ const { componentElem, target } = data;
+ const { contentFromTargets } = componentElem.config;
+ if (!contentFromTargets) return;
+
+ componentElem.append(...target.children);
+ }
+
+ async connectComponent(data) {
+ const { componentElem } = data;
+ componentElem.setAttribute('isloading', '');
const initialized = new Promise((resolve) => {
const initListener = async (event) => {
- if (event.detail.block === element) {
- element.removeEventListener('initialized', initListener);
- await ComponentMixin.startAll(element);
- resolve();
+ if (event.detail.element === componentElem) {
+ componentElem.removeEventListener('initialized', initListener);
+ await ComponentMixin.startAll(componentElem);
+ componentElem.removeAttribute('isloading');
+ resolve(componentElem);
}
};
- element.addEventListener('initialized', initListener);
+ componentElem.addEventListener('initialized', initListener);
});
- const isSemanticElement = config.semanticBlocks.includes(this.block.tagName.toLowerCase());
- const addComponentMethod = isSemanticElement ? 'append' : 'replaceWith';
- this.block[addComponentMethod](element);
- await initialized;
+ const { targetsAsContainers } = this.loaderConfig;
+ const conf = componentElem.config;
+ const addToTargetMethod = targetsAsContainers ? conf.targetsAsContainers.addToTargetMethod : conf.addToTargetMethod;
+ data.target[addToTargetMethod](componentElem);
+
+ return initialized;
}
- async start() {
+ async loadAndDefine() {
try {
let cssLoaded = Promise.resolve();
- if (!this.handler) {
- this.handler = (async () => {
+ if (!this.Handler) {
+ this.Handler = (async () => {
const { css, js } = loadModule(this.pathWithoutExtension);
cssLoaded = css;
const mod = await js;
- if (this.isWebComponentClass(mod.default)) {
+ if (mod.default.prototype instanceof HTMLElement) {
window.customElements.define(this.webComponentName, mod.default);
}
return mod.default;
})();
}
- this.handler = await this.handler;
- if (this.block) {
- if (this.isWebComponentClass()) {
- await this.setupElement();
- } else {
- await this.handler(this.block);
- }
- }
+ this.Handler = await this.Handler;
await cssLoaded;
+ return true;
} catch (error) {
// eslint-disable-next-line no-console
- console.error(`failed to load module for ${this.blockName}`, error);
+ console.error(`failed to load module for ${this.componentName}`, error);
+ return false;
}
}
}
diff --git a/scripts/init.js b/scripts/init.js
index 027ea915..92589464 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -1,84 +1,156 @@
import ComponentLoader from './component-loader.js';
import ComponentMixin from './component-mixin.js';
-import {
- config,
- eagerImage,
- getMeta,
-} from './libs.js';
-
-function getInfo(block) {
- const el = block;
- const tagName = el.tagName.toLowerCase();
- let name = tagName;
- if (!config.semanticBlocks.includes(tagName)) {
- [name] = Array.from(el.classList);
- }
- return {
- name,
- el,
- };
-}
-
-function getInfos(blocks) {
- return blocks.map((block) => getInfo(block));
-}
-
-export async function start({ name, el }) {
- const loader = new ComponentLoader(name, el);
- return loader.start();
-}
-
-export async function startBlock(block) {
- return start(getInfo(block));
-}
-
-function initEagerImages() {
- const eagerImages = getMeta('eager-images');
- if (eagerImages) {
- const length = parseInt(eagerImages, 10);
- eagerImage(document.body, length);
- }
-}
-
-function getLcp() {
- const lcpMeta = getMeta('lcp');
- return lcpMeta
- ? lcpMeta.split(',').map((name) => ({ name: name.trim() }))
- : [];
-}
-
-function includesInfo(infos, search) {
- return infos.find(({ name }) => name === search);
-}
-
-async function init() {
- ComponentMixin.getMixins();
-
- // mechanism of retrieving lang to be used in the app
- // TODO - set this based on url structure or meta tag for current path
- document.documentElement.lang ||= 'en';
-
- initEagerImages();
-
- const blocks = [
- document.body.querySelector(config.semanticBlocks[0]),
- ...document.querySelectorAll('[class]:not([class^=style]'),
- ...document.body.querySelectorAll(config.semanticBlocks.slice(1).join(',')),
- ];
-
- const data = getInfos(blocks);
- const lcp = getLcp().map(({ name }) => includesInfo(data, name) || { name });
- const delay = window.raqnLCPDelay || [];
- const lazy = data.filter(
- ({ name }) => !includesInfo(lcp, name) && !includesInfo(delay, name),
- );
-
- // start with lcp
- Promise.all(lcp.map(({ name, el }) => start({ name, el }))).then(() => {
- document.body.style.display = 'unset';
- });
- // timeout for the rest to proper prioritize in case of stalled loading
- lazy.map(({ name, el }) => setTimeout(() => start({ name, el })));
-}
-
-init();
+import { globalConfig, eagerImage, getMeta, getMetaGroup } from './libs.js';
+
+const component = {
+ async init(settings) {
+ return new ComponentLoader({
+ ...settings,
+ componentName: settings.componentName ?? this.getBlockData(settings?.targets?.[0]).componentName,
+ }).init();
+ },
+
+ async loadAndDefine(componentName) {
+ await new ComponentLoader({ componentName }).loadAndDefine();
+ },
+
+ getBlockData(block) {
+ const tagName = block.tagName.toLowerCase();
+ const lcp = block.classList.contains('lcp');
+ let componentName = tagName;
+ if (!globalConfig.semanticBlocks.includes(tagName)) {
+ componentName = block.classList.item(0);
+ }
+ return { block, componentName, lcp };
+ },
+};
+
+const onLoadComponents = {
+ staticStructureComponents: [
+ {
+ componentName: 'image',
+ block: document,
+ loaderConfig: {
+ targetsAsContainers: true,
+ targetsSelectorsPrefix: 'main > div >',
+ },
+ },
+ {
+ componentName: 'button',
+ block: 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('[class]:not([class^=style]'),
+ ...document.body.querySelectorAll(globalConfig.semanticBlocks.slice(1).join(',')),
+ ];
+ },
+
+ setBlocksData() {
+ const structureData = this.structureComponents.map(({ componentName }) => ({
+ componentName,
+ block: document,
+ loaderConfig: {
+ targetsAsContainers: true,
+ },
+ }));
+ structureData.push(...this.staticStructureComponents);
+
+ const blocksData = this.blocks.map((block) => component.getBlockData(block));
+ this.blocksData = [...structureData, ...blocksData];
+ },
+
+ setLcp() {
+ const lcpMeta = getMeta('lcp');
+ const defaultLcp = ['theme', 'header', 'breadcrumbs'];
+ this.lcp = lcpMeta?.length
+ ? lcpMeta.split(',').map((componentName) => ({ componentName: componentName.trim() }))
+ : defaultLcp;
+ },
+
+ setStructure() {
+ const structureComponents = getMetaGroup('structure');
+ this.structureComponents = structureComponents.flatMap(({ name, content }) => {
+ if (content !== true) return [];
+ return {
+ componentName: name.trim(),
+ };
+ });
+ },
+
+ setLcpBlocks() {
+ this.lcpBlocks = this.blocksData.filter((data) => !!this.findLcp(data));
+ },
+
+ setLazyBlocks() {
+ this.lazyBlocks = this.blocksData.filter((data) => !this.findLcp(data));
+ },
+
+ findLcp(data) {
+ return (
+ this.lcp.find(({ componentName }) => componentName === data.componentName) ||
+ data.lcp /* ||
+ [...document.querySelectorAll('main > div > [class]:nth-child(-n+2)')].find((el) => el === data.block) */
+ );
+ },
+
+ initBlocks() {
+ Promise.all(
+ this.lcpBlocks.map(async ({ componentName, block, loaderConfig }) =>
+ component.init({ componentName, targets: [block], loaderConfig }),
+ ),
+ ).then(() => {
+ document.body.style.display = 'unset';
+ });
+ this.lazyBlocks.map(({ componentName, block, loaderConfig }) =>
+ setTimeout(() => component.init({ componentName, targets: [block], loaderConfig })),
+ );
+ },
+};
+
+const globalInit = {
+ async init() {
+ this.loadMixins();
+ this.setLang();
+ this.initEagerImages();
+ onLoadComponents.init();
+ },
+
+ loadMixins() {
+ ComponentMixin.getMixins();
+ },
+
+ // TODO - maybe take this from the url structure.
+ setLang() {
+ document.documentElement.lang ||= 'en';
+ },
+
+ initEagerImages() {
+ const eagerImages = getMeta('eager-images');
+ if (eagerImages) {
+ const length = parseInt(eagerImages, 10);
+ eagerImage(document.body, length);
+ }
+ },
+};
+
+globalInit.init();
+
+export default component;
diff --git a/scripts/libs.js b/scripts/libs.js
index 531ccbd9..7d30580e 100644
--- a/scripts/libs.js
+++ b/scripts/libs.js
@@ -1,4 +1,4 @@
-export const config = {
+export const globalConfig = {
semanticBlocks: ['header', 'footer'],
breakpoints: {
xs: 0,
@@ -35,7 +35,7 @@ export function getBreakPoints() {
// return if already set
if (window.raqnBreakpoints.ordered.length) return window.raqnBreakpoints;
- window.raqnBreakpoints.ordered = Object.entries(config.breakpoints)
+ window.raqnBreakpoints.ordered = Object.entries(globalConfig.breakpoints)
.sort((a, b) => a[1] - b[1])
.map(([breakpointMinName, breakpointMin], index, arr) => {
const [, breakpointNext] = arr[index + 1] || [];
@@ -60,9 +60,9 @@ export function getBreakPoints() {
export function listenBreakpointChange(callback) {
const breakpoints = getBreakPoints();
let { active } = breakpoints;
-
+ const listeners = [];
breakpoints.ordered.forEach((breakpoint) => {
- breakpoint.matchMedia.addEventListener('change', (e) => {
+ const fn = (e) => {
e.raqnBreakpoint = { ...breakpoint };
if (e.matches) {
@@ -74,8 +74,16 @@ export function listenBreakpointChange(callback) {
}
callback?.(e);
- });
+ };
+ listeners.push({ media: breakpoint.matchMedia, callback: fn });
+ breakpoint.matchMedia.addEventListener('change', fn);
});
+
+ return {
+ removeBreakpointListeners: () => {
+ listeners.forEach((listener) => listener.media.removeEventListener('change', listener.callback));
+ },
+ };
}
export const debounce = (func, wait, immediate) => {
@@ -107,34 +115,88 @@ export const eagerImage = (block, length = 1) => {
});
};
+export function stringToJsVal(string) {
+ switch (string.trim()) {
+ case 'true':
+ return true;
+ case 'false':
+ return false;
+ case 'null':
+ return null;
+ case 'undefined':
+ return undefined;
+ default:
+ return string;
+ }
+}
+
export function getMeta(name) {
const meta = document.querySelector(`meta[name="${name}"]`);
if (!meta) {
return null;
}
- return meta.content;
+ return stringToJsVal(meta.content);
}
-export function collectAttributes(blockName, classes, mixins, knownAttributes = [], element = null) {
+export function getMetaGroup(group) {
+ const prefix = `${group}-`;
+ const metaGroup = [...document.querySelectorAll(`meta[name^="${prefix}"]`)];
+ return metaGroup.map((meta) => ({
+ name: meta.name.replace(new RegExp(`^${prefix}`), ''),
+ content: stringToJsVal(meta.content),
+ }));
+}
+
+export function collectAttributes(componentName, classes, mixins, knownAttributes = [], element = null) {
+ const classesList = [];
const mediaAttributes = {};
- // inherit default param values
const attributesValues = element?.attributesValues || {};
+ const nestedComponents = {};
+ /**
+ * 1. get all nested components config names
+ * 2. get all the classes prefixed with the config name
+ */
+ const nestPrefix = 'nest-';
+ classes.forEach((c) => {
+ const isNested = c.startsWith(nestPrefix);
+ if (isNested) {
+ const name = c.slice(nestPrefix.length);
+ nestedComponents[name] = {
+ componentName: name,
+ active: true,
+ /* targets: [element] */
+ };
+ } else {
+ classesList.push(c);
+ }
+ });
+
+ const nestedComponentsNames = Object.keys(nestedComponents);
const mixinKnownAttributes = mixins.flatMap((mixin) => mixin.observedAttributes || []);
- const attrs = Array.from(classes)
- .filter((c) => c !== blockName && c !== 'block')
+ const attrs = classesList
+ .filter((c) => c !== componentName && c !== 'block')
.reduce((acc, c) => {
let value = c;
let isKnownAttribute = null;
let isMixinKnownAttributes = null;
- const classBreakpoint = Object.keys(config.breakpoints).find((b) => c.startsWith(`${b}-`));
+ const classBreakpoint = Object.keys(globalConfig.breakpoints).find((b) => c.startsWith(`${b}-`));
const activeBreakpoint = getBreakPoints().active.name;
if (classBreakpoint) {
value = value.slice(classBreakpoint.length + 1);
}
+ const nested = nestedComponentsNames.find((prefix) => value.startsWith(prefix));
+ if (nested) {
+ nestedComponents[nested].rawClasses ??= '';
+ nestedComponents[nested].rawClasses += `${classBreakpoint ? `${classBreakpoint}-` : ''}${value.slice(
+ nested.length + 1,
+ )} `;
+ return acc;
+ }
+
let key = 'class';
const isClassValue = value.startsWith(key);
if (isClassValue) {
@@ -160,11 +222,12 @@ export function collectAttributes(blockName, classes, mixins, knownAttributes =
}
if (isKnownAttribute) attributesValues[camelCaseKey][classBreakpoint] = value;
if (isClass) {
- if (attributesValues[camelCaseKey][classBreakpoint]) {
- attributesValues[camelCaseKey][classBreakpoint] += ` ${value}`;
- } else {
- attributesValues[camelCaseKey][classBreakpoint] = value;
- }
+ attributesValues[camelCaseKey][classBreakpoint] ??= '';
+ // if (attributesValues[camelCaseKey][classBreakpoint]) {
+ attributesValues[camelCaseKey][classBreakpoint] += `${value} `;
+ // } else {
+ // attributesValues[camelCaseKey][classBreakpoint] = value;
+ // }
}
// support multivalue attributes
} else if (acc[key]) {
@@ -173,13 +236,12 @@ export function collectAttributes(blockName, classes, mixins, knownAttributes =
acc[key] = value;
}
- if (isKnownAttribute || isClass) attributesValues[camelCaseKey].all = acc[key];
+ if ((isKnownAttribute || isClass) && acc[key]) attributesValues[camelCaseKey].all = acc[key];
return acc;
}, {});
return {
- // TODO improve how classes are collected and merged.
currentAttributes: {
...attrs,
...mediaAttributes,
@@ -188,6 +250,7 @@ export function collectAttributes(blockName, classes, mixins, knownAttributes =
}),
},
attributesValues,
+ nestedComponents,
};
}
@@ -212,3 +275,42 @@ export function loadModule(urlWithoutExtension) {
return { css, js };
}
+
+export function mergeUniqueArrays(...arrays) {
+ const mergedArrays = arrays.reduce((acc, arr) => [...acc, ...(arr || [])], []);
+ return [...new Set(mergedArrays)];
+}
+
+export function getBaseUrl() {
+ return document.head.querySelector('base').href;
+}
+
+export function isHomePage(url) {
+ return getBaseUrl() === (url || window.location.href);
+}
+
+export function isObject(item) {
+ return item && typeof item === 'object' && !Array.isArray(item);
+}
+
+export function isObjectNotWindow(item) {
+ return isObject(item) && item !== window;
+}
+
+export function deepMerge(origin, ...toMerge) {
+ if (!toMerge.length) return origin;
+ const merge = toMerge.shift();
+
+ if (isObjectNotWindow(origin) && isObjectNotWindow(merge)) {
+ Object.keys(merge).forEach((key) => {
+ if (isObjectNotWindow(merge[key])) {
+ if (!origin[key]) Object.assign(origin, { [key]: {} });
+ deepMerge(origin[key], merge[key]);
+ } else {
+ Object.assign(origin, { [key]: merge[key] });
+ }
+ });
+ }
+
+ return deepMerge(origin, ...toMerge);
+}
diff --git a/styles/styles.css b/styles/styles.css
index 4bb0a9b2..ddbcea56 100644
--- a/styles/styles.css
+++ b/styles/styles.css
@@ -62,18 +62,18 @@ caption {
color: currentcolor;
}
-/* header {
+header {
--scope-background: var(--scope-header-background, #fff);
--scope-color: var(--scope-header-color, #000);
min-height: var(--scope-header-height, 64px);
display: grid;
background: var(--scope-header-background, #fff);
-} */
+}
-main {
+/* main {
margin-top: var(--scope-header-height, 64px);
-}
+} */
main > div {
max-width: var(--scope-max-width, 100%);
@@ -154,6 +154,8 @@ img {
height: auto;
}
+
+[isloading],
.hide {
display: none;
pointer-events: none;