diff --git a/blocks/card/card.css b/blocks/card/card.css
index 45a8ff8f..d3face91 100644
--- a/blocks/card/card.css
+++ b/blocks/card/card.css
@@ -9,7 +9,6 @@ raqn-card {
}
raqn-card > div {
- display: flex;
gap: var(--gap, 20px);
position: relative;
background: var(--inner-background, transparent);
diff --git a/blocks/card/card.js b/blocks/card/card.js
index 8b55be88..7a956d20 100644
--- a/blocks/card/card.js
+++ b/blocks/card/card.js
@@ -8,7 +8,7 @@ export default class Card extends ComponentBase {
attributesValues = {
all: {
data: {
- columns: '4',
+ columns: '3',
ratio: 'auto',
eager: '0',
},
diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css
index ba44b35b..54798d84 100644
--- a/blocks/grid/grid.css
+++ b/blocks/grid/grid.css
@@ -19,12 +19,9 @@ raqn-grid {
display: grid;
/* defaults to 2 columns */
- grid-template-columns: var(--grid-tpl-columns);
- grid-template-rows: var(--grid-tpl-rows);
- grid-template-areas: var(--grid-tpl-areas);
- grid-auto-columns: var(--grid-auto-columns);
- grid-auto-rows: var(--grid-auto-rows);
- gap: var(--gap, 20px);
+ grid-template-columns: var(--grid-template-columns, 1fr 1fr);
+ grid-template-rows: var(--grid-template-rows, 1fr);
+ gap: var(--grid-gap, 20px);
justify-items: var(--grid-justify-items);
align-items: var(--grid-align-items);
justify-content: var(--grid-justify-content);
diff --git a/blocks/grid/grid.editor.js b/blocks/grid/grid.editor.js
new file mode 100644
index 00000000..c4dad629
--- /dev/null
+++ b/blocks/grid/grid.editor.js
@@ -0,0 +1,209 @@
+export default function config() {
+ return {
+ inline: {
+ component: 'InlineEditGridComponent',
+ inputs: [],
+ },
+ attributes: {
+ grid: {
+ 'template-rows': {
+ type: 'text',
+ label: 'Row',
+ helpText: 'The row number.',
+ value: '1fr',
+ },
+ 'template-columns': {
+ type: 'text',
+ label: 'Columns',
+ helpText: 'The column number.',
+ value: '1fr 1fr',
+ },
+ gap: {
+ type: 'text',
+ label: 'Gap',
+ helpText: 'The gap between the grid items.',
+ value: '20px',
+ },
+ },
+ data: {
+ height: {
+ type: 'text',
+ label: 'Height',
+ helpText: 'The height of the grid.',
+ value: 'initial',
+ },
+ width: {
+ type: 'text',
+ label: 'Width',
+ helpText: 'The width of the grid.',
+ value: 'auto',
+ },
+ reverse: {
+ type: 'select',
+ options: [
+ {
+ label: 'Default',
+ value: 'default',
+ },
+ {
+ label: 'True',
+ value: 'true',
+ },
+ {
+ label: 'Alternate',
+ value: 'alternate',
+ },
+ ],
+ label: 'Reverse',
+ helpText: 'Reverse the order of the grid items.',
+ },
+ columns: {
+ type: 'text',
+ label: 'Columns',
+ helpText: 'Number of columns in the grid.',
+ value: 'auto',
+ },
+ rows: {
+ type: 'text',
+ label: 'Rows',
+ helpText: 'Number of rows in the grid.',
+ value: 'auto',
+ },
+ 'auto-columns': {
+ type: 'text',
+ label: 'Auto Columns',
+ helpText: 'The width of the columns.',
+ value: 'auto',
+ },
+ 'auto-rows': {
+ type: 'text',
+ label: 'Auto Rows',
+ helpText: 'The height of the rows.',
+ value: 'auto',
+ },
+ areas: {
+ type: 'text',
+ label: 'Areas',
+ helpText: 'The grid areas.',
+ value: '',
+ },
+ 'justify-items': {
+ type: 'select',
+ options: [
+ {
+ label: 'Start',
+ value: 'start',
+ },
+ {
+ label: 'End',
+ value: 'end',
+ },
+ {
+ label: 'Center',
+ value: 'center',
+ },
+ {
+ label: 'Stretch',
+ value: 'stretch',
+ },
+ ],
+ label: 'Justify Items',
+ helpText: 'The alignment of the items along the inline (row) axis.',
+ },
+ 'align-items': {
+ type: 'select',
+ options: [
+ {
+ label: 'Start',
+ value: 'start',
+ },
+ {
+ label: 'End',
+ value: 'end',
+ },
+ {
+ label: 'Center',
+ value: 'center',
+ },
+ {
+ label: 'Stretch',
+ value: 'stretch',
+ },
+ ],
+ label: 'Align Items',
+ helpText: 'The alignment of the items along the block (column) axis.',
+ },
+ 'justify-content': {
+ type: 'select',
+ options: [
+ {
+ label: 'Start',
+ value: 'start',
+ },
+ {
+ label: 'End',
+ value: 'end',
+ },
+ {
+ label: 'Center',
+ value: 'center',
+ },
+ {
+ label: 'Stretch',
+ value: 'stretch',
+ },
+ {
+ label: 'Space Around',
+ value: 'space-around',
+ },
+ {
+ label: 'Space Between',
+ value: 'space-between',
+ },
+ {
+ label: 'Space Evenly',
+ value: 'space-evenly',
+ },
+ ],
+ label: 'Justify Content',
+ helpText: 'The alignment of the grid along the inline (row) axis.',
+ },
+ 'align-content': {
+ type: 'select',
+ options: [
+ {
+ label: 'Start',
+ value: 'start',
+ },
+ {
+ label: 'End',
+ value: 'end',
+ },
+ {
+ label: 'Center',
+ value: 'center',
+ },
+ {
+ label: 'Stretch',
+ value: 'stretch',
+ },
+ {
+ label: 'Space Around',
+ value: 'space-around',
+ },
+ {
+ label: 'Space Between',
+ value: 'space-between',
+ },
+ {
+ label: 'Space Evenly',
+ value: 'space-evenly',
+ },
+ ],
+ label: 'Align Content',
+ helpText: 'The alignment of the grid along the block (column) axis.',
+ },
+ },
+ },
+ };
+}
diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js
index b46a68d5..3845b786 100644
--- a/blocks/grid/grid.js
+++ b/blocks/grid/grid.js
@@ -1,48 +1,40 @@
import ComponentBase from '../../scripts/component-base.js';
-import { stringToJsVal } from '../../scripts/libs.js';
-import component from '../../scripts/init.js';
+import { flat, stringToJsVal } from '../../scripts/libs.js';
export default class Grid extends ComponentBase {
- static observedAttributes = [
- 'data-level',
- 'data-height',
- 'data-width',
- 'data-reverse',
- 'data-columns', // value can be any valid css value or a number which creates as many equal columns
- 'data-rows', // value can be any valid css value or a number which creates as many equal rows
- 'data-auto-columns',
- 'data-auto-rows',
- 'data-areas',
- 'data-justify-items',
- 'data-align-items',
- 'data-justify-content',
- 'data-align-content',
- ];
-
- nestedComponentsConfig = {};
+ // only one attribute is observed rest is set as css variables directly
+ static observedAttributes = ['data-reverse'];
attributesValues = {
all: {
- data: {
- level: 1,
+ grid: {
+ gap: '20px',
},
},
};
- setDefaults() {
- super.setDefaults();
- }
-
- get gridItems() {
- return [...this.children];
- }
-
- onAttributeHeightChanged({ oldValue, newValue }) {
- this.setStyleProp('height', oldValue, newValue);
+ // use `grid` item as action from component base and apply those as css variables
+ // dinamic from {@link ../scripts/component-base.js:runConfigsByViewports}
+ // EG ${viewport}-grid-${attr}-"${value}"
+ applyGrid(grid) {
+ const f = flat(grid);
+ Object.keys(f).forEach((key) => {
+ this.style.setProperty(`--grid-${key}`, f[key]);
+ });
}
- onAttributeWidthChanged({ oldValue, newValue }) {
- this.setStyleProp('width', oldValue, newValue);
+ // for backwards compatibility
+ applyData(data) {
+ ['columns', 'rows'].forEach((key) => {
+ if (data[key]) {
+ if (data.template) {
+ data.template[key] = data[key];
+ } else {
+ data.template = { [key]: data[key] };
+ }
+ }
+ });
+ this.applyGrid(data);
}
async onAttributeReverseChanged({ oldValue, newValue }) {
@@ -54,17 +46,16 @@ export default class Grid extends ComponentBase {
const val = stringToJsVal(newValue);
const reverse = val === true || newValue === 'alternate';
- let items = this.gridItems;
+ let items = [...this.children];
switch (val) {
case true:
- items = [...this.gridItems].reverse();
+ items = [...this.children].reverse();
break;
case 'alternate':
items = this.alternateReverse();
break;
default:
- items = this.gridItems;
break;
}
@@ -79,7 +70,7 @@ export default class Grid extends ComponentBase {
// swaps every 2 items [1,2,3,4,5,6,7] => [2,1,4,3,6,5,7]
alternateReverse() {
- return this.gridItems.reduce((acc, x, i, arr) => {
+ return [...this.children].reduce((acc, x, i, arr) => {
if ((i + 1) % 2) {
if (arr.length === i + 1) acc.push(x);
return acc;
@@ -88,189 +79,4 @@ export default class Grid extends ComponentBase {
return acc;
}, []);
}
-
- onAttributeColumnsChanged({ oldValue, newValue }) {
- this.setRowsOrColumns('columns', oldValue, newValue);
- }
-
- onAttributeRowsChanged({ oldValue, newValue }) {
- this.setRowsOrColumns('rows', oldValue, newValue);
- }
-
- setRowsOrColumns(name, oldValue, newValue) {
- if (oldValue === newValue) return;
-
- const tplCol = `--grid-tpl-${name}`;
- const col = `--grid-${name}`;
-
- if (!newValue) {
- this.style.removeProperty(tplCol);
- this.style.removeProperty(col);
- return;
- }
-
- const nrOfCols = Number(newValue);
-
- if (nrOfCols) {
- this.style.removeProperty(tplCol);
- this.style.setProperty(col, nrOfCols);
- return;
- }
-
- this.style.removeProperty(col);
- this.style.setProperty(tplCol, newValue.replace(/-+/g, ' '));
- }
-
- onAttributeAutoColumnsChanged({ oldValue, newValue }) {
- this.setStyleProp('auto-columns', oldValue, newValue);
- }
-
- onAttributeAutoRowsChanged({ oldValue, newValue }) {
- this.setStyleProp('auto-rows', oldValue, newValue);
- }
-
- /**
- * Grid areas names should be defined using the following pattern:
- * `item-{index}`
- * where index is the grid item position starting from 1 as a child element of the grid;
- *
- * The grid-template-area should be defined with an equal amount of areas as the number of grid items.
- *
- * The grid-area with the same pattern is automatically set on the grid item when grid-template-area is set
- * on the grid using data-areas attribute;
- */
- async onAttributeAreasChanged({ oldValue, newValue }) {
- // await for initialization because access to this.gridItems is required;
- await this.initialization;
- const cleanValue = newValue.replace(/"\s+"/g, '" "').replace(/\n+|^\s+|\s+$/g, '');
-
- // For validation check if the areas template includes all grid items
- const missingItems = [];
-
- this.gridItems.forEach((item) => {
- const areaCheck = cleanValue?.includes(item.areaName);
- item.setAutoAreaName(!!cleanValue && areaCheck);
-
- if (cleanValue && !areaCheck) {
- missingItems.push(item.areaName);
- }
- });
-
- if (missingItems.length) {
- // eslint-disable-next-line no-console
- console.warn(`The following items are not included in the areas template: ${missingItems.join(',')}`, this);
- }
-
- this.setStyleProp('tpl-areas', oldValue, cleanValue);
- }
-
- onAttributeJustifyItemsChanged({ oldValue, newValue }) {
- this.setStyleProp('justify-items', oldValue, newValue);
- }
-
- onAttributeAlignItemsChanged({ oldValue, newValue }) {
- this.setStyleProp('align-items', oldValue, newValue);
- }
-
- onAttributeJustifyContentChanged({ oldValue, newValue }) {
- this.setStyleProp('justify-content', oldValue, newValue);
- }
-
- onAttributeAlignContentChanged({ oldValue, newValue }) {
- this.setStyleProp('align-content', oldValue, newValue);
- }
-
- setStyleProp(name, oldValue, newValue) {
- if (oldValue === newValue) return;
- const prop = `--grid-${name}`;
- if (newValue) {
- this.style.setProperty(prop, newValue);
- } else {
- this.style.removeProperty(prop);
- }
- }
-
- async connected() {
- await this.collectGridItemsFromBlocks();
- }
-
- ready() {
- this.cleanGridItems();
- }
-
- cleanGridItems() {
- // Get all the grid items and remove any non grid item element.
- return [...this.children].filter((child) => child.matches('raqn-grid-item') || child.remove());
- }
-
- async collectGridItemsFromBlocks() {
- if (!this.isInitAsBlock) return;
-
- await this.recursiveItems(this.nextElementSibling);
- }
-
- async recursiveItems(elem, children = []) {
- if (!elem) return;
- if (this.isForbiddenGridItem(elem)) return;
- if (this.isForbiddenBlockGrid(elem)) return;
- if (this.isForbiddenRaqnGrid(elem)) return;
-
- if (this.isThisGridItem(elem)) {
- await this.createGridItem([...children], [...elem.classList]);
- await this.recursiveItems(elem.nextElementSibling, []);
- elem.remove();
- return;
- }
-
- children.push(elem);
-
- await this.recursiveItems(elem.nextElementSibling, children);
- }
-
- getLevel(elem = this) {
- return Number(elem.dataset.level);
- }
-
- getLevelFromClass(elem) {
- const levelClass = [...elem.classList].find((cls) => cls.startsWith('data-level-')) || 'data-level-1';
- return Number(levelClass.slice('data-level-'.length));
- }
-
- isGridItem(elem) {
- return elem.tagName === 'DIV' && elem.classList.contains('grid-item');
- }
-
- isThisGridItem(elem) {
- return this.isGridItem(elem) && this.getLevelFromClass(elem) === this.getLevel();
- }
-
- isForbiddenGridItem(elem) {
- return this.isGridItem(elem) && this.getLevelFromClass(elem) > this.getLevel();
- }
-
- isBlockGrid(elem) {
- return elem.tagName === 'DIV' && elem.classList.contains('grid');
- }
-
- isRaqnGrid(elem) {
- return elem.tagName === 'RAQN-GRID';
- }
-
- isForbiddenRaqnGrid(elem) {
- return this.isRaqnGrid(elem) && this.getLevel() >= this.getLevel(elem);
- }
-
- isForbiddenBlockGrid(elem) {
- return this.isBlockGrid(elem) && this.getLevelFromClass(elem) <= this.getLevel();
- }
-
- async createGridItem(children, configByClasses) {
- await component.loadAndDefine('grid-item');
- const tempGridItem = document.createElement('raqn-grid-item');
- tempGridItem.init({ configByClasses });
- tempGridItem.gridParent = this;
- tempGridItem.append(...children);
- this.gridItems.push(tempGridItem);
- this.append(tempGridItem);
- }
}
diff --git a/blocks/layout/layout.css b/blocks/layout/layout.css
new file mode 100644
index 00000000..5a38840a
--- /dev/null
+++ b/blocks/layout/layout.css
@@ -0,0 +1,3 @@
+raqn-grid {
+ display: grid;
+}
diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js
index 01352a95..bab70441 100644
--- a/blocks/navigation/navigation.js
+++ b/blocks/navigation/navigation.js
@@ -1,4 +1,3 @@
-import component from '../../scripts/init.js';
import { blockBodyScroll } from '../../scripts/libs.js';
import ComponentBase from '../../scripts/component-base.js';
@@ -72,7 +71,6 @@ export default class Navigation extends ComponentBase {
async setupCompactedNav() {
if (!this.navCompactedContentInit) {
this.navCompactedContentInit = true;
- await component.multiLoadAndDefine(['accordion', 'icon']);
this.setupClasses(this.navCompactedContent, true);
this.navCompactedContent.addEventListener('click', (e) => this.activate(e));
}
diff --git a/blocks/popup-trigger/popup-trigger.js b/blocks/popup-trigger/popup-trigger.js
index 010006c0..5dc729d6 100644
--- a/blocks/popup-trigger/popup-trigger.js
+++ b/blocks/popup-trigger/popup-trigger.js
@@ -1,5 +1,5 @@
import ComponentBase from '../../scripts/component-base.js';
-import component from '../../scripts/init.js';
+
import { popupState } from '../../scripts/libs.js';
export default class PopupTrigger extends ComponentBase {
@@ -131,9 +131,7 @@ export default class PopupTrigger extends ComponentBase {
}
async createPopup() {
- await component.loadAndDefine('popup');
const popup = document.createElement('raqn-popup');
-
popup.dataset.url = this.popupSourceUrl;
popup.dataset.active = true;
// Set the popupTrigger property of the popup component to this trigger instance
diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js
index 6acd93d0..d8af4307 100644
--- a/blocks/theming/theming.js
+++ b/blocks/theming/theming.js
@@ -8,6 +8,7 @@ import {
metaTags,
readValue,
unFlat,
+ getBaseUrl,
} from '../../scripts/libs.js';
const k = Object.keys;
@@ -38,16 +39,22 @@ export default class Theming extends ComponentBase {
this.fontFace = names
.map((key) => {
// files
+
const types = Object.keys(data[key].options);
return types
- .map(
- (type) => `@font-face {
+ .map((type) => {
+ document.head.insertAdjacentHTML(
+ 'beforeend',
+ ``,
+ );
+ return `@font-face {
+ font-display: fallback;
font-family: '${key}';
src: url('${window.location.origin}/fonts/${data[key].options[type]}');
${type === 'italic' ? 'font-style' : 'font-weight'}: ${type};
}
- `,
- )
+ `;
+ })
.join('');
})
.join('');
@@ -166,14 +173,18 @@ export default class Theming extends ComponentBase {
async loadFragment() {
const themeConfigs = getMetaGroup(metaTags.themeConfig.metaNamePrefix);
-
+ const base = getBaseUrl();
await Promise.allSettled(
- themeConfigs.map(async ({ name, content }) =>
- fetch(`${content}.json`).then((response) => this.processFragment(response, name)),
- ),
+ themeConfigs.map(async ({ name, content }) => {
+ const response = await fetch(`${name !== 'fontface' ? base : ''}${content}.json`);
+ return this.processFragment(response, name);
+ }),
);
this.defineVariations();
this.styles();
+ setTimeout(() => {
+ document.body.style.display = 'block';
+ });
}
}
diff --git a/head.html b/head.html
index a0c21054..f6a4ce33 100644
--- a/head.html
+++ b/head.html
@@ -1,22 +1,24 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/scripts/component-base.js b/scripts/component-base.js
index a3b749dd..db05d9a4 100644
--- a/scripts/component-base.js
+++ b/scripts/component-base.js
@@ -1,6 +1,4 @@
-import component from './init.js';
import {
- globalConfig,
getBreakPoints,
listenBreakpointChange,
camelCaseAttr,
@@ -12,15 +10,83 @@ import {
flatAsValue,
flat,
mergeUniqueArrays,
- getBlocksAndGrids,
} from './libs.js';
import { externalConfig } from './libs/external-config.js';
+import { generalManipulation, generateDom, renderVirtualDom } from './render/dom.js';
export default class ComponentBase extends HTMLElement {
// All supported data attributes must be added to observedAttributes
// The order of observedAttributes is the order in which the values from config are added.
static observedAttributes = [];
+ dataAttributesKeys = [];
+
+ uuid = `gen${crypto.randomUUID().split('-')[0]}`;
+
+ webComponentName = this.tagName.toLowerCase();
+
+ componentName = this.webComponentName.replace(/^raqn-/, '');
+
+ overrideExternalConfig = false;
+
+ wasInitBeforeConnected = false;
+
+ fragmentPath = null;
+
+ fragmentCache = 'default';
+
+ dependencies = [];
+
+ attributesValues = {};
+
+ initOptions = {};
+
+ externalOptions = {};
+
+ elements = {};
+
+ childComponents = {
+ // using the nested feature
+ nestedComponents: [],
+ // from inner html blocks
+ innerComponents: [],
+ // from inner html blocks
+ innerGrids: [],
+ };
+
+ // set only if content is loaded externally
+ innerBlocks = [];
+
+ // set only if content is loaded externally
+ innerGrids = [];
+
+ initError = null;
+
+ breakpoints = getBreakPoints();
+
+ // use the extendConfig() method to extend the default config
+ config = {
+ listenBreakpoints: false,
+ hideOnInitError: true,
+ hideOnChildrenError: false,
+ addToTargetMethod: 'replaceWith',
+ contentFromTargets: true,
+ targetsAsContainers: {
+ addToTargetMethod: 'replaceWith',
+ contentFromTargets: true,
+ },
+ };
+
+ // use the extendNestedConfig() method to extend the default config
+ nestedComponentsConfig = {
+ image: {
+ componentName: 'image',
+ },
+ button: {
+ componentName: 'button',
+ },
+ };
+
static loaderConfig = {
targetsSelectorsPrefix: null,
targetsSelectors: null,
@@ -49,58 +115,7 @@ export default class ComponentBase extends HTMLElement {
this.setBinds();
}
- setDefaults() {
- this.uuid = `gen${crypto.randomUUID().split('-')[0]}`;
- this.webComponentName = this.tagName.toLowerCase();
- this.componentName = this.webComponentName.replace(/^raqn-/, '');
- this.overrideExternalConfig = false;
- this.wasInitBeforeConnected = false;
- this.fragmentPath = null;
- this.fragmentCache = 'default';
- this.dependencies = [];
- this.attributesValues = {};
- this.initOptions = {};
- this.externalOptions = {};
- this.elements = {};
- this.childComponents = {
- // using the nested feature
- nestedComponents: [],
- // from inner html blocks
- innerComponents: [],
- // from inner html blocks
- innerGrids: [],
- };
- // set only if content is loaded externally
- this.innerBlocks = [];
- // set only if content is loaded externally
- this.innerGrids = [];
- this.initError = null;
- this.breakpoints = getBreakPoints();
- this.dataAttributesKeys = this.setDataAttributesKeys();
-
- // use the this.extendConfig() method to extend the default config
- this.config = {
- listenBreakpoints: false,
- hideOnInitError: true,
- hideOnChildrenError: false,
- addToTargetMethod: 'replaceWith',
- contentFromTargets: true,
- targetsAsContainers: {
- addToTargetMethod: 'replaceWith',
- contentFromTargets: true,
- },
- };
-
- // use the this.extendNestedConfig() method to extend the default config
- this.nestedComponentsConfig = {
- image: {
- componentName: 'image',
- },
- button: {
- componentName: 'button',
- },
- };
- }
+ setDefaults() {}
setInitializationPromise() {
this.initialization = new Promise((resolve, reject) => {
@@ -134,8 +149,8 @@ export default class ComponentBase extends HTMLElement {
this.onBreakpointChange = this.onBreakpointChange.bind(this);
}
- async setDataAttributesKeys() {
- const { observedAttributes } = await this.Handler;
+ setDataAttributesKeys() {
+ const { observedAttributes } = this.constructor;
this.dataAttributesKeys = observedAttributes.map((dataAttr) => {
const [, key] = dataAttr.split('data-');
@@ -176,14 +191,23 @@ export default class ComponentBase extends HTMLElement {
* use the data attr values as default for attributesValues
*/
setInitialAttributesValues() {
- const initialAttributesValues = { all: { data: {} } };
-
+ const initial = [...this.classList];
+ initial.unshift(); // remove the component name
+ this.initialAttributesValues = classToFlat(initial.splice(1));
+ const initialAttributesValues = this.initialAttributesValues || { all: { data: {} } };
+ if (this.dataAttributesKeys && !this.dataAttributesKeys.length) {
+ this.setDataAttributesKeys();
+ } else {
+ this.dataAttributesKeys = this.dataAttributesKeys || [];
+ }
this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => {
const value = this.dataset[noDataCamelCase];
if (typeof value === 'undefined') return {};
const initialValue = unFlat({ [noData]: value });
- initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue);
+ if (initialAttributesValues.all && initialAttributesValues.all.data) {
+ initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue);
+ }
return initialAttributesValues;
});
@@ -217,10 +241,9 @@ export default class ComponentBase extends HTMLElement {
if (!this.initialized) {
await this.initOnConnected();
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.dataAttributesKeys = await this.setDataAttributesKeys();
this.addListeners(); // html is ready add listeners
await this.ready(); // add extra functionality
this.setAttribute('initialized', true);
@@ -352,7 +375,9 @@ export default class ComponentBase extends HTMLElement {
// received as {col:{ direction:2 }, columns: 2}
const values = flat(entries);
// transformed into values as {col-direction: 2, columns: 2}
-
+ if (!this.dataAttributesKeys) {
+ this.setDataAttributesKeys();
+ }
// Add only supported data attributes from observedAttributes;
// Sometimes the order in which the attributes are set matters.
// Control the order by using the order of the observedAttributes.
@@ -437,57 +462,6 @@ export default class ComponentBase extends HTMLElement {
}
}
- async initChildComponents() {
- await Promise.allSettled([this.initNestedComponents(), this.initInnerBlocks()]);
- await this.initInnerGrids();
- }
-
- async initNestedComponents() {
- if (!Object.keys(this.nestedComponentsConfig).length) return;
- const nestedSettings = Object.values(this.nestedComponentsConfig).flatMap((setting) => {
- if (!setting.active) return [];
- return this.innerBlocks.length
- ? deepMerge({}, setting, {
- // Exclude nested components query from innerBlocks. Inner Components will query their own nested components.
- loaderConfig: {
- targetsSelectorsPrefix: ':scope > div >', // Limit only to default content, exclude blocks.
- },
- })
- : setting;
- });
-
- 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;
-
- this.childComponents.innerComponents = await component.multiInit(this.innerBlocks);
-
- const { allInitialized } = this.childComponents.innerComponents;
- const { hideOnChildrenError } = this.config;
- this.hideWithError(!allInitialized && hideOnChildrenError, 'has-inner-error');
- }
-
- async initInnerGrids() {
- if (!this.innerGrids.length) return;
-
- this.childComponents.innerGrids = await component.multiSequentialInit(this.innerGrids);
-
- const { allInitialized } = this.childComponents.innerGrids;
- const { hideOnChildrenError } = this.config;
- this.hideWithError(!allInitialized && hideOnChildrenError, 'has-inner-error');
- }
-
- async loadDependencies() {
- if (!this.dependencies.length) return;
- component.multiLoadAndDefine(this.dependencies);
- }
-
async loadFragment(path) {
if (typeof path !== 'string') return;
const response = await this.getFragment(path);
@@ -500,23 +474,15 @@ export default class ComponentBase extends HTMLElement {
async processFragment(response) {
if (response.ok) {
- this.fragmentContent = response.text();
+ this.fragmentContent = await response.text();
await this.addFragmentContent();
- this.setInnerBlocksAndGrids();
}
}
async addFragmentContent() {
- this.innerHTML = await this.fragmentContent;
- }
-
- // Set only if content is loaded externally;
- setInnerBlocksAndGrids() {
- const { blocks, grids } = getBlocksAndGrids(
- [...this.querySelectorAll(globalConfig.blockSelector)].map((elem) => component.getBlockData(elem)),
- );
- this.innerBlocks = blocks;
- this.innerGrids = grids;
+ const element = document.createElement('div');
+ element.innerHTML = this.fragmentContent;
+ this.append(...renderVirtualDom(generalManipulation(generateDom(element.childNodes))));
}
queryElements() {
diff --git a/scripts/component-loader.js b/scripts/component-loader.js
index 3d935868..e69de29b 100644
--- a/scripts/component-loader.js
+++ b/scripts/component-loader.js
@@ -1,212 +0,0 @@
-import { loadModule, deepMerge, mergeUniqueArrays, getBreakPoints } from './libs.js';
-
-window.raqnInstances = window.raqnInstances || {};
-
-export default class ComponentLoader {
- constructor({
- componentName,
- targets = [],
- loaderConfig,
- configByClasses,
- attributesValues,
- externalConfigName,
- componentConfig,
- props,
- nestedComponentsConfig,
- active,
- }) {
- window.raqnComponents ??= {};
- if (!componentName) {
- throw new Error('`componentName` is required');
- }
- this.instances = window.raqnInstances || {};
- this.componentName = componentName;
- this.targets = targets.map((target) => ({ target }));
- this.loaderConfig = loaderConfig;
- 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;
- this.active = active;
- }
-
- get Handler() {
- return window.raqnComponents[this.componentName];
- }
-
- set Handler(handler) {
- window.raqnComponents[this.componentName] = handler;
- }
-
- 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.componentName.toLowerCase()}`;
- }
-
- async init() {
- if (this.active === false) return [];
- if (!this.componentName) return [];
- const { loaded, error } = await this.loadAndDefine();
- if (!loaded) throw error;
- this.setHandlerType();
- 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 (targetData) => {
- let returnVal = null;
- const data = this.getInitData(targetData);
- if (this.isWebComponent) {
- returnVal = this.initWebComponent(data);
- }
-
- if (this.isClass) {
- returnVal = this.initClass(data);
- }
-
- if (this.isFn) {
- returnVal = this.initFn(data);
- }
- return returnVal;
- }),
- );
- }
-
- async initWebComponent(data) {
- let elem = null;
- try {
- elem = await this.createElementAndConfigure(data);
- elem.webComponentName = this.webComponentName;
- this.instances[elem.componentName] = this.instances[elem.componentName] || [];
- this.instances[elem.componentName].push(elem);
- } catch (error) {
- error.elem ??= elem;
- elem?.classList.add('hide-with-error');
- elem?.setAttribute('has-loader-error', '');
- // eslint-disable-next-line no-console
- console.error(
- `There was an error while initializing the '${this.componentName}' webComponent:`,
- error.elem,
- error,
- );
- throw error;
- }
- return elem;
- }
-
- async initClass(data) {
- try {
- return new this.Handler({
- componentName: this.componentName,
- ...data,
- });
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error(`There was an error while initializing the '${this.componentName}' class:`, data.target, error);
- throw error;
- }
- }
-
- async initFn(data) {
- try {
- return this.Handler(data);
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error(`There was an error while initializing the '${this.componentName}' function:`, data.target, error);
- throw error;
- }
- }
-
- getInitData({ target, container }) {
- return {
- throwInitError: true,
- target,
- container,
- configByClasses:
- !container && target ? 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() {
- 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);
- try {
- await componentElem.init(data);
- } catch (error) {
- error.elem = componentElem;
- throw error;
- }
- return componentElem;
- }
-
- async loadAndDefine() {
- try {
- let cssLoaded = Promise.resolve();
- 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 the '${this.componentName}' component:`, error);
- return { loaded: false, error };
- }
- }
-}
diff --git a/scripts/editor-preview.js b/scripts/editor-preview.js
index 6774a402..9ef18877 100644
--- a/scripts/editor-preview.js
+++ b/scripts/editor-preview.js
@@ -1,27 +1,29 @@
-import ComponentLoader from './component-loader.js';
+// import { publish } from './pubsub.js';
import { deepMerge } from './libs.js';
import { publish } from './pubsub.js';
+import { generateDom, manipulation, renderVirtualDom } from './render/dom.js';
export default async function preview(component, classes, uuid) {
- const { componentName } = component;
- const header = document.querySelector('header');
- const footer = document.querySelector('footer');
- const main = document.querySelector('main');
- main.innerHTML = '';
-
- if (header) {
- header.parentNode.removeChild(header);
- }
- if (footer) {
- footer.parentNode.removeChild(footer);
- }
- const loader = new ComponentLoader({ componentName });
- await loader.init();
+ document.body.innerHTML = '';
+ const main = document.createElement('main');
const webComponent = document.createElement(component.webComponentName);
webComponent.overrideExternalConfig = true;
webComponent.innerHTML = component.html;
- webComponent.attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues);
main.appendChild(webComponent);
+ const virtualdom = generateDom(main.childNodes);
+ virtualdom[0].attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues);
+
+ main.innerHTML = '';
+ document.body.append(main);
+ await main.append(...renderVirtualDom(manipulation(virtualdom)));
+
+ webComponent.style.display = 'inline-grid';
+ webComponent.style.width = 'auto';
+ webComponent.style.marginInlineStart = '0px';
+ // webComponent.runConfigsByViewport();
+ await document.body.style.setProperty('display', 'block');
+ await main.style.setProperty('display', 'block');
+ await window.getComputedStyle(document.body);
window.addEventListener(
'click',
@@ -31,15 +33,8 @@ export default async function preview(component, classes, uuid) {
},
true,
);
-
- webComponent.style.display = 'inline-grid';
- webComponent.style.width = 'auto';
- webComponent.style.marginInlineStart = '0px';
- webComponent.runConfigsByViewport();
- document.body.style.setProperty('display', 'block');
- main.style.setProperty('display', 'block');
- setTimeout(() => {
- const bodyRect = webComponent.getBoundingClientRect();
+ setTimeout(async () => {
+ const bodyRect = await document.body.getBoundingClientRect();
publish('raqn:editor:preview:render', { bodyRect, uuid }, { usePostMessage: true, targetOrigin: '*' });
- }, 100);
+ }, 250);
}
diff --git a/scripts/editor.js b/scripts/editor.js
index b75057a3..2a9b92f7 100644
--- a/scripts/editor.js
+++ b/scripts/editor.js
@@ -1,4 +1,4 @@
-import { deepMerge, getBaseUrl, loadModule } from './libs.js';
+import { deepMerge, flat, getBaseUrl, loadModule } from './libs.js';
import { publish } from './pubsub.js';
window.raqnEditor = window.raqnEditor || {};
@@ -17,12 +17,11 @@ export const MessagesEvents = {
};
export function refresh(id) {
- Object.keys(window.raqnEditor).forEach((name) => {
- const { webComponentName } = window.raqnInstances[name][0];
+ Object.keys(window.raqnEditor).forEach((webComponentName) => {
const instancesOrdered = Array.from(document.querySelectorAll(webComponentName));
- window.raqnEditor[name].instances = instancesOrdered.map((item) =>
+ window.raqnEditor[webComponentName].instances = instancesOrdered.map((item) =>
// eslint-disable-next-line no-use-before-define
- getComponentValues(window.raqnEditor[name].dialog, item),
+ getComponentValues(window.raqnEditor[webComponentName].dialog, item),
);
});
const bodyRect = window.document.body.getBoundingClientRect();
@@ -34,8 +33,8 @@ export function refresh(id) {
}
export function updateComponent(component) {
- const { componentName, uuid } = component;
- const instance = window.raqnInstances[componentName].find((element) => element.uuid === uuid);
+ const { webComponentName, uuid } = component;
+ const instance = window.raqnComponents[webComponentName].instances.find((element) => element.uuid === uuid);
if (!instance) return;
instance.attributesValues = deepMerge({}, instance.attributesValues, component.attributesValues);
@@ -54,8 +53,23 @@ export function getComponentValues(dialog, element) {
return data;
}, {});
attributes = Object.keys(attributes).reduce((data, attribute) => {
- const value = element.getAttribute(attribute);
+ if (attribute === 'data') {
+ const flatData = flat(element.dataset);
+ Object.keys(flatData).forEach((key) => {
+ const value = flatData[key];
+ if (attributes[attribute] && attributes[attribute][key]) {
+ if (data[attribute]) {
+ const extend = { ...attributes[attribute][key], value };
+ data[attribute][key] = extend;
+ } else {
+ data[attribute] = { [key]: { ...attributes[attribute][key], value } };
+ }
+ }
+ });
+ return data;
+ }
+ const value = element.getAttribute(attribute);
data[attribute] = { ...attributes[attribute], value };
return data;
}, {});
@@ -64,8 +78,8 @@ export function getComponentValues(dialog, element) {
delete cleanData.childComponents;
delete cleanData.nestedComponents;
delete cleanData.nestedComponentsConfig;
-
- return { ...cleanData, domRect, editor: { attributes }, html };
+ const editor = { ...dialog, attributes };
+ return { ...cleanData, domRect, dialog, editor, html };
}
export default function initEditor(listeners = true) {
@@ -75,7 +89,9 @@ export default function initEditor(listeners = true) {
new Promise((resolve) => {
setTimeout(async () => {
try {
- const component = await loadModule(`/blocks/${componentName}/${componentName}.editor`, false);
+ const fn = window.raqnComponents[componentName];
+ const name = fn.name.toLowerCase();
+ const component = await loadModule(`/blocks/${name}/${name}.editor`, false);
const mod = await component.js;
if (mod && mod.default) {
const dialog = await mod.default();
@@ -104,7 +120,7 @@ export default function initEditor(listeners = true) {
{
components: window.raqnEditor,
bodyRect,
- baseURL: getBaseUrl(),
+ baseURL: window.location.origin + getBaseUrl(),
masterConfig: window.raqnComponentsMasterConfig,
},
{ usePostMessage: true, targetOrigin: '*' },
diff --git a/scripts/index.js b/scripts/index.js
new file mode 100644
index 00000000..51f6f85b
--- /dev/null
+++ b/scripts/index.js
@@ -0,0 +1,13 @@
+import { generateDom, manipulation, renderVirtualDom } from './render/dom.js';
+// extract all the nodes from the body
+window.raqnVirtualDom = manipulation(generateDom(document.body.childNodes));
+// clear the body
+document.body.innerHTML = '';
+// append the nodes to the body after manipulation
+document.body.append(...renderVirtualDom(window.raqnVirtualDom));
+
+// EG callback to loadModules
+await Promise.all(window.initialization).then(() => {
+ // some after main modules loaded
+ console.log('All modules loaded');
+});
diff --git a/scripts/init.js b/scripts/init.js
deleted file mode 100644
index fd905986..00000000
--- a/scripts/init.js
+++ /dev/null
@@ -1,273 +0,0 @@
-import ComponentLoader from './component-loader.js';
-import {
- globalConfig,
- metaTags,
- eagerImage,
- getMeta,
- getMetaGroup,
- mergeUniqueArrays,
- getBlocksAndGrids,
-} from './libs.js';
-
-const component = {
- async init(settings) {
- // some components may have multiple targets
- const { componentName = this.getBlockData(settings?.targets?.[0]).componentName } = settings || {};
- try {
- const loader = new ComponentLoader({
- ...settings,
- componentName,
- });
- const instances = await loader.init();
- const init = {
- componentName,
- instances: [],
- failedInstances: [],
- };
-
- instances.forEach((data) => {
- if (data.status === 'fulfilled') init.instances.push(data.value);
- if (data.reason) init.failedInstances.push(data.reason.elem || data.reason);
- });
- return init;
- } catch (error) {
- const init = {
- componentName,
- initError: error,
- };
- // eslint-disable-next-line no-console
- console.error(`There was an error while initializing the '${componentName}' component`, error);
- return init;
- }
- },
-
- async multiInit(settings) {
- const initializing = await Promise.allSettled(settings.map((s) => this.init(s)));
- const initialized = initializing.map((data) => data.value || data.reason);
- const status = {
- allInitialized: initialized.every((c) => !(c.initError || c.failedInstances.length)),
- instances: initialized,
- };
- return status;
- },
-
- async multiSequentialInit(settings) {
- const initialized = [];
- const sequentialInit = async (set) => {
- if (!set.length) return;
- const initializing = await this.init(set.shift());
- initialized.unshift(initializing);
- sequentialInit(set);
- };
-
- await sequentialInit([...settings]);
-
- const status = {
- allInitialized: initialized.every((c) => !(c.initError || c.failedInstances.length)),
- instances: initialized,
- };
- return status;
- },
-
- async loadAndDefine(componentName) {
- const status = await new ComponentLoader({ componentName }).loadAndDefine();
- return { componentName, status };
- },
-
- async multiLoadAndDefine(componentNames) {
- const loading = await Promise.allSettled(componentNames.map((n) => this.loadAndDefine(n)));
- const loaded = loading.map((data) => data.value || data.reason);
- const status = {
- allLoaded: loaded.every((m) => m.status.loaded),
- modules: loaded,
- };
-
- return status;
- },
-
- 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 { targets: [block], componentName, lcp };
- },
-};
-
-export const onLoadComponents = {
- // default content
- staticStructureComponents: [
- {
- componentName: 'image',
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- targetsSelectorsPrefix: 'main > div >',
- },
- },
- {
- componentName: 'button',
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- targetsSelectorsPrefix: 'main > div >',
- },
- },
- ],
-
- async init() {
- this.setLcp();
- this.setStructure();
- this.queryAllBlocks();
- this.setBlocksData();
- this.setLcpBlocks();
- this.setLazyBlocks();
- this.initBlocks();
- },
-
- queryAllBlocks() {
- this.blocks = [
- document.body.querySelector(globalConfig.semanticBlocks[0]),
- ...document.querySelectorAll(globalConfig.blockSelector),
- ...document.body.querySelectorAll(globalConfig.semanticBlocks.slice(1).join(',')),
- ];
- },
-
- setBlocksData() {
- const structureData = this.structureComponents.map(({ componentName }) => ({
- componentName,
- targets: [document],
- loaderConfig: {
- targetsAsContainers: true,
- },
- }));
- structureData.push(...this.staticStructureComponents);
-
- const blocksData = this.blocks.map((block) => component.getBlockData(block));
- this.blocksData = [...structureData, ...blocksData];
- },
-
- setLcp() {
- const { metaName, fallbackContent } = metaTags.lcp;
- const lcpMeta = getMeta(metaName, { getArray: true });
- const defaultLcp = fallbackContent;
- const lcp = lcpMeta?.length ? lcpMeta : defaultLcp;
- // theming must be in LCP to prevent CLS
- this.lcp = mergeUniqueArrays(lcp, ['theming']).map((componentName) => ({
- componentName: componentName.trim(),
- }));
- },
-
- setStructure() {
- const structureComponents = getMetaGroup(metaTags.structure.metaNamePrefix, { getFallback: false });
- this.structureComponents = structureComponents.flatMap(({ name, content }) => {
- if (content !== true) return [];
- return {
- componentName: name.trim(),
- };
- });
- const template = getMeta(metaTags.template.metaName);
- if(template) {
- this.structureComponents = [...this.structureComponents, {
- componentName: template,
- }];
- }
- },
-
- setLcpBlocks() {
- this.lcpBlocks = this.blocksData.filter((data) => !!this.findLcp(data));
- },
-
- setLazyBlocks() {
- const allLazy = this.blocksData.filter((data) => !this.findLcp(data));
- const { grids, blocks } = getBlocksAndGrids(allLazy);
-
- this.lazyBlocks = blocks;
- this.grids = grids;
- },
-
- findLcp(data) {
- return (
- this.lcp.find(({ componentName }) => componentName === data.componentName) || data.lcp /* ||
- [...document.querySelectorAll('main > div > [class]:nth-child(-n+1)')].find((el) => el === data?.targets?.[0]) */
- );
- },
-
- async initBlocks() {
- // Keep the page hidden until specific components are initialized to prevent CLS
- component.multiInit(this.lcpBlocks).then(() => {
- window.postMessage({ message: 'raqn:components:loaded' });
- document.body.style.setProperty('display', 'block');
- });
-
- await component.multiInit(this.lazyBlocks);
- // grids must be initialized sequentially starting from the deepest level.
- // all the blocks that will be contained by the grids must be already initialized before they are added to the grids.
- component.multiSequentialInit(this.grids);
- },
-};
-
-export const globalInit = {
- async init() {
- this.setLang();
- this.initEagerImages();
- onLoadComponents.init();
- },
-
- // TODO - maybe take this from the url structure.
- setLang() {
- document.documentElement.lang ||= 'en';
- },
-
- initEagerImages() {
- const eagerImages = getMeta(metaTags.eagerImage.metaName);
- if (eagerImages) {
- const length = parseInt(eagerImages, 10);
- eagerImage(document.body, length);
- }
- },
-};
-
-globalInit.init();
-
-// init editor if message from parent
-window.addEventListener('message', async (e) => {
- if (e && e.data) {
- const { message, params } = e.data;
- if (!Array.isArray(params)) {
- const query = new URLSearchParams(window.location.search);
- switch (message) {
- case 'raqn:editor:start':
- (async function startEditor() {
- const editor = await import('./editor.js');
- const { origin, target, preview = false } = params;
- setTimeout(() => {
- editor.default(origin, target, preview);
- }, 2000);
- })();
- break;
- // other cases?
- case 'raqn:editor:preview:component':
- // preview editor with only a component
- if (query.has('preview')) {
- (async function startEditor() {
- const preview = query.get('preview');
- const win = await import('./editor-preview.js');
- const { uuid } = params;
-
- if (uuid === preview) {
- win.default(params.component, params.classes, uuid);
- }
- })();
- }
- break;
- default:
- break;
- }
- }
- }
-});
-
-export default component;
diff --git a/scripts/libs.js b/scripts/libs.js
index ecd0b011..6cbaec32 100644
--- a/scripts/libs.js
+++ b/scripts/libs.js
@@ -25,6 +25,10 @@ export const globalConfig = {
};
export const metaTags = {
+ basepath: {
+ metaName: 'basepath',
+ fallbackContent: window.location.origin,
+ },
breadcrumbRoot: {
metaName: 'breadcrumb-root',
fallbackContent: '/',
@@ -354,7 +358,15 @@ export function mergeUniqueArrays(...arrays) {
}
export function getBaseUrl() {
- return document.head.querySelector('base').href;
+ const basepath = getMeta(metaTags.basepath.metaName);
+ const base = document.head.querySelector('base');
+
+ if (!base) {
+ const element = document.createElement('base');
+ element.href = basepath;
+ document.head.append(element);
+ }
+ return basepath;
}
export function isHomePage(url) {
@@ -532,45 +544,3 @@ export function blockBodyScroll(boolean) {
const { noScroll } = globalConfig.classes;
document.body.classList.toggle(noScroll, boolean);
}
-
-// Separate any other blocks from grids and grid-item because:
-// grids must be initialized only after all the other blocks are initialized
-// grid-item component are going to be generated and initialized by the grid component and should be excluded from blocks.
-export function getBlocksAndGrids(elements) {
- const blocksAndGrids = elements.reduce(
- (acc, block) => {
- // exclude grid items
- if (block.componentName === 'grid-item') return acc;
- if (block.componentName === 'grid') {
- // separate grids
- acc.grids.push(block);
- } else {
- // separate the rest of blocks
- acc.blocks.push(block);
- }
- return acc;
- },
- { grids: [], blocks: [] },
- );
-
- // if a grid doesn't specify its level will default to level 1
- const getGridLevel = (elem) => {
- const levelClass = [...elem.classList].find((cls) => cls.startsWith('data-level-')) || 'data-level-1';
- return Number(levelClass.slice('data-level-'.length));
- };
-
- // Based on how each gird is identifying it's own grid items, the grid initialization
- // must be done starting from the deepest level grids.
- // This is because each grid can contain other grids in their grid-items
- // To achieve this infinite nesting each grid deeper than level 1 must specify their level of
- // nesting with the data-level= option e.g data-level=2
- blocksAndGrids.grids.sort(({ targets: [elemA] }, { targets: [elemB] }) => {
- const levelA = getGridLevel(elemA);
- const levelB = getGridLevel(elemB);
- if (levelA <= levelB) return 1;
- if (levelA > levelB) return -1;
- return 0;
- });
-
- return blocksAndGrids;
-}
diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js
index ad494878..6ea8eb02 100644
--- a/scripts/libs/external-config.js
+++ b/scripts/libs/external-config.js
@@ -1,4 +1,4 @@
-import { getMeta, metaTags, readValue, deepMerge } from '../libs.js';
+import { getMeta, metaTags, readValue, deepMerge, getBaseUrl } from '../libs.js';
window.raqnComponentsMasterConfig = window.raqnComponentsMasterConfig || null;
@@ -18,7 +18,8 @@ export const externalConfig = {
window.raqnComponentsConfig ??= (async () => {
const { metaName } = metaTags.themeConfigComponent;
const metaConfigPath = getMeta(metaName);
- const configPath = `${metaConfigPath}.json`;
+ const basepath = getBaseUrl();
+ const configPath = `${basepath}${metaConfigPath}.json`;
let result = null;
try {
const response = await fetch(`${configPath}`);
diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js
new file mode 100644
index 00000000..b5fa0332
--- /dev/null
+++ b/scripts/render/component-list.js
@@ -0,0 +1,117 @@
+/*
+list of components that will are available to be set in the dom
+
+ [class or tag]: { => class or tag that will be replaced by the tag
+ tag: string, => tag that will replace the class or tag
+ script: string, => path to the script that will be loaded
+ priority: number, => priority to load the script
+ transform: function, => function that will transform the node
+ dependencies: [string], => list of dependencies that will be loaded before the script
+}
+
+*/
+export const componentList = {
+ grid: {
+ tag: 'raqn-grid',
+ script: '/blocks/grid/grid',
+ priority: 0,
+ dependencies: ['grid-item'],
+ },
+ picture: {
+ tag: 'raqn-image',
+ script: '/blocks/image/image',
+ priority: 0,
+ transform: (node) => {
+ const nextSibling = { ...node.nextSibling };
+ if (nextSibling && nextSibling.tag === 'a') {
+ const { aria } = nextSibling.children[0].text;
+ node.attributes['aria-label'] = aria;
+ nextSibling.children = [node];
+ node.parentNode.children.splice(nextSibling.indexInParent, 1, {
+ tag: 'textNode',
+ text: '',
+ });
+ return nextSibling;
+ }
+ return node;
+ },
+ },
+ navigation: {
+ tag: 'raqn-navigation',
+ script: '/blocks/navigation/navigation',
+ priority: 1,
+ dependencies: ['accordion', 'icon'],
+ },
+ 'grid-item': {
+ tag: 'raqn-grid-item',
+ script: '/blocks/grid-item/grid-item',
+ priority: 0,
+ },
+ icon: {
+ tag: 'raqn-icon',
+ script: '/blocks/icon/icon',
+ priority: 1,
+ },
+ card: {
+ tag: 'raqn-card',
+ script: '/blocks/card/card',
+ priority: 2,
+ },
+ header: {
+ tag: 'raqn-header',
+ script: '/blocks/header/header',
+ dependencies: ['navigation', 'grid', 'grid-item'],
+ priority: 1,
+ },
+ footer: {
+ tag: 'raqn-footer',
+ script: '/blocks/footer/footer',
+ priority: 3,
+ },
+ theming: {
+ tag: 'raqn-theming',
+ script: '/blocks/theming/theming',
+ priority: 3,
+ },
+ accordion: {
+ tag: 'raqn-accordion',
+ script: '/blocks/accordion/accordion',
+ priority: 2,
+ },
+ popup: {
+ tag: 'raqn-popup',
+ script: '/blocks/popup/popup',
+ priority: 3,
+ },
+ 'popup-trigger': {
+ tag: 'raqn-popup-trigger',
+ script: '/blocks/popup-trigger/popup-trigger',
+ priority: 3,
+ },
+ a: {
+ tag: 'raqn-button',
+ priority: 0,
+ script: '/blocks/button/button',
+ transform: (node) => {
+ if (
+ !node.nextSibling &&
+ node.parentNode.tag === 'div' &&
+ !['raqn-image', 'picture'].includes(node.children[0].tag)
+ ) {
+ const child = { ...node };
+ node.tag = 'raqn-button';
+ node.children = [child];
+ }
+ return node;
+ },
+ },
+};
+
+export const injectedComponents = [
+ {
+ tag: 'div',
+ class: ['theming'],
+ children: [],
+ attributes: [],
+ },
+];
diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js
new file mode 100644
index 00000000..ebfd21f7
--- /dev/null
+++ b/scripts/render/dom-reducers.js
@@ -0,0 +1,168 @@
+// eslint-disable-next-line import/prefer-default-export
+
+import { getMeta, loadModule } from '../libs.js';
+import { componentList, injectedComponents } from './component-list.js';
+
+window.loadedComponents = window.loadedComponents || {};
+window.initialization = window.initialization || [];
+window.raqnComponents = window.raqnComponents || {};
+const { loadedComponents } = window;
+
+export const filterNodes = (nodes, tag, className) => {
+ const filtered = [];
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+
+ if (node.tag === tag && (className ? node.class.includes(className) : true)) {
+ node.initialIndex = i;
+ filtered.push(node);
+ }
+ }
+ return filtered;
+};
+
+export const eagerImage = (node) => {
+ if (!window.raqnEagerImages) {
+ const eager = getMeta('eager-images');
+ window.raqnEagerImages = parseInt(eager, 10) || 0;
+ }
+ if (node.tag === 'picture' && window.raqnEagerImages > 0) {
+ const img = node.children.find((child) => child.tag === 'img');
+ if (img) {
+ const { width, height } = img.attributes;
+ img.attributes.style = `aspect-ratio: ${width} / ${height};`;
+ img.attributes.loading = 'eager';
+ window.raqnEagerImages -= 1;
+ }
+ }
+ return node;
+};
+
+export const prepareGrid = (node) => {
+ if (node.children && node.children.length > 0) {
+ const grids = filterNodes(node.children, 'raqn-grid');
+ const gridItems = filterNodes(node.children, 'raqn-grid-item');
+
+ grids.map((grid, i) => {
+ const initial = node.children.indexOf(grid);
+ const nextGridIndex = grids[i + 1] ? node.children.indexOf(grids[i + 1]) : node.children.length;
+ gridItems.map((item) => {
+ const itemIndex = node.children.indexOf(item);
+ // get elements between grid and item and insert into grid
+ if (itemIndex > initial && itemIndex < nextGridIndex) {
+ const children = node.children.splice(initial + 1, itemIndex - initial);
+ const gridItem = children.pop(); // remove grid item from children
+ gridItem.children = children;
+ grid.children.push(gridItem);
+ }
+ });
+ return grid;
+ });
+ }
+ return node;
+};
+// Compare this snippet from scripts/render/dom.js:
+
+export const recursive = (fn) => (nodes, level) =>
+ nodes.map((node) => {
+ if (node.children) {
+ node.children = recursive(fn)(node.children, level + 1);
+ }
+ return fn(node, level);
+ });
+
+// eslint-disable-next-line prefer-destructuring
+export const toWebComponent = (node) => {
+ Object.keys(componentList).forEach((componentClass) => {
+ if ((node.class && node.class.includes(componentClass)) || node.tag === componentClass) {
+ const { dependencies } = componentList[componentClass];
+ if (componentList[componentClass].transform) {
+ // eslint-disable-next-line no-param-reassign
+ node = componentList[componentClass].transform(node);
+ } else {
+ node.tag = componentList[componentClass].tag;
+ }
+
+ if (!loadedComponents[componentClass]) {
+ loadedComponents[componentClass] = componentList[componentClass];
+ }
+ if (dependencies) {
+ dependencies.forEach((dependency) => {
+ if (!loadedComponents[dependency]) {
+ loadedComponents[dependency] = componentList[dependency];
+ }
+ });
+ }
+ }
+ });
+ return node;
+};
+
+// load modules in order of priority
+
+export const loadModules = (nodes, extra = {}) => {
+ const modules = { ...loadedComponents, ...extra };
+ window.initialization = Object.keys(modules)
+ .sort((a, b) => modules[a].priority - modules[b].priority)
+ .map((component) => {
+ const { script, tag, priority } = modules[component];
+ if (window.raqnComponents[tag]) return window.raqnComponents[tag].default;
+ return new Promise((resolve) => {
+ setTimeout(async () => {
+ const { js, css } = loadModule(script);
+
+ const mod = await js;
+ const style = await css;
+ if (mod.default.prototype instanceof HTMLElement) {
+ if (!window.customElements.get(tag)) {
+ window.customElements.define(tag, mod.default);
+ window.raqnComponents[tag] = mod.default;
+ }
+ }
+ resolve({ tag, mod, style });
+ }, priority || 0);
+ });
+ });
+ return nodes;
+};
+
+// Just inject components that are not in the list
+export const inject = (nodes) => {
+ const items = nodes.slice();
+ items.unshift(...injectedComponents);
+ return items;
+};
+// clear empty text nodes or nodes with only text breaklines and spaces
+export const cleanEmptyTextNodes = (node) => {
+ // remove empty text nodes to avoid rendering those
+ if (node.children) {
+ node.children = node.children.filter((n) => {
+ if (n.tag === 'textNode') {
+ const text = n.text.replace(/ /g, '').replace(/\n/g, '');
+ return text !== '';
+ }
+ return true;
+ });
+ }
+ return node;
+};
+
+// clear empty nodes that are not necessary to avoid rendering
+export const cleanEmptyNodes = (node) => {
+ if (node.tag === 'p' && node.children.length === 1 && ['a', 'picture'].includes(node.children[0].tag)) {
+ return node.children[0];
+ }
+ if (node.tag === 'em' && node.children.length === 1 && node.children[0].tag === 'a') {
+ return node.children[0];
+ }
+ if (
+ node.tag === 'div' &&
+ node.class.length === 0 &&
+ node.children.length === 1 &&
+ node.children[0].tag !== 'textNode'
+ ) {
+ return node.children[0];
+ }
+ return node;
+};
diff --git a/scripts/render/dom.js b/scripts/render/dom.js
new file mode 100644
index 00000000..3901e283
--- /dev/null
+++ b/scripts/render/dom.js
@@ -0,0 +1,172 @@
+import { classToFlat } from '../libs.js';
+import {
+ prepareGrid,
+ recursive,
+ cleanEmptyNodes,
+ cleanEmptyTextNodes,
+ inject,
+ loadModules,
+ toWebComponent,
+ eagerImage,
+} from './dom-reducers.js';
+
+// define instances for web components
+window.raqnInstances = window.raqnInstances || {};
+
+// recursive apply the path of the parent / current node
+export const recursiveParent = (node) => {
+ const current = `${node.tag}${node.class.length > 0 ? `.${[...node.class].join('.')}` : ''}`;
+ if (node.parentNode) {
+ return `${recursiveParent(node.parentNode)} ${node.tag ? current : 'textNode'}`;
+ }
+ return current;
+};
+
+// proxy object to enhance virtual dom node object.
+export const nodeProxy = (node) => {
+ const p = new Proxy(node, {
+ get(target, prop) {
+ if (prop === 'hasAttributes') {
+ return () => Object.keys(target.attributes).length > 0;
+ }
+ if (prop === 'path') {
+ return recursiveParent(target);
+ }
+ if (prop === 'uuid') {
+ return node.reference.uuid;
+ }
+
+ if (prop === 'nextSibling') {
+ if (target.parentNode) {
+ return target.parentNode.children[target.indexInParent + 1];
+ }
+ return false;
+ }
+ if (prop === 'previousSibling') {
+ if (target.parentNode) {
+ return target.parentNode.children[target.indexInParent - 1];
+ }
+ return false;
+ }
+ return target[prop];
+ },
+ set(target, prop, value) {
+ if (prop === 'children') {
+ target.children = value;
+ target.children.forEach((child, i) => {
+ child.indexInParent = i;
+ child.parentNode = p;
+ });
+ return value;
+ }
+ if (prop === 'parentNode') {
+ target.parentNode = value;
+ }
+
+ target[prop] = value;
+ return true;
+ },
+ });
+ return p;
+};
+
+// extract the virtual dom from the real dom
+export const generateDom = (virtualdom) => {
+ const dom = [];
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < virtualdom.length; i++) {
+ const element = virtualdom[i];
+ const { childNodes } = element;
+ const child = childNodes.length > 0 ? generateDom(childNodes) : [];
+ const classList = element.classList && element.classList.length > 0 ? [...element.classList] : [];
+
+ const attributes = {};
+ if (element.hasAttributes && element.hasAttributes()) {
+ // eslint-disable-next-line no-plusplus
+ for (let j = 0; j < element.attributes.length; j++) {
+ const { name, value } = element.attributes[j];
+ attributes[name] = value;
+ }
+ }
+ dom.push(
+ nodeProxy({
+ tag: element.tagName ? element.tagName.toLowerCase() : 'textNode',
+ children: child,
+ class: classList,
+ attributesValues: classToFlat(classList),
+ id: element.id,
+ attributes,
+ text: !element.tagName ? element.textContent : null,
+ reference: element,
+ }),
+ );
+ }
+ return dom;
+};
+// render the virtual dom into real dom
+export const renderVirtualDom = (virtualdom) => {
+ const dom = [];
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < virtualdom.length; i++) {
+ const element = virtualdom[i];
+ const { children } = element;
+ const child = children ? renderVirtualDom(children) : null;
+ if (element.tag !== 'textNode') {
+ const el = document.createElement(element.tag);
+ if (element.tag.indexOf('raqn-') === 0) {
+ window.raqnInstances[element.tag] = el;
+ }
+ if (element.class.length > 0) {
+ el.classList.add(...element.class);
+ }
+ if (element.id) {
+ el.id = element.id;
+ }
+ if (element.attributes) {
+ // eslint-disable-next-line no-plusplus
+ Object.keys(element.attributes).forEach((name) => {
+ const value = element.attributes[name];
+ el.setAttribute(name, value);
+ });
+ }
+ element.initialAttributesValues = classToFlat(element.class);
+ if (element.text) {
+ el.textContent = element.text;
+ }
+
+ if (child) {
+ el.append(...child);
+ }
+ dom.push(el);
+ } else {
+ dom.push(document.createTextNode(element.text));
+ }
+ }
+ return dom;
+};
+
+// receives a array of action to reduce the virtual dom
+export const curryManipulation =
+ (items = []) =>
+ (virtualdom) =>
+ items.reduce((acc, m) => m(acc, 0), virtualdom);
+
+// preset manipulation for main page
+export const manipulation = curryManipulation([
+ recursive(cleanEmptyTextNodes),
+ recursive(cleanEmptyNodes),
+ recursive(eagerImage),
+ inject,
+ recursive(toWebComponent),
+ recursive(prepareGrid),
+
+ loadModules,
+]);
+// preset manipulation for framents and external HTML
+export const generalManipulation = curryManipulation([
+ recursive(cleanEmptyTextNodes),
+ recursive(cleanEmptyNodes),
+ recursive(toWebComponent),
+ recursive(prepareGrid),
+ loadModules,
+]);
diff --git a/styles/styles.css b/styles/styles.css
index afb13051..cff1972f 100644
--- a/styles/styles.css
+++ b/styles/styles.css
@@ -69,6 +69,7 @@ body {
padding: 0;
margin: 0;
width: 100%;
+ padding-top: var(--header-height, 110px);
}
body.no-scroll {
@@ -81,9 +82,20 @@ body.no-scroll {
bottom: 0;
}
+/* avoid font most swaps */
+body,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: var(--p-font-family, system-ui);
+}
+
body > * {
color: var(--text, #000);
- font-family: var(--p-font-family, roboto);
+ font-family: var(--p-font-family, system-ui);
font-size: var(--p-font-size, 1rem);
font-weight: var(--p-font-weight, normal);
font-style: var(--p-font-style, normal);
@@ -118,7 +130,7 @@ h4 {
}
h1 {
- font-family: var(--h1-font-family, roboto);
+ font-family: var(--h1-font-family, system-ui);
font-size: var(--h1-font-size, 1rem);
font-weight: var(--h1-font-weight, normal);
font-style: var(--h1-font-style, normal);
@@ -126,7 +138,7 @@ h1 {
}
h2 {
- font-family: var(--h2-font-family, roboto);
+ font-family: var(--h2-font-family, system-ui);
font-size: var(--h2-font-size, 1rem);
font-weight: var(--h2-font-weight, normal);
font-style: var(--h2-font-style, normal);
@@ -134,7 +146,7 @@ h2 {
}
h3 {
- font-family: var(--h3-font-family, roboto);
+ font-family: var(--h3-font-family, system-ui);
font-size: var(--h3-font-size, 1rem);
font-weight: var(--h3-font-weight, normal);
font-style: var(--h3-font-style, normal);
@@ -142,7 +154,7 @@ h3 {
}
h5 {
- font-family: var(--h5-font-family, roboto);
+ font-family: var(--h5-font-family, system-ui);
font-size: var(--h5-font-size, 1rem);
font-weight: var(--h5-font-weight, normal);
font-style: var(--h5-font-style, normal);
@@ -150,7 +162,7 @@ h5 {
}
h4 {
- font-family: var(--h4-font-family, roboto);
+ font-family: var(--h4-font-family, system-ui);
font-size: var(--h4-font-size, 1rem);
font-weight: var(--h4-font-weight, normal);
font-style: var(--h4-font-style, normal);
@@ -158,7 +170,7 @@ h4 {
}
h6 {
- font-family: var(--h6-font-family, roboto);
+ font-family: var(--h6-font-family, system-ui);
font-size: var(--h6-font-size, 1rem);
font-weight: var(--h6-font-weight, normal);
font-style: var(--h6-font-style, normal);
@@ -166,7 +178,7 @@ h6 {
}
label {
- font-family: var(--label-font-family, roboto);
+ font-family: var(--label-font-family, system-ui);
font-size: var(--label-font-size, 1rem);
font-weight: var(--label-font-weight, normal);
font-style: var(--label-font-style, normal);
@@ -220,7 +232,7 @@ a {
align-items: center;
color: var(--highlight, inherit);
text-decoration: none;
- font-family: var(--a-font-family, roboto);
+ font-family: var(--a-font-family, system-ui);
font-size: var(--a-font-size, 1rem);
font-weight: var(--a-font-weight, normal);
font-style: var(--a-font-style, normal);
@@ -238,7 +250,7 @@ button {
display: inline-flex;
align-items: center;
text-decoration: none;
- font-family: var(--button-font-family, roboto);
+ font-family: var(--button-font-family, system-ui);
font-size: var(--button-font-size, 1rem);
font-weight: var(--button-font-weight, normal);
font-style: var(--button-font-style, normal);