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