diff --git a/blocks/breadcrumbs/breadcrumbs.js b/blocks/breadcrumbs/breadcrumbs.js
index d35d88e5..b74ab36b 100644
--- a/blocks/breadcrumbs/breadcrumbs.js
+++ b/blocks/breadcrumbs/breadcrumbs.js
@@ -4,7 +4,7 @@ import { getBaseUrl } from '../../scripts/libs.js';
export default class Breadcrumbs extends ComponentBase {
static loaderConfig = {
...ComponentBase.loaderConfig,
- targetsSelectors: 'main > div',
+ targetsSelectors: 'main > div:first-child',
targetsSelectorsLimit: 1,
};
diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css
deleted file mode 100644
index 1879b40e..00000000
--- a/blocks/columns/columns.css
+++ /dev/null
@@ -1,3 +0,0 @@
-raqn-column {
- margin: var(--scope-margin, 0);
-}
diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js
deleted file mode 100644
index 1eb9b63c..00000000
--- a/blocks/columns/columns.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { collectAttributes } from '../../scripts/libs.js';
-
-export default class Columns {
- static observedAttributes = ['data-position', 'data-size', 'data-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.element.dataset.position, 10);
- this.element.dataset.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.element.dataset.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.element.dataset.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.element.dataset.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;
- }
- }
-}
diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js
index d9ce7c23..452e5037 100644
--- a/blocks/footer/footer.js
+++ b/blocks/footer/footer.js
@@ -4,7 +4,14 @@ import { getMeta } from '../../scripts/libs.js';
const metaFooter = getMeta('footer');
const metaFragment = !!metaFooter && `${metaFooter}.plain.html`;
export default class Footer extends ComponentBase {
- fragment = metaFragment || 'footer.plain.html';
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ loaderStopInit() {
+ return metaFooter === false;
+ },
+ };
+
+ fragmentPath = metaFragment || 'footer.plain.html';
extendConfig() {
return [
@@ -15,10 +22,6 @@ export default class Footer extends ComponentBase {
];
}
- static earlyStopRender() {
- return metaFooter === false;
- }
-
ready() {
const child = this.children[0];
child.replaceWith(...child.children);
diff --git a/blocks/header/header.js b/blocks/header/header.js
index c1ef9540..0a9209e3 100644
--- a/blocks/header/header.js
+++ b/blocks/header/header.js
@@ -4,7 +4,14 @@ import { eagerImage, getMeta } from '../../scripts/libs.js';
const metaHeader = getMeta('header');
const metaFragment = !!metaHeader && `${metaHeader}.plain.html`;
export default class Header extends ComponentBase {
- fragment = metaFragment || 'header.plain.html';
+ static loaderConfig = {
+ ...ComponentBase.loaderConfig,
+ loaderStopInit() {
+ return metaHeader === false;
+ },
+ };
+
+ fragmentPath = metaFragment || 'header.plain.html';
dependencies = ['navigation', 'image'];
@@ -17,10 +24,6 @@ export default class Header extends ComponentBase {
];
}
- static earlyStopRender() {
- return metaHeader === false;
- }
-
connected() {
eagerImage(this, 1);
}
diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js
index f9611761..49ca9535 100644
--- a/blocks/icon/icon.js
+++ b/blocks/icon/icon.js
@@ -39,8 +39,15 @@ export default class Icon extends ComponentBase {
async connected() {
this.setAttribute('aria-hidden', 'true');
+ }
+
+ onAttributeIconChanged({ oldValue, newValue }) {
+ if (oldValue === newValue) return;
+ this.loadIcon(newValue);
+ }
- this.iconName = this.dataset.icon;
+ async loadIcon(icon) {
+ this.iconName = icon;
if (!this.cache[this.iconName]) {
this.cache[this.iconName] = {
loading: new Promise((resolve) => {
diff --git a/blocks/image/image.js b/blocks/image/image.js
index 24a5f0ca..74aa774a 100644
--- a/blocks/image/image.js
+++ b/blocks/image/image.js
@@ -5,7 +5,7 @@ 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),
+ selectorTest: (el) => [el.childNodes.length, el.childNodes[0].childNodes.length].every((len) => len === 1),
targetsAsContainers: true,
};
diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js
index 80e6a893..e7a97191 100644
--- a/blocks/navigation/navigation.js
+++ b/blocks/navigation/navigation.js
@@ -9,30 +9,6 @@ export default class Navigation extends ComponentBase {
targetsSelectors: ':scope > :is(:first-child)',
};
- attributesValues = {
- compact: {
- xs: 'true',
- s: 'true',
- m: 'true',
- all: 'false',
- },
- };
-
- createButton() {
- this.navButton = document.createElement('button');
- this.navButton.setAttribute('aria-label', 'Menu');
- this.navButton.setAttribute('aria-expanded', 'false');
- this.navButton.setAttribute('aria-controls', 'navigation');
- this.navButton.setAttribute('aria-haspopup', 'true');
- this.navButton.setAttribute('type', 'button');
- this.navButton.innerHTML = '';
- this.navButton.addEventListener('click', () => {
- this.classList.toggle('active');
- this.navButton.setAttribute('aria-expanded', this.classList.contains('active'));
- });
- return this.navButton;
- }
-
async ready() {
this.active = {};
this.navContent = this.querySelector('ul');
@@ -90,18 +66,34 @@ export default class Navigation extends ComponentBase {
}
}
- createIcon(name = this.icon) {
- const icon = document.createElement('raqn-icon');
- icon.setAttribute('icon', name);
- return icon;
+ onAttributeIconChanged({ newValue }) {
+ if (!this.initialized) return;
+ if (!this.isCompact) return;
+ this.navIcon.dataset.icon = newValue;
+ }
+
+ createButton() {
+ this.navButton = document.createElement('button');
+ this.navButton.setAttribute('aria-label', 'Menu');
+ this.navButton.setAttribute('aria-expanded', 'false');
+ this.navButton.setAttribute('aria-controls', 'navigation');
+ this.navButton.setAttribute('aria-haspopup', 'true');
+ this.navButton.setAttribute('type', 'button');
+ this.navButton.innerHTML = ``;
+ this.navIcon = this.navButton.querySelector('raqn-icon');
+ this.navButton.addEventListener('click', () => {
+ this.classList.toggle('active');
+ this.navButton.setAttribute('aria-expanded', this.classList.contains('active'));
+ });
+ return this.navButton;
}
addIcon(elem) {
component.init({
componentName: 'icon',
targets: [elem],
- rawClasses: 'icon-chevron-right',
- config: {
+ configByClasses: 'icon-chevron-right',
+ componentConfig: {
addToTargetMethod: 'append',
},
});
@@ -111,7 +103,7 @@ export default class Navigation extends ComponentBase {
component.init({
componentName: 'accordion',
targets: [elem],
- config: {
+ componentConfig: {
addToTargetMethod: 'append',
},
nestedComponentsConfig: {
diff --git a/blocks/section-metadata/section-metadata.js b/blocks/section-metadata/section-metadata.js
index 6f12ef74..18994efc 100644
--- a/blocks/section-metadata/section-metadata.js
+++ b/blocks/section-metadata/section-metadata.js
@@ -1,27 +1,30 @@
-import { collectAttributes } from '../../scripts/libs.js';
import ComponentBase from '../../scripts/component-base.js';
+import { stringToArray } from '../../scripts/libs.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')].map(
- (keyCell) => `${keyCell.textContent.trim()}-${keyCell.nextElementSibling.textContent.trim()}`,
- );
+ static observedAttributes = ['class'];
- const { currentAttributes } = collectAttributes(
- 'section-metadata',
- classes,
- SectionMetadata.observedAttributes,
- this,
- );
- const section = this.parentElement;
- Object.keys(currentAttributes).forEach((key) => {
- if (key === 'class') {
- section.setAttribute(key, currentAttributes[key]);
- } else {
- section.setAttribute(`data-${key}`, currentAttributes[key]);
- }
- });
+ 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/theming/theming.js b/blocks/theming/theming.js
index dc2fcd4f..607c005b 100644
--- a/blocks/theming/theming.js
+++ b/blocks/theming/theming.js
@@ -9,11 +9,11 @@ export default class Theming extends ComponentBase {
nestedComponentsConfig = {};
- constructor() {
- super();
+ setDefaults() {
+ super.setDefaults();
this.scapeDiv = document.createElement('div');
// keep as it is
- this.fragment = metaFragment || 'theming.json';
+ this.fragmentPath = metaFragment || 'theming.json';
this.skip = ['tags'];
this.toTags = [
'font-size',
diff --git a/head.html b/head.html
index 9711645d..92697385 100644
--- a/head.html
+++ b/head.html
@@ -12,17 +12,18 @@
});
const headerMeta = document.querySelector('meta[name="header"]');
- const link = document.createElement('link');
- link.setAttribute('rel', 'preload');
- link.setAttribute('as', 'fetch');
- link.setAttribute('crossorigin', 'anonymous');
const url = headerMeta?.content.trim();
- link.href = url ? `${url}.plain.html` : 'header.plain.html';
- document.head.appendChild(link);
+ if (url?.toLowerCase() !== 'false') {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'preload');
+ link.setAttribute('as', 'fetch');
+ link.setAttribute('crossorigin', 'anonymous');
+ link.href = url ? `${url}.plain.html` : 'header.plain.html';
+ document.head.appendChild(link);
+ }
-
diff --git a/scripts/component-base.js b/scripts/component-base.js
index 79bc3e83..fe575a1f 100644
--- a/scripts/component-base.js
+++ b/scripts/component-base.js
@@ -1,23 +1,15 @@
import component from './init.js';
-
-import { getBreakPoints, listenBreakpointChange, camelCaseAttr, capitalizeCaseAttr, deepMerge } from './libs.js';
+import {
+ getBreakPoints,
+ listenBreakpointChange,
+ camelCaseAttr,
+ capitalizeCaseAttr,
+ deepMerge,
+ buildConfig,
+} from './libs.js';
export default class ComponentBase extends HTMLElement {
-
- constructor() {
- super();
- this.uuid = `gen${crypto.randomUUID().split('-')[0]}`;
- this.componentName = null; // set by component loader
- this.webComponentName = null; // set by component loader
- this.fragment = false;
- this.dependencies = [];
- this.breakpoints = getBreakPoints();
- this.initError = null;
- this.attributesValues = {}; // the values are set by the component loader
- this.setConfig('config', 'extendConfig');
- this.setConfig('nestedComponentsConfig', 'extendNestedConfig');
- this.setBinds();
- }
+ static observedAttributes = [];
static loaderConfig = {
targetsSelectorsPrefix: null,
@@ -25,42 +17,74 @@ export default class ComponentBase extends HTMLElement {
selectorTest: null, // a function to filter elements matched by targetsSelectors
targetsSelectorsLimit: null,
targetsAsContainers: false,
+ loaderStopInit() {
+ return false;
+ },
};
- static async earlyStopRender() {
- return false;
+ get Handler() {
+ return window.raqnComponents[this.componentName];
+ }
+
+ constructor() {
+ super();
+ this.setDefaults();
+ this.extendConfigRunner({ config: 'config', method: 'extendConfig' });
+ this.extendConfigRunner({ config: 'nestedComponentsConfig', method: 'extendNestedConfig' });
+ this.setBinds();
}
- attributesValues = {}; // the values are set by the component loader
+ setDefaults() {
+ this.uuid = `gen${crypto.randomUUID().split('-')[0]}`;
+ this.webComponentName = this.tagName.toLowerCase();
+ this.componentName = this.webComponentName.replace(/^raqn-/, '');
+ this.fragmentPath = null;
+ this.dependencies = [];
+ this.attributesValues = {};
+ this.childComponents = {
+ // using the nested feature
+ nestedComponents: [],
+ // from inner html blocks
+ innerComponents: [],
+ };
+ this.nestedComponents = [];
+ this.innerBlocks = [];
+ this.innerComponents = [];
+ this.initError = null;
+ this.breakpoints = getBreakPoints();
- config = {
- hideOnInitError: true,
- hideOnNestedError: false,
- addToTargetMethod: 'replaceWith',
- contentFromTargets: true,
- targetsAsContainers: {
+ // use the this.extendConfig() method to extend the default config
+ this.config = {
+ hideOnInitError: true,
+ hideOnChildrenError: false,
addToTargetMethod: 'replaceWith',
- },
- };
+ contentFromTargets: true,
+ targetsAsContainers: {
+ addToTargetMethod: 'replaceWith',
+ },
+ };
- // Default values are set by component loader
- nestedComponentsConfig = {
- image: {
- componentName: 'image',
- },
- button: {
- componentName: 'button',
- },
- columns: {
- componentName: 'columns',
- active: false,
- loaderConfig: {
- targetsAsContainers: false,
+ // use the this.extendNestedConfig() method to extend the default config
+ this.nestedComponentsConfig = {
+ image: {
+ componentName: 'image',
},
- },
- };
+ button: {
+ componentName: 'button',
+ },
+ columns: {
+ componentName: 'columns',
+ active: false,
+ loaderConfig: {
+ targetsAsContainers: false,
+ },
+ },
+ };
+ }
- setConfig(config, method) {
+ // 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 }) {
const conf = this[method]?.();
if (!conf.length) return;
this[config] = deepMerge({}, ...conf);
@@ -78,13 +102,152 @@ export default class ComponentBase extends HTMLElement {
this.onBreakpointChange = this.onBreakpointChange.bind(this);
}
+ // ! Needs to be called after the element is created;
+ async init(initOptions) {
+ try {
+ this.initOptions = initOptions || {};
+ const { externalConfigName, configByClasses } = this.initOptions;
+
+ this.externalOptions = await buildConfig(
+ this.componentName,
+ externalConfigName,
+ configByClasses,
+ this.Handler.observedAttributes,
+ );
+
+ this.mergeConfigs();
+ this.setAttributesClassesAndProps();
+ this.addDefaultsToNestedConfig();
+ // Add extra functionality to be run on init.
+ await this.onInit();
+ this.addContentFromTarget();
+ await this.connectComponent();
+ } catch (error) {
+ if (initOptions.throwInitError) {
+ throw error;
+ } else {
+ // eslint-disable-next-line no-console
+ console.error(`There was an error while initializing the '${this.componentName}' webComponent:`, this, error);
+ }
+ }
+ }
+
+ async connectComponent() {
+ const { uuid } = this;
+ this.setAttribute('isloading', '');
+ const initialized = new Promise((resolve, reject) => {
+ const initListener = async (event) => {
+ const { error } = event.detail;
+ this.removeEventListener(`initialized:${uuid}`, initListener);
+ this.removeAttribute('isloading');
+ if (error) {
+ reject(error);
+ }
+ resolve(this);
+ };
+ this.addEventListener(`initialized:${uuid}`, initListener);
+ });
+ const { targetsAsContainers } = deepMerge({}, this.Handler.loaderConfig, this.loaderConfig);
+ const conf = this.config;
+ const addToTargetMethod = targetsAsContainers ? conf.targetsAsContainers.addToTargetMethod : conf.addToTargetMethod;
+ this.initOptions.target[addToTargetMethod](this);
+
+ return initialized;
+ }
+
+ // Build-in method called after the element is added to the DOM.
+ async connectedCallback() {
+ try {
+ this.initialized = this.getAttribute('initialized');
+ this.initSubscriptions(); // must subscribe each time the element is added to the document
+ if (!this.initialized) {
+ this.setAttribute('id', this.uuid);
+ this.loadDependencies(); // do not wait for dependencies;
+ await this.loadFragment(this.fragmentPath);
+ await this.connected(); // manipulate/create the html
+ await this.initChildComponents();
+ this.addListeners(); // html is ready add listeners
+ await this.ready(); // add extra functionality
+ this.setAttribute('initialized', true);
+ this.initialized = true;
+ this.dispatchEvent(new CustomEvent(`initialized:${this.uuid}`, { detail: { element: this } }));
+ }
+ } catch (error) {
+ this.dispatchEvent(new CustomEvent(`initialized:${this.uuid}`, { detail: { error } }));
+ this.initError = error;
+ this.hideWithError(this.config.hideOnInitError, 'has-init-error');
+ // eslint-disable-next-line no-console
+ console.error(`There was an error after the '${this.componentName}' webComponent was connected:`, this, error);
+ }
+ }
+
+ mergeConfigs() {
+ this.props = deepMerge({}, this.initOptions.props, this.externalOptions.props);
+
+ this.config = deepMerge({}, this.config, this.initOptions.componentConfig, this.externalOptions.config);
+
+ this.attributesValues = deepMerge(
+ this.attributesValues,
+ this.initOptions.attributesValues,
+ this.externalOptions.attributesValues,
+ );
+
+ this.nestedComponentsConfig = deepMerge(
+ this.nestedComponentsConfig,
+ this.initOptions.nestedComponentsConfig,
+ this.externalOptions.nestedComponentsConfig,
+ );
+ }
+
+ setAttributesClassesAndProps() {
+ Object.entries(this.props).forEach(([prop, value]) => {
+ this[prop] = value;
+ });
+ // Set attributes based on attributesValues
+ Object.entries(this.attributesValues).forEach(([attr, attrValues]) => {
+ const isClass = attr === 'class';
+ const val = (attrValues[this.breakpoints.active.name] ?? attrValues.all);
+ if (isClass) {
+ const classes = (attrValues.all ? `${attrValues.all} ` : '') + (attrValues[this.breakpoints.active.name] ?? '');
+ const classesArr = classes.split(' ').flatMap((cls) => {
+ if (cls) return cls.trim();
+ return [];
+ });
+ if (!classesArr.length) return;
+ this.classList.add(...classesArr);
+ } else {
+ this.dataset[attr] = val;
+ }
+ });
+ }
+
+ addDefaultsToNestedConfig() {
+ Object.keys(this.nestedComponentsConfig).forEach((key) => {
+ const defaults = {
+ targets: [this],
+ active: true,
+ loaderConfig: {
+ targetsAsContainers: true,
+ },
+ };
+ this.nestedComponentsConfig[key] = deepMerge(defaults, this.nestedComponentsConfig[key]);
+ });
+ }
+
+ addContentFromTarget() {
+ const { target } = this.initOptions;
+ const { contentFromTargets } = this.config;
+ if (!contentFromTargets) return;
+
+ this.append(...target.childNodes);
+ }
+
onBreakpointChange(e) {
if (e.matches) {
this.setBreakpointAttributesValues(e);
}
}
- // TODO change to dataset attributes
setBreakpointAttributesValues(e) {
Object.entries(this.attributesValues).forEach(([attribute, breakpointsValues]) => {
const isAttribute = attribute !== 'class';
@@ -140,47 +303,39 @@ export default class ComponentBase extends HTMLElement {
listenBreakpointChange(this.onBreakpointChange);
}
- async connectedCallback() {
- try {
- this.initialized = this.getAttribute('initialized');
- this.initSubscriptions(); // must subscribe each time the element is added to the document
- if (!this.initialized) {
- this.setAttribute('id', this.uuid);
- this.loadDependencies(); // do not wait for dependencies;
- await this.loadFragment(this.fragment);
- await this.connected(); // manipulate/create the html
- await this.initNestedComponents();
- this.addListeners(); // html is ready add listeners
- await this.ready(); // add extra functionality
- this.setAttribute('initialized', true);
- this.initialized = true;
- this.dispatchEvent(new CustomEvent(`initialized:${this.uuid}`, { detail: { element: this } }));
- }
- } catch (error) {
- this.dispatchEvent(new CustomEvent(`initialized:${this.uuid}`, { detail: { error } }));
- this.initError = error;
- this.hideWithError(this.config.hideOnInitError, 'has-nested-error');
- }
+ async initChildComponents() {
+ await Promise.allSettled([this.initNestedComponents(), this.initInnerBlocks()]);
}
async initNestedComponents() {
- const settings = Object.values(this.nestedComponentsConfig).flatMap((setting) => {
+ if (!Object.keys(this.nestedComponentsConfig).length) return;
+ const nestedSettings = Object.values(this.nestedComponentsConfig).flatMap((setting) => {
if (!setting.active) return [];
- return this.fragment
+ return this.innerBlocks.length
? deepMerge({}, setting, {
- // Content can contain blocks which are going to init their own nestedComponents.
+ // 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.
},
})
: setting;
});
- this.nestedComponents = await component.multiInit(settings);
- const {
- nestedComponents: { allInitialized },
- config: { hideOnNestedError },
- } = this;
- this.hideWithError(!allInitialized && hideOnNestedError, 'has-nested-error');
+
+ this.childComponents.nestedComponents = await component.multiInit(nestedSettings);
+
+ const { allInitialized } = this.childComponents.nestedComponents;
+ const { hideOnChildrenError } = this.config;
+ this.hideWithError(!allInitialized && hideOnChildrenError, 'has-nested-error');
+ }
+
+ async initInnerBlocks() {
+ if (!this.innerBlocks.length) return;
+ const innerBlocksSettings = this.innerBlocks.map((block) => ({ targets: [block] }));
+ this.childComponents.innerComponents = await component.multiInit(innerBlocksSettings);
+
+ const { allInitialized } = this.childComponents.innerComponents;
+ const { hideOnChildrenError } = this.config;
+ this.hideWithError(!allInitialized && hideOnChildrenError, 'has-inner-error');
}
async loadDependencies() {
@@ -189,7 +344,7 @@ export default class ComponentBase extends HTMLElement {
}
async loadFragment(path) {
- if (!path) return;
+ if (typeof path !== 'string') return;
const response = await this.getFragment(path);
await this.processFragment(response);
}
@@ -202,11 +357,8 @@ export default class ComponentBase extends HTMLElement {
if (response.ok) {
const html = await response.text();
this.innerHTML = html;
- const fragmentNested = await component.multiInit(
- [...this.querySelectorAll('div[class]')].map((block) => ({ targets: [block] })),
- );
- const { allInitialized } = fragmentNested;
- this.hideWithError(!allInitialized && this.config.hideOnNestedError, 'has-nested-error');
+
+ this.innerBlocks = [...this.querySelectorAll('div[class]')];
}
}
@@ -219,6 +371,8 @@ export default class ComponentBase extends HTMLElement {
initSubscriptions() {}
+ onInit() {}
+
connected() {}
ready() {}
diff --git a/scripts/component-loader.js b/scripts/component-loader.js
index fba668b1..98676370 100644
--- a/scripts/component-loader.js
+++ b/scripts/component-loader.js
@@ -1,7 +1,18 @@
-import { collectAttributes, loadModule, deepMerge, mergeUniqueArrays } from './libs.js';
+import { loadModule, deepMerge, mergeUniqueArrays, getBreakPoints } from './libs.js';
export default class ComponentLoader {
- constructor({ componentName, targets = [], loaderConfig, rawClasses, config, nestedComponentsConfig, active }) {
+ constructor({
+ componentName,
+ targets = [],
+ loaderConfig,
+ configByClasses,
+ attributesValues,
+ externalConfigName,
+ componentConfig,
+ props,
+ nestedComponentsConfig,
+ active,
+ }) {
window.raqnComponents ??= {};
if (!componentName) {
throw new Error('`componentName` is required');
@@ -9,10 +20,14 @@ export default class ComponentLoader {
this.componentName = componentName;
this.targets = targets.map((target) => ({ target }));
this.loaderConfig = loaderConfig;
- this.rawClasses = rawClasses?.trim?.().split?.(' ') || [];
- this.config = config;
+ this.configByClasses = configByClasses?.trim?.().split?.(' ') || [];
+ this.attributesValues = attributesValues;
+ this.externalConfigName = externalConfigName;
+ this.breakpoints = getBreakPoints();
+ this.componentConfig = componentConfig;
this.nestedComponentsConfig = nestedComponentsConfig;
this.pathWithoutExtension = `/blocks/${this.componentName}/${this.componentName}`;
+ this.props = props ?? {};
this.isWebComponent = null;
this.isClass = null;
this.isFn = null;
@@ -41,16 +56,17 @@ export default class ComponentLoader {
if (this.active === false) return [];
if (!this.componentName) return [];
const { loaded, error } = await this.loadAndDefine();
- if (!loaded) throw new Error(error);
+ if (!loaded) throw error;
this.setHandlerType();
- if (await this.Handler?.earlyStopRender?.()) return [];
+ this.loaderConfig = deepMerge({}, this.Handler.loaderConfig, this.loaderConfig);
+ if (await this.loaderConfig?.loaderStopInit?.()) return [];
if (!this.targets?.length) return [];
this.setTargets();
return Promise.allSettled(
- this.targets.map(async (target) => {
+ this.targets.map(async (targetData) => {
let returnVal = null;
- const data = this.getTargetData(target);
+ const data = this.getInitData(targetData);
if (this.isWebComponent) {
returnVal = this.initWebComponent(data);
}
@@ -68,25 +84,22 @@ export default class ComponentLoader {
}
async initWebComponent(data) {
- let returnVal = null;
+ let elem = null;
try {
- const elem = await this.createElementAndConfigure(data);
- data.componentElem = elem;
- returnVal = elem;
- this.addContentFromTarget(data);
- await this.connectComponent(data);
+ elem = await this.createElementAndConfigure(data);
} catch (error) {
- const err = new Error(error);
- err.elem = returnVal;
+ error.elem ??= elem;
+ elem?.classList.add('hide-with-error');
+ elem?.setAttribute('has-loader-error', '');
// eslint-disable-next-line no-console
console.error(
`There was an error while initializing the '${this.componentName}' webComponent:`,
- returnVal,
+ error.elem,
error,
);
- throw err;
+ throw error;
}
- return returnVal;
+ return elem;
}
async initClass(data) {
@@ -112,17 +125,22 @@ export default class ComponentLoader {
}
}
- getTargetData({ target, container }) {
+ getInitData({ target, container }) {
return {
+ throwInitError: true,
target,
container,
- rawClasses: !container ? mergeUniqueArrays(this.rawClasses, target.classList) : this.rawClasses,
- // content: target?.childNodes,
+ configByClasses: !container ? mergeUniqueArrays(this.configByClasses, target.classList) : this.configByClasses,
+ props: this.props,
+ componentConfig: this.componentConfig,
+ externalConfigName: this.externalConfigName,
+ attributesValues: this.attributesValues,
+ nestedComponentsConfig: this.nestedComponentsConfig,
+ loaderConfig: this.loaderConfig,
};
}
setTargets() {
- this.loaderConfig = deepMerge({}, this.Handler.loaderConfig, this.loaderConfig);
const { targetsSelectorsPrefix, targetsSelectors, targetsSelectorsLimit, targetsAsContainers, selectorTest } =
this.loaderConfig;
const selector = `${targetsSelectorsPrefix || ''} ${targetsSelectors}`;
@@ -154,95 +172,33 @@ export default class ComponentLoader {
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,
- this?.Handler?.observedAttributes,
- componentElem,
- );
-
- Object.keys(currentAttributes).forEach((key) => {
- const attr = key === 'class' ? key : `data-${key}`;
- componentElem.setAttribute(attr, 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]);
- });
-
+ try {
+ await componentElem.init(data);
+ } catch (error) {
+ error.elem = componentElem;
+ throw error;
+ }
return componentElem;
}
- addContentFromTarget(data) {
- const { componentElem, target } = data;
- const { contentFromTargets } = componentElem.config;
- if (!contentFromTargets) return;
-
- componentElem.append(...target.childNodes);
- }
-
- async connectComponent(data) {
- const { componentElem } = data;
- const { uuid } = componentElem;
- componentElem.setAttribute('isloading', '');
- const initialized = new Promise((resolve, reject) => {
- const initListener = async (event) => {
- const { error } = event.detail;
- componentElem.removeEventListener(`initialized:${uuid}`, initListener);
- componentElem.removeAttribute('isloading');
- if (error) {
- reject(error);
- }
- resolve(componentElem);
- };
- componentElem.addEventListener(`initialized:${uuid}`, initListener);
- });
- const { targetsAsContainers } = this.loaderConfig;
- const conf = componentElem.config;
- const addToTargetMethod = targetsAsContainers ? conf.targetsAsContainers.addToTargetMethod : conf.addToTargetMethod;
- data.target[addToTargetMethod](componentElem);
-
- return initialized;
- }
-
async loadAndDefine() {
try {
let cssLoaded = Promise.resolve();
- if (!this.Handler) {
- this.Handler = (async () => {
- const { css, js } = loadModule(this.pathWithoutExtension);
- cssLoaded = css;
- const mod = await js;
- if (mod.default.prototype instanceof HTMLElement) {
- window.customElements.define(this.webComponentName, mod.default);
- }
- return mod.default;
- })();
- }
+ this.Handler ??= (async () => {
+ const { css, js } = loadModule(this.pathWithoutExtension);
+ cssLoaded = css;
+ const mod = await js;
+ if (mod.default.prototype instanceof HTMLElement) {
+ window.customElements.define(this.webComponentName, mod.default);
+ }
+ return mod.default;
+ })();
this.Handler = await this.Handler;
await cssLoaded;
return { loaded: true };
} catch (error) {
// eslint-disable-next-line no-console
- console.error(`Failed to load module for ${this.componentName}:`, error);
+ console.error(`Failed to load module for the '${this.componentName}' component:`, error);
return { loaded: false, error };
}
}
diff --git a/scripts/init.js b/scripts/init.js
index 78aea1fd..a8ec1420 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -28,7 +28,7 @@ const component = {
initError: error,
};
// eslint-disable-next-line no-console
- console.error(`There was an error while initializing the ${componentName} component`, error);
+ console.error(`There was an error while initializing the '${componentName}' component`, error);
return init;
}
},
@@ -71,10 +71,11 @@ const component = {
};
const onLoadComponents = {
+ // default content
staticStructureComponents: [
{
componentName: 'image',
- block: document,
+ targets: [document],
loaderConfig: {
targetsAsContainers: true,
targetsSelectorsPrefix: 'main > div >',
@@ -82,7 +83,7 @@ const onLoadComponents = {
},
{
componentName: 'button',
- block: document,
+ targets: [document],
loaderConfig: {
targetsAsContainers: true,
targetsSelectorsPrefix: 'main > div >',
@@ -111,7 +112,7 @@ const onLoadComponents = {
setBlocksData() {
const structureData = this.structureComponents.map(({ componentName }) => ({
componentName,
- block: document,
+ targets: [document],
loaderConfig: {
targetsAsContainers: true,
},
@@ -158,6 +159,7 @@ const onLoadComponents = {
},
initBlocks() {
+ // Keep the page hidden until specific components are initialized to prevent CLS
component.multiInit(this.lcpBlocks).then(() => {
document.body.style.setProperty('display', 'unset');
});
diff --git a/scripts/libs.js b/scripts/libs.js
index 0ab08d5e..2265173f 100644
--- a/scripts/libs.js
+++ b/scripts/libs.js
@@ -115,7 +115,8 @@ export const eagerImage = (block, length = 1) => {
});
};
-export function stringToJsVal(string) {
+export function stringToJsVal(string, options) {
+ const { trim = false } = options || {};
switch (string?.trim().toLowerCase()) {
case 'true':
return true;
@@ -126,10 +127,22 @@ export function stringToJsVal(string) {
case 'undefined':
return undefined;
default:
- return string;
+ return trim ? string.trim() : string;
}
}
+export function stringToArray(val, options) {
+ const { divider = ',' } = options || {};
+ if (typeof val !== 'string') return [];
+ const cleanVal = val.trim().replace(new RegExp(`^${divider}+|${divider}+$`, 'g'), '');
+ if (!cleanVal?.length) return [];
+ return cleanVal.split(divider).flatMap((x) => {
+ const value = x.trim();
+ if (value === '') return [];
+ return [value];
+ });
+}
+
export function getMeta(name, settings) {
const { getArray = false } = settings || {};
const meta = document.querySelector(`meta[name="${name}"]`);
@@ -138,8 +151,7 @@ export function getMeta(name, settings) {
}
const val = stringToJsVal(meta.content);
if (getArray) {
- if (!val?.length) return [];
- return val.split(',').map((x) => x.trim());
+ return stringToArray(val);
}
return val;
}
@@ -153,107 +165,251 @@ export function getMetaGroup(group) {
}));
}
-export function collectAttributes(componentName, classes, knownAttributes = [], element = null) {
- const classesList = [];
- const mediaAttributes = {};
- 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);
+export function isObject(item) {
+ return item && typeof item === 'object' && !Array.isArray(item);
+}
- const attrs = classesList
- .filter((c) => c !== componentName && c !== 'block')
- .reduce((acc, c) => {
- let value = c;
- let isKnownAttribute = null;
+export function isObjectNotWindow(item) {
+ return isObject(item) && item !== window;
+}
- const classBreakpoint = Object.keys(globalConfig.breakpoints).find((b) => c.startsWith(`${b}-`));
- const activeBreakpoint = getBreakPoints().active.name;
+export function deepMerge(origin, ...toMerge) {
+ if (!toMerge.length) return origin;
+ const merge = toMerge.shift();
- if (classBreakpoint) {
- value = value.slice(classBreakpoint.length + 1);
+ 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] });
}
+ });
+ }
- 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;
- }
+ return deepMerge(origin, ...toMerge);
+}
- let key = 'class';
- const isClassValue = value.startsWith(key);
- if (isClassValue) {
- value = value.slice(key.length + 1);
- } else {
- [isKnownAttribute] = knownAttributes.flatMap((attribute) => {
- const noDataPrefix = attribute.replace(/^data-/, '');
- if (!value.startsWith(`${noDataPrefix}-`)) return [];
- return noDataPrefix;
- });
- if (isKnownAttribute) {
- key = isKnownAttribute;
- value = value.slice(isKnownAttribute.length + 1);
+export const externalConfig = {
+ defaultConfig(rawConfig = []) {
+ return { attributesValues: {}, nestedComponentsConfig: {}, props: {}, config: {}, rawConfig };
+ },
+
+ async getConfig(componentName, configName, knownAttributes) {
+ if (!configName) return this.defaultConfig(); // to be removed in the feature and fallback to 'default'
+ const masterConfig = await this.loadConfig();
+ const componentConfig = masterConfig?.[componentName];
+ let parsedConfig = componentConfig?.parsed?.[configName];
+ if (parsedConfig) return parsedConfig;
+ const rawConfig = componentConfig?.data.filter((conf) => conf.configName?.trim() === configName /* ?? 'default' */);
+ if (!rawConfig?.length) {
+ // eslint-disable-next-line no-console
+ console.error(`The config named '${configName}' for '${componentName}' webComponent is not valid.`);
+ return this.defaultConfig();
+ }
+ const safeConfig = JSON.parse(JSON.stringify(rawConfig));
+ parsedConfig = this.parseRawConfig(safeConfig, knownAttributes);
+ componentConfig.parsed ??= {};
+ componentConfig.parsed[configName] = parsedConfig;
+
+ return parsedConfig;
+ },
+
+ async loadConfig() {
+ window.raqnComponentsConfig ??= (async () => {
+ const metaConfigPath = getMeta('component-config');
+ const defaultConfig = 'components-config.json';
+ const configPath = (!!metaConfigPath && `${metaConfigPath}.json`) || defaultConfig;
+ let result = null;
+ try {
+ const response = await fetch(`${configPath}`);
+ if (response.ok) {
+ result = await response.json();
}
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
}
+ return result;
+ })();
- const isClass = key === 'class';
- const camelCaseKey = camelCaseAttr(key);
- if (isKnownAttribute || isClass) attributesValues[camelCaseKey] ??= {};
+ window.raqnComponentsConfig = await window.raqnComponentsConfig;
- // media params always overwrite
- if (classBreakpoint) {
- if (classBreakpoint === activeBreakpoint) {
- mediaAttributes[key] = value;
- }
- if (isKnownAttribute) attributesValues[camelCaseKey][classBreakpoint] = value;
- if (isClass) {
- attributesValues[camelCaseKey][classBreakpoint] ??= '';
- attributesValues[camelCaseKey][classBreakpoint] += `${value} `;
+ return window.raqnComponentsConfig;
+ },
+
+ parseRawConfig(configArr, knownAttributes) {
+ const parsedConfig = configArr?.reduce((acc, breakpointConfig) => {
+ const breakpoint = breakpointConfig.viewport.toLowerCase();
+ const isMainConfig = breakpoint === 'all';
+
+ Object.entries(breakpointConfig).forEach(([key, val]) => {
+ if (val.trim() === '') return;
+
+ const parsedVal = stringToJsVal(val, { trim: true });
+
+ if (knownAttributes.includes(key) || key === 'class') {
+ this.parseAttrValues(parsedVal, acc, key, breakpoint);
+ } else if (isMainConfig) {
+ const configPrefix = 'config-';
+ const propPrefix = 'prop-';
+ if (key.startsWith(configPrefix)) {
+ this.parseConfig(parsedVal, acc, key, configPrefix);
+ } else if (key.startsWith(propPrefix)) {
+ acc.props[key.slice(propPrefix.length)] = parsedVal;
+ } else if (key === 'nest') {
+ this.parseNestedConfig(val, acc);
+ }
}
- // support multivalue attributes
- } else if (acc[key]) {
- acc[key] += ` ${value}`;
- } else {
- acc[key] = value;
- }
+ });
+ return acc;
+ }, this.defaultConfig(configArr));
+
+ return parsedConfig;
+ },
+
+ parseAttrValues(parsedVal, acc, key, breakpoint) {
+ const keyProp = key.replace(/^data-/, '');
+ acc.attributesValues[keyProp] ??= {};
+ acc.attributesValues[keyProp][breakpoint] = parsedVal;
+ },
- if ((isKnownAttribute || isClass) && acc[key]) attributesValues[camelCaseKey].all = acc[key];
+ parseConfig(parsedVal, acc, key, configPrefix) {
+ const configKeys = key.slice(configPrefix.length).split('.');
+ const indexLength = configKeys.length - 1;
+ configKeys.reduce((cof, confKey, index) => {
+ cof[confKey] = index < indexLength ? {} : parsedVal;
+ return cof[confKey];
+ }, acc.config);
+ },
+
+ parseNestedConfig(val, acc) {
+ const parsedVal = stringToArray(val).reduce((nestConf, confVal) => {
+ const [componentName, activeOrConfigName] = confVal.split('=');
+ const parsedActiveOrConfigName = stringToJsVal(activeOrConfigName);
+ const isString = typeof parsedActiveOrConfigName === 'string';
+ nestConf[componentName] ??= {
+ componentName,
+ externalConfigName: isString ? parsedActiveOrConfigName : null,
+ active: isString || parsedActiveOrConfigName,
+ };
+ return nestConf;
+ }, {});
+ acc.nestedComponentsConfig = parsedVal;
+ },
+};
+export const configFromClasses = {
+ getConfig(componentName, configByClasses, knownAttributes) {
+ const nestedComponentsConfig = this.nestedConfigFromClasses(configByClasses);
+ const attributesValues = this.attributeValuesFromClasses(componentName, configByClasses, knownAttributes);
+ return {
+ attributesValues,
+ nestedComponentsConfig,
+ };
+ },
+
+ nestedComponentsNames(configByClasses) {
+ const nestPrefix = 'nest-'; //
+
+ return configByClasses.flatMap((c) => (c.startsWith(nestPrefix) ? [c.slice(nestPrefix.length)] : []));
+ },
+
+ nestedConfigFromClasses(configByClasses) {
+ const nestedComponentsNames = this.nestedComponentsNames(configByClasses);
+ const nestedComponentsConfig = configByClasses.reduce((acc, c) => {
+ let value = c;
+
+ const classBreakpoint = this.classBreakpoint(c);
+ const isBreakpoint = this.isBreakpoint(classBreakpoint);
+
+ if (isBreakpoint) value = value.slice(classBreakpoint.length + 1);
+
+ const componentName = nestedComponentsNames.find((prefix) => value.startsWith(prefix));
+ if (componentName) {
+ acc[componentName] ??= { componentName, active: true };
+ const val = value.slice(componentName.length + 1);
+ const active = 'active-';
+ if (val.startsWith(active)) {
+ acc[componentName].active = stringToJsVal(val.slice(active.length));
+ } else {
+ acc[componentName].configByClasses ??= '';
+ acc[componentName].configByClasses += `${isBreakpoint ? `${classBreakpoint}-` : ''}${val} `;
+ }
+ }
return acc;
}, {});
+ return nestedComponentsConfig;
+ },
- return {
- currentAttributes: {
- ...attrs,
- ...mediaAttributes,
- ...((attrs.class || mediaAttributes.class) && {
- class: `${attrs.class ? attrs.class : ''}${mediaAttributes.class ? ` ${mediaAttributes.class}` : ''}`,
- }),
- },
- attributesValues,
- nestedComponents,
- };
+ attributeValuesFromClasses(componentName, configByClasses, knownAttributes) {
+ const nestedComponentsNames = this.nestedComponentsNames(configByClasses);
+ const onlyKnownAttributes = knownAttributes.filter((a) => a !== 'class');
+ const attributesValues = configByClasses
+ .filter((c) => c !== componentName && c !== 'block')
+ .reduce((acc, c) => {
+ let value = c;
+ let isKnownAttribute = null;
+
+ const classBreakpoint = this.classBreakpoint(c);
+ const isBreakpoint = this.isBreakpoint(classBreakpoint);
+
+ if (isBreakpoint) value = value.slice(classBreakpoint.length + 1);
+
+ const excludeNested = nestedComponentsNames.find((prefix) => value.startsWith(prefix));
+ if (excludeNested) return acc;
+
+ let key = 'class';
+ const isClassValue = value.startsWith(key);
+ if (isClassValue) {
+ value = value.slice(key.length + 1);
+ } else {
+ [isKnownAttribute] = onlyKnownAttributes.flatMap((attribute) => {
+ const noDataPrefix = attribute.replace(/^data-/, '');
+ if (!value.startsWith(`${noDataPrefix}-`)) return [];
+ return noDataPrefix;
+ });
+ if (isKnownAttribute) {
+ key = isKnownAttribute;
+ value = value.slice(isKnownAttribute.length + 1);
+ }
+ }
+
+ const isClass = key === 'class';
+ const camelCaseKey = camelCaseAttr(key);
+ if (isKnownAttribute || isClass) acc[camelCaseKey] ??= {};
+ if (isKnownAttribute) acc[camelCaseKey][classBreakpoint] = value;
+ if (isClass) {
+ acc[camelCaseKey][classBreakpoint] ??= '';
+ acc[camelCaseKey][classBreakpoint] += `${value} `;
+ }
+ return acc;
+ }, {});
+
+ return attributesValues;
+ },
+ classBreakpoint(c) {
+ return Object.keys(globalConfig.breakpoints).find((b) => c.startsWith(`${b}-`)) || 'all';
+ },
+ isBreakpoint(classBreakpoint) {
+ return classBreakpoint !== 'all';
+ },
+};
+
+export async function buildConfig(componentName, externalConf, configByClasses, knownAttributes = []) {
+ const configPrefix = 'config-';
+ let config;
+ const externalConfigName =
+ configByClasses.find((c) => c.startsWith(configPrefix))?.slice?.(configPrefix.length) || externalConf;
+
+ if (externalConfigName) {
+ config = await externalConfig.getConfig(componentName, externalConfigName, knownAttributes);
+ } else {
+ config = configFromClasses.getConfig(componentName, configByClasses, knownAttributes);
+ }
+
+ return config;
}
export function loadModule(urlWithoutExtension) {
@@ -290,29 +446,3 @@ export function getBaseUrl() {
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);
-}