From 496bf8898ce07b0d81b218bbc8ea8d9b10570e87 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 23 Sep 2024 16:54:14 +0200 Subject: [PATCH 01/22] Virtual dom + transforms poc --- blocks/grid/grid.css | 9 +- blocks/grid/grid.editor.js | 215 ++++++++++++++++++++++++ blocks/grid/grid.js | 289 +++++--------------------------- blocks/header/header.js | 1 + blocks/layout/layout.css | 3 + blocks/layout/layout.js | 13 ++ blocks/navigation/navigation.js | 4 + blocks/theming/theming.js | 9 +- head.html | 35 ++-- scripts/component-base.js | 50 ++---- scripts/editor.js | 25 ++- scripts/index.js | 9 + scripts/init.js | 19 ++- scripts/libs.js | 15 +- scripts/libs/external-config.js | 5 +- scripts/libs/render.js | 0 scripts/render/components.js | 170 +++++++++++++++++++ scripts/render/dom.js | 131 +++++++++++++++ scripts/render/rules.js | 49 ++++++ styles/styles.css | 1 + 20 files changed, 722 insertions(+), 330 deletions(-) create mode 100644 blocks/grid/grid.editor.js create mode 100644 blocks/layout/layout.css create mode 100644 blocks/layout/layout.js create mode 100644 scripts/index.js create mode 100644 scripts/libs/render.js create mode 100644 scripts/render/components.js create mode 100644 scripts/render/dom.js create mode 100644 scripts/render/rules.js 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..5aeca4bf --- /dev/null +++ b/blocks/grid/grid.editor.js @@ -0,0 +1,215 @@ +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: { + level: { + type: 'text', + label: 'Level', + helpText: 'The level of the grid.', + value: '1', + }, + height: { + type: 'text', + label: 'Height', + helpText: 'The height of the grid.', + value: '100%', + }, + width: { + type: 'text', + label: 'Width', + helpText: 'The width of the grid.', + value: '100%', + }, + 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..a02bebe2 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -1,276 +1,75 @@ import ComponentBase from '../../scripts/component-base.js'; -import { stringToJsVal } from '../../scripts/libs.js'; -import component from '../../scripts/init.js'; +import { flat } 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 = {}; + gridElements = []; + + gridItemsElements = []; + attributesValues = { all: { - data: { - level: 1, + grid: { + template: { + columns: '1fr 1fr 1fr', + rows: '1fr 1fr', + }, + gap: '20px', }, }, }; - setDefaults() { - super.setDefaults(); - } - - get gridItems() { - return [...this.children]; - } - - onAttributeHeightChanged({ oldValue, newValue }) { - this.setStyleProp('height', oldValue, newValue); - } - - onAttributeWidthChanged({ oldValue, newValue }) { - this.setStyleProp('width', oldValue, newValue); - } - - async onAttributeReverseChanged({ oldValue, newValue }) { - // await for initialization because access to this.gridItems is required; - await this.initialization; - - if (oldValue === newValue) return; - - const val = stringToJsVal(newValue); - const reverse = val === true || newValue === 'alternate'; - - let items = this.gridItems; - - switch (val) { - case true: - items = [...this.gridItems].reverse(); - break; - case 'alternate': - items = this.alternateReverse(); - break; - default: - items = this.gridItems; - break; - } - - items.forEach((item, index) => { - if (reverse) { - item.dataset.order = index + 1; - } else { - delete item.dataset.order; - } + applyGrid(grid) { + const f = flat(grid); + Object.keys(f).forEach((key) => { + this.style.setProperty(`--grid-${key}`, f[key]); }); } - // 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) => { - if ((i + 1) % 2) { - if (arr.length === i + 1) acc.push(x); - return acc; - } - acc.push(x, arr[i - 1]); - 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); + await this.checkIndexes(this.nextElementSibling); } - 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'; - } + async checkIndexes() { + const siblings = Array.from(this.parentNode.children); + // check index of this element + this.dataset.index = siblings.indexOf(this); + // verify other grid elements + this.gridElements = Array.from(this.parentNode.querySelectorAll('raqn-grid')).filter((grid) => { + grid.dataset.index = siblings.indexOf(grid); + return grid.dataset.index > this.dataset.index; + }); + // get max index of the next grid item + const nextGrid = this.gridElements.length > 0 ? siblings.indexOf(this.gridElements[0]) : siblings.length; + // get all grid items between this and next grid + this.gridItemsElements = Array.from(this.parentNode.querySelectorAll('.grid-item')).filter((item) => { + item.dataset.index = siblings.indexOf(item); + return item.dataset.index > siblings.indexOf(this) && item.dataset.index <= nextGrid; + }); - isForbiddenRaqnGrid(elem) { - return this.isRaqnGrid(elem) && this.getLevel() >= this.getLevel(elem); - } + let previous = siblings.indexOf(this) + 1; - isForbiddenBlockGrid(elem) { - return this.isBlockGrid(elem) && this.getLevelFromClass(elem) <= this.getLevel(); + return Promise.allSettled( + this.gridItemsElements.map(async (item) => { + const children = Array.from(this.parentNode.children).slice(previous, item.dataset.index - 1); + const configByClasses = [...item.classList]; + previous = item.dataset.index; + item.remove(); + return this.createGridItem(children, configByClasses); + }), + ); } 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); + await this.append(tempGridItem); + return tempGridItem; } } diff --git a/blocks/header/header.js b/blocks/header/header.js index 335f228c..ca3d1d87 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -33,6 +33,7 @@ export default class Header extends ComponentBase { } connected() { + console.log('Header connected'); eagerImage(this, 1); } } 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/layout/layout.js b/blocks/layout/layout.js new file mode 100644 index 00000000..d5d87aee --- /dev/null +++ b/blocks/layout/layout.js @@ -0,0 +1,13 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class Layout extends ComponentBase { + static observedAttributes = [ + 'data-reverse', + 'data-columns', + 'data-rows', // value can be any valid css value or a number which creates as many equal rows + ]; + + connected() { + console.log('connected', this); + } +} diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index 01352a95..5e2756d9 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -36,6 +36,7 @@ export default class Navigation extends ComponentBase { }; setDefaults() { + console.log('Navigation setDefaults'); super.setDefaults(); this.active = {}; this.isActive = false; @@ -44,6 +45,7 @@ export default class Navigation extends ComponentBase { } async ready() { + console.log('Navigation ready'); this.navContent = this.querySelector('ul'); this.innerHTML = ''; this.navCompactedContent = this.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified @@ -61,6 +63,7 @@ export default class Navigation extends ComponentBase { } setupNav() { + console.log('Navigation setupNav'); if (!this.navContentInit) { this.navContentInit = true; this.setupClasses(this.navContent); @@ -70,6 +73,7 @@ export default class Navigation extends ComponentBase { } async setupCompactedNav() { + console.log('Navigation setupCompactedNav'); if (!this.navCompactedContentInit) { this.navCompactedContentInit = true; await component.multiLoadAndDefine(['accordion', 'icon']); diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index 6acd93d0..f1face9e 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; @@ -20,6 +21,7 @@ export default class Theming extends ComponentBase { variations = {}; setDefaults() { + console.log('Theming setDefaults'); super.setDefaults(); this.scapeDiv = document.createElement('div'); this.themeJson = {}; @@ -166,14 +168,19 @@ export default class Theming extends ComponentBase { async loadFragment() { const themeConfigs = getMetaGroup(metaTags.themeConfig.metaNamePrefix); + const base = getBaseUrl(); + console.log('loadFragment', base); await Promise.allSettled( themeConfigs.map(async ({ name, content }) => - fetch(`${content}.json`).then((response) => this.processFragment(response, name)), + fetch(`${name !== 'fontface' ? base : ''}${content}.json`).then((response) => + this.processFragment(response, name), + ), ), ); this.defineVariations(); this.styles(); + document.body.style.display = 'block'; } } diff --git a/head.html b/head.html index a0c21054..28f62426 100644 --- a/head.html +++ b/head.html @@ -1,22 +1,13 @@ - - - - - - - - - - + + + + + + + + + diff --git a/scripts/component-base.js b/scripts/component-base.js index a3b749dd..e6c1358a 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,17 @@ 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 = []; + static loaderConfig = { targetsSelectorsPrefix: null, targetsSelectors: null, @@ -76,7 +76,6 @@ export default class ComponentBase extends HTMLElement { this.innerGrids = []; this.initError = null; this.breakpoints = getBreakPoints(); - this.dataAttributesKeys = this.setDataAttributesKeys(); // use the this.extendConfig() method to extend the default config this.config = { @@ -135,7 +134,7 @@ export default class ComponentBase extends HTMLElement { } async setDataAttributesKeys() { - const { observedAttributes } = await this.Handler; + const { observedAttributes } = await this.constructor; this.dataAttributesKeys = observedAttributes.map((dataAttr) => { const [, key] = dataAttr.split('data-'); @@ -217,10 +216,10 @@ 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); @@ -444,20 +443,6 @@ export default class ComponentBase extends HTMLElement { 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'); @@ -466,8 +451,6 @@ export default class ComponentBase extends HTMLElement { 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'); @@ -476,18 +459,13 @@ export default class ComponentBase extends HTMLElement { async initInnerGrids() { if (!this.innerGrids.length) return; - this.childComponents.innerGrids = await component.multiSequentialInit(this.innerGrids); + // 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 +478,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/editor.js b/scripts/editor.js index b75057a3..fc1818e4 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 || {}; @@ -54,8 +54,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 +79,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) { @@ -104,7 +119,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..39aff039 --- /dev/null +++ b/scripts/index.js @@ -0,0 +1,9 @@ +import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; + +window.raqnVirtualDom = manipulation(generateDom(document.body.childNodes)); +document.body.innerHTML = ''; +document.body.append(...renderVirtualDom(window.raqnVirtualDom)); + +await Promise.allSettled(window.inicialization).finally(() => { + console.log('All components loaded'); +}); diff --git a/scripts/init.js b/scripts/init.js index fd905986..5d2e3896 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -155,7 +155,7 @@ export const onLoadComponents = { const defaultLcp = fallbackContent; const lcp = lcpMeta?.length ? lcpMeta : defaultLcp; // theming must be in LCP to prevent CLS - this.lcp = mergeUniqueArrays(lcp, ['theming']).map((componentName) => ({ + this.lcp = mergeUniqueArrays(['grid'], lcp, ['theming']).map((componentName) => ({ componentName: componentName.trim(), })); }, @@ -169,10 +169,13 @@ export const onLoadComponents = { }; }); const template = getMeta(metaTags.template.metaName); - if(template) { - this.structureComponents = [...this.structureComponents, { - componentName: template, - }]; + if (template) { + this.structureComponents = [ + ...this.structureComponents, + { + componentName: template, + }, + ]; } }, @@ -197,12 +200,12 @@ export const onLoadComponents = { async initBlocks() { // Keep the page hidden until specific components are initialized to prevent CLS - component.multiInit(this.lcpBlocks).then(() => { + await component.multiInit(this.lcpBlocks).then(() => { window.postMessage({ message: 'raqn:components:loaded' }); document.body.style.setProperty('display', 'block'); }); - await component.multiInit(this.lazyBlocks); + 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); @@ -230,7 +233,7 @@ export const globalInit = { }, }; -globalInit.init(); +// globalInit.init(); // init editor if message from parent window.addEventListener('message', async (e) => { diff --git a/scripts/libs.js b/scripts/libs.js index ecd0b011..266563d4 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -25,6 +25,10 @@ export const globalConfig = { }; export const metaTags = { + basepath: { + metaName: 'basepath', + fallbackContent: '/', + }, breadcrumbRoot: { metaName: 'breadcrumb-root', fallbackContent: '/', @@ -354,7 +358,16 @@ 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); + } + console.log('basepath', basepath); + return basepath; } export function isHomePage(url) { 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/libs/render.js b/scripts/libs/render.js new file mode 100644 index 00000000..e69de29b diff --git a/scripts/render/components.js b/scripts/render/components.js new file mode 100644 index 00000000..02560f8e --- /dev/null +++ b/scripts/render/components.js @@ -0,0 +1,170 @@ +// eslint-disable-next-line import/prefer-default-export + +import { loadModule } from '../libs.js'; + +window.loadedComponents = window.loadedComponents || {}; +window.inicialization = window.inicialization || []; +window.raqnComponents = window.raqnComponents || {}; +const { loadedComponents } = window; + +export const componentList = { + grid: { + tag: 'raqn-grid', + script: '/blocks/grid/grid', + priority: 2, + }, + 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.push({ name: 'aria-label', value: 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: 0, + }, + 'grid-item': { + tag: 'raqn-grid-item', + script: '/blocks/grid-item/grid-item', + priority: 2, + }, + card: { + tag: 'raqn-card', + script: '/blocks/card/card', + priority: 2, + }, + header: { + tag: 'raqn-header', + script: '/blocks/header/header', + priority: 1, + }, + footer: { + tag: 'raqn-footer', + script: '/blocks/footer/footer', + priority: 1, + }, + theming: { + tag: 'raqn-theming', + script: '/blocks/theming/theming', + priority: 0, + }, + + a: { + tag: 'a', + 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'], + }, +]; + +// eslint-disable-next-line prefer-destructuring + +export const toWebComponent = (node) => { + Object.keys(componentList).forEach((componentClass) => { + if ((node.tag === 'div' && node.class.includes(componentClass)) || node.tag === 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]; + } + } + }); + return node; +}; + +export const loadModules = (nodes) => { + window.inicialization = Object.keys(loadedComponents) + .sort((a, b) => { + if (loadedComponents[a].priority > loadedComponents[b].priority) { + return 1; + } + if (loadedComponents[a].priority < loadedComponents[b].priority) { + return -1; + } + return 0; + }) + .map(async (component) => { + const { script, tag, priority } = loadedComponents[component]; + if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; + const { js, css } = await loadModule(script); + const mod = await js; + if (mod.default.prototype instanceof HTMLElement) { + window.customElements.define(tag, mod.default); + window.raqnComponents[tag] = mod.default; + } + return { component, script, tag, priority, js, css, mod }; + }); + return nodes; +}; + +export const templating = (nodes) => { + const items = nodes.slice(); + items.unshift(...injectedComponents); + return items; +}; + +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; +}; + +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 === '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..72fc9397 --- /dev/null +++ b/scripts/render/dom.js @@ -0,0 +1,131 @@ +import { cleanEmptyNodes, cleanEmptyTextNodes, loadModules, templating, toWebComponent } from './components.js'; +import { prepareGrid, recursive } from './rules.js'; + +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; +}; + +export const nodeProxy = (node) => { + const p = new Proxy(node, { + get(target, prop) { + if (prop === 'hasAttributes') { + return () => target.attributes.length > 0; + } + if (prop === 'path') { + return recursiveParent(target); + } + 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; +}; + +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) : []; + dom.push( + nodeProxy({ + tag: element.tagName ? element.tagName.toLowerCase() : 'textNode', + children: child, + class: element.classList && element.classList.length > 0 ? [...element.classList] : [], + id: element.id, + attributes: element.hasAttributes && element.hasAttributes() ? element.attributes : [], + text: !element.tagName ? element.textContent : null, + reference: element, + }), + ); + } + return 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.class.length > 0) { + el.classList.add(...element.class); + } + if (element.id) { + el.id = element.id; + } + if (element.attributes) { + // eslint-disable-next-line no-plusplus + for (let j = 0; j < element.attributes.length; j++) { + const { name, value } = element.attributes[j]; + el.setAttribute(name, value); + } + } + if (element.text) { + el.textContent = element.text; + } + + if (child) { + el.append(...child); + } + dom.push(el); + } else { + dom.push(document.createTextNode(element.text)); + } + } + return dom; +}; + +export const manipulation = (virtualdom) => + [ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + templating, + recursive(toWebComponent), + recursive(prepareGrid), + loadModules, + ].reduce((acc, m) => m(acc, 0), virtualdom); + +export const generalManipulation = (virtualdom) => + [ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + recursive(toWebComponent), + recursive(prepareGrid), + loadModules, + ].reduce((acc, m) => m(acc, 0), virtualdom); diff --git a/scripts/render/rules.js b/scripts/render/rules.js new file mode 100644 index 00000000..96899589 --- /dev/null +++ b/scripts/render/rules.js @@ -0,0 +1,49 @@ +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.inicialIndex = i; + filtered.push(node); + } + } + return filtered; +}; + +export const prepareGrid = (node, level) => { + if (level === 1 || level === 2) { + // console.log('Level', level, node); + const grids = filterNodes(node.children, 'raqn-grid'); + const gridItems = filterNodes(node.children, 'raqn-grid-item'); + + grids.map((grid, i) => { + const inicial = 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 > inicial && itemIndex < nextGridIndex) { + const children = node.children.splice(inicial + 1, itemIndex - inicial); + const gridItem = children.pop(); // remove grid item from children + gridItem.children = children; + grid.children.push(gridItem); + } + // eslint-disable-next-line no-param-reassign + }); + + 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); + }); diff --git a/styles/styles.css b/styles/styles.css index afb13051..d4176f36 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 { From 15742bf3ae128cbdd94b4a2f29b146669810f0c1 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 14:57:42 +0200 Subject: [PATCH 02/22] virtual transformations --- blocks/card/card.css | 1 - blocks/card/card.js | 2 +- blocks/navigation/navigation.js | 5 - scripts/component-base.js | 21 +- scripts/component-loader.js | 212 ------------- scripts/editor-preview.js | 56 ++-- scripts/editor.js | 15 +- scripts/index.js | 4 +- scripts/init.js | 514 +++++++++++++++----------------- scripts/render/components.js | 38 ++- scripts/render/dom.js | 52 ++-- scripts/render/rules.js | 7 +- 12 files changed, 361 insertions(+), 566 deletions(-) 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/navigation/navigation.js b/blocks/navigation/navigation.js index 5e2756d9..a5f22165 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'; @@ -45,7 +44,6 @@ export default class Navigation extends ComponentBase { } async ready() { - console.log('Navigation ready'); this.navContent = this.querySelector('ul'); this.innerHTML = ''; this.navCompactedContent = this.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified @@ -63,7 +61,6 @@ export default class Navigation extends ComponentBase { } setupNav() { - console.log('Navigation setupNav'); if (!this.navContentInit) { this.navContentInit = true; this.setupClasses(this.navContent); @@ -73,10 +70,8 @@ export default class Navigation extends ComponentBase { } async setupCompactedNav() { - console.log('Navigation 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/scripts/component-base.js b/scripts/component-base.js index e6c1358a..4a40506e 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -133,8 +133,8 @@ export default class ComponentBase extends HTMLElement { this.onBreakpointChange = this.onBreakpointChange.bind(this); } - async setDataAttributesKeys() { - const { observedAttributes } = await this.constructor; + setDataAttributesKeys() { + const { observedAttributes } = this.constructor; this.dataAttributesKeys = observedAttributes.map((dataAttr) => { const [, key] = dataAttr.split('data-'); @@ -175,14 +175,21 @@ export default class ComponentBase extends HTMLElement { * use the data attr values as default for attributesValues */ setInitialAttributesValues() { - const initialAttributesValues = { all: { data: {} } }; - + const inicial = [...this.classList]; + inicial.unshift(); // remove the component name + this.initialAttributesValues = classToFlat(inicial.splice(1)); + const initialAttributesValues = this.initialAttributesValues || { all: { data: {} } }; + if (!this.dataAttributesKeys.length) { + this.setDataAttributesKeys(); + } 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; }); @@ -351,7 +358,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. 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..78a95249 100644 --- a/scripts/editor-preview.js +++ b/scripts/editor-preview.js @@ -1,28 +1,39 @@ -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); +export const onlyNodeWithUUID = (uuid) => (node) => { + console.log('onlyNodeWithId', node, node.uuid, uuid); + if (node.uuid !== uuid && node.parentNode) { + node.parentNode.children = node.parentNode.children.splice(node.indexInParent, 1); } - if (footer) { - footer.parentNode.removeChild(footer); - } - const loader = new ComponentLoader({ componentName }); - await loader.init(); + return node; +}; + +export default async function preview(component, classes, uuid) { + 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', (e) => { @@ -31,15 +42,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 fc1818e4..2a9b92f7 100644 --- a/scripts/editor.js +++ b/scripts/editor.js @@ -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); @@ -90,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(); diff --git a/scripts/index.js b/scripts/index.js index 39aff039..6ffa3fbf 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,9 +1,11 @@ import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; +// console.log('Initializating components', generateDom(document.body.childNodes)); + window.raqnVirtualDom = manipulation(generateDom(document.body.childNodes)); document.body.innerHTML = ''; document.body.append(...renderVirtualDom(window.raqnVirtualDom)); await Promise.allSettled(window.inicialization).finally(() => { - console.log('All components loaded'); + console.log('All components initialized'); }); diff --git a/scripts/init.js b/scripts/init.js index 5d2e3896..f6ec11cd 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -1,276 +1,238 @@ -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(['grid'], 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 - await component.multiInit(this.lcpBlocks).then(() => { - window.postMessage({ message: 'raqn:components:loaded' }); - document.body.style.setProperty('display', 'block'); - }); - - 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; +// 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(['grid'], 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 +// await component.multiInit(this.lcpBlocks).then(() => { +// window.postMessage({ message: 'raqn:components:loaded' }); +// document.body.style.setProperty('display', 'block'); +// }); + +// 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(); + +// export default component; diff --git a/scripts/render/components.js b/scripts/render/components.js index 02560f8e..ea383e74 100644 --- a/scripts/render/components.js +++ b/scripts/render/components.js @@ -36,12 +36,18 @@ export const componentList = { tag: 'raqn-navigation', script: '/blocks/navigation/navigation', priority: 0, + dependencies: ['accordion', 'icon'], }, 'grid-item': { tag: 'raqn-grid-item', script: '/blocks/grid-item/grid-item', priority: 2, }, + icon: { + tag: 'raqn-icon', + script: '/blocks/icon/icon', + priority: 0, + }, card: { tag: 'raqn-card', script: '/blocks/card/card', @@ -62,9 +68,13 @@ export const componentList = { script: '/blocks/theming/theming', priority: 0, }, - + accordion: { + tag: 'raqn-accordion', + script: '/blocks/accordion/accordion', + priority: 1, + }, a: { - tag: 'a', + tag: 'raqn-button', priority: 0, script: '/blocks/button/button', transform: (node) => { @@ -86,6 +96,8 @@ export const injectedComponents = [ { tag: 'div', class: ['theming'], + children: [], + attributes: [], }, ]; @@ -93,7 +105,8 @@ export const injectedComponents = [ export const toWebComponent = (node) => { Object.keys(componentList).forEach((componentClass) => { - if ((node.tag === 'div' && node.class.includes(componentClass)) || node.tag === 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); @@ -104,6 +117,13 @@ export const toWebComponent = (node) => { if (!loadedComponents[componentClass]) { loadedComponents[componentClass] = componentList[componentClass]; } + if (dependencies) { + dependencies.forEach((dependency) => { + if (!loadedComponents[dependency]) { + loadedComponents[dependency] = componentList[dependency]; + } + }); + } } }); return node; @@ -121,15 +141,17 @@ export const loadModules = (nodes) => { return 0; }) .map(async (component) => { - const { script, tag, priority } = loadedComponents[component]; + const { script, tag } = loadedComponents[component]; if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; - const { js, css } = await loadModule(script); + const { js } = await loadModule(script); const mod = await js; if (mod.default.prototype instanceof HTMLElement) { - window.customElements.define(tag, mod.default); - window.raqnComponents[tag] = mod.default; + if (!window.customElements.get(tag)) { + window.customElements.define(tag, mod.default); + window.raqnComponents[tag] = mod.default; + } } - return { component, script, tag, priority, js, css, mod }; + return js; }); return nodes; }; diff --git a/scripts/render/dom.js b/scripts/render/dom.js index 72fc9397..fbf43226 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -1,6 +1,9 @@ +import { classToFlat } from '../libs.js'; import { cleanEmptyNodes, cleanEmptyTextNodes, loadModules, templating, toWebComponent } from './components.js'; import { prepareGrid, recursive } from './rules.js'; +window.raqnInstances = window.raqnInstances || {}; + export const recursiveParent = (node) => { const current = `${node.tag}${node.class.length > 0 ? `.${[...node.class].join('.')}` : ''}`; if (node.parentNode) { @@ -18,6 +21,10 @@ export const nodeProxy = (node) => { 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]; @@ -59,11 +66,13 @@ export const generateDom = (virtualdom) => { const element = virtualdom[i]; const { childNodes } = element; const child = childNodes.length > 0 ? generateDom(childNodes) : []; + const classList = element.classList && element.classList.length > 0 ? [...element.classList] : []; dom.push( nodeProxy({ tag: element.tagName ? element.tagName.toLowerCase() : 'textNode', children: child, - class: element.classList && element.classList.length > 0 ? [...element.classList] : [], + class: classList, + attributesValues: classToFlat(classList), id: element.id, attributes: element.hasAttributes && element.hasAttributes() ? element.attributes : [], text: !element.tagName ? element.textContent : null, @@ -83,6 +92,9 @@ export const renderVirtualDom = (virtualdom) => { 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); } @@ -96,6 +108,7 @@ export const renderVirtualDom = (virtualdom) => { el.setAttribute(name, value); } } + element.initialAttributesValues = classToFlat(element.class); if (element.text) { el.textContent = element.text; } @@ -111,21 +124,24 @@ export const renderVirtualDom = (virtualdom) => { return dom; }; -export const manipulation = (virtualdom) => - [ - recursive(cleanEmptyTextNodes), - recursive(cleanEmptyNodes), - templating, - recursive(toWebComponent), - recursive(prepareGrid), - loadModules, - ].reduce((acc, m) => m(acc, 0), virtualdom); +export const curryManipulation = + (items = []) => + (virtualdom) => + items.reduce((acc, m) => m(acc, 0), virtualdom); + +export const manipulation = curryManipulation([ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + templating, + recursive(toWebComponent), + recursive(prepareGrid), + loadModules, +]); -export const generalManipulation = (virtualdom) => - [ - recursive(cleanEmptyTextNodes), - recursive(cleanEmptyNodes), - recursive(toWebComponent), - recursive(prepareGrid), - loadModules, - ].reduce((acc, m) => m(acc, 0), virtualdom); +export const generalManipulation = curryManipulation([ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + recursive(toWebComponent), + recursive(prepareGrid), + loadModules, +]); diff --git a/scripts/render/rules.js b/scripts/render/rules.js index 96899589..5aa034fb 100644 --- a/scripts/render/rules.js +++ b/scripts/render/rules.js @@ -12,9 +12,8 @@ export const filterNodes = (nodes, tag, className) => { return filtered; }; -export const prepareGrid = (node, level) => { - if (level === 1 || level === 2) { - // console.log('Level', level, 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'); @@ -30,9 +29,7 @@ export const prepareGrid = (node, level) => { gridItem.children = children; grid.children.push(gridItem); } - // eslint-disable-next-line no-param-reassign }); - return grid; }); } From f464335685c2cc5aebd4c35a0e361d6c7a9eb500 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 15:28:01 +0200 Subject: [PATCH 03/22] Feature virtual dom and dom reducers --- blocks/grid/grid.js | 86 ++++--- blocks/popup-trigger/popup-trigger.js | 4 +- scripts/component-base.js | 31 --- scripts/index.js | 8 +- scripts/init.js | 238 ------------------ scripts/libs.js | 43 ---- scripts/render/component-list.js | 121 +++++++++ .../render/{components.js => dom-reducers.js} | 143 ++++------- scripts/render/dom.js | 23 +- scripts/render/rules.js | 46 ---- 10 files changed, 236 insertions(+), 507 deletions(-) delete mode 100644 scripts/init.js create mode 100644 scripts/render/component-list.js rename scripts/render/{components.js => dom-reducers.js} (58%) delete mode 100644 scripts/render/rules.js diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index a02bebe2..3408d3bf 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -1,16 +1,14 @@ import ComponentBase from '../../scripts/component-base.js'; -import { flat } from '../../scripts/libs.js'; +import { flat, stringToJsVal } from '../../scripts/libs.js'; export default class Grid extends ComponentBase { - nestedComponentsConfig = {}; - - gridElements = []; - - gridItemsElements = []; + // only one attribute is observed rest is set as css variables directly + static observedAttributes = ['data-reverse']; attributesValues = { all: { grid: { + // set props directly as css variables instead of data-attributes template: { columns: '1fr 1fr 1fr', rows: '1fr 1fr', @@ -20,6 +18,9 @@ export default class Grid extends ComponentBase { }, }; + // 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) => { @@ -27,49 +28,46 @@ export default class Grid extends ComponentBase { }); } - async connected() { - await this.collectGridItemsFromBlocks(); - } + async onAttributeReverseChanged({ oldValue, newValue }) { + // await for initialization because access to this.gridItems is required; + await this.initialization; - async collectGridItemsFromBlocks() { - await this.checkIndexes(this.nextElementSibling); - } + if (oldValue === newValue) return; - async checkIndexes() { - const siblings = Array.from(this.parentNode.children); - // check index of this element - this.dataset.index = siblings.indexOf(this); - // verify other grid elements - this.gridElements = Array.from(this.parentNode.querySelectorAll('raqn-grid')).filter((grid) => { - grid.dataset.index = siblings.indexOf(grid); - return grid.dataset.index > this.dataset.index; - }); - // get max index of the next grid item - const nextGrid = this.gridElements.length > 0 ? siblings.indexOf(this.gridElements[0]) : siblings.length; - // get all grid items between this and next grid - this.gridItemsElements = Array.from(this.parentNode.querySelectorAll('.grid-item')).filter((item) => { - item.dataset.index = siblings.indexOf(item); - return item.dataset.index > siblings.indexOf(this) && item.dataset.index <= nextGrid; - }); + const val = stringToJsVal(newValue); + const reverse = val === true || newValue === 'alternate'; - let previous = siblings.indexOf(this) + 1; + let items = [...this.children]; - return Promise.allSettled( - this.gridItemsElements.map(async (item) => { - const children = Array.from(this.parentNode.children).slice(previous, item.dataset.index - 1); - const configByClasses = [...item.classList]; - previous = item.dataset.index; - item.remove(); - return this.createGridItem(children, configByClasses); - }), - ); + switch (val) { + case true: + items = [...this.children].reverse(); + break; + case 'alternate': + items = this.alternateReverse(); + break; + default: + break; + } + + items.forEach((item, index) => { + if (reverse) { + item.dataset.order = index + 1; + } else { + delete item.dataset.order; + } + }); } - async createGridItem(children, configByClasses) { - const tempGridItem = document.createElement('raqn-grid-item'); - tempGridItem.init({ configByClasses }); - tempGridItem.append(...children); - await this.append(tempGridItem); - return tempGridItem; + // swaps every 2 items [1,2,3,4,5,6,7] => [2,1,4,3,6,5,7] + alternateReverse() { + return [...this.children].reduce((acc, x, i, arr) => { + if ((i + 1) % 2) { + if (arr.length === i + 1) acc.push(x); + return acc; + } + acc.push(x, arr[i - 1]); + return acc; + }, []); } } 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/scripts/component-base.js b/scripts/component-base.js index 4a40506e..0d582a49 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -225,7 +225,6 @@ export default class ComponentBase extends HTMLElement { this.setAttribute('id', this.uuid); 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 @@ -445,36 +444,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 { allInitialized } = this.childComponents.nestedComponents; - const { hideOnChildrenError } = this.config; - this.hideWithError(!allInitialized && hideOnChildrenError, 'has-nested-error'); - } - - async initInnerBlocks() { - if (!this.innerBlocks.length) return; - - 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 loadFragment(path) { if (typeof path !== 'string') return; const response = await this.getFragment(path); diff --git a/scripts/index.js b/scripts/index.js index 6ffa3fbf..8699c0d1 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,11 +1,13 @@ import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; -// console.log('Initializating components', generateDom(document.body.childNodes)); - +// 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)); +// callback to loadModules await Promise.allSettled(window.inicialization).finally(() => { - console.log('All components initialized'); + console.log('All modules loaded initialized'); }); diff --git a/scripts/init.js b/scripts/init.js deleted file mode 100644 index f6ec11cd..00000000 --- a/scripts/init.js +++ /dev/null @@ -1,238 +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(['grid'], 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 -// await component.multiInit(this.lcpBlocks).then(() => { -// window.postMessage({ message: 'raqn:components:loaded' }); -// document.body.style.setProperty('display', 'block'); -// }); - -// 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(); - -// export default component; diff --git a/scripts/libs.js b/scripts/libs.js index 266563d4..1c10416c 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -366,7 +366,6 @@ export function getBaseUrl() { element.href = basepath; document.head.append(element); } - console.log('basepath', basepath); return basepath; } @@ -545,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/render/component-list.js b/scripts/render/component-list.js new file mode 100644 index 00000000..da64b262 --- /dev/null +++ b/scripts/render/component-list.js @@ -0,0 +1,121 @@ +/* +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: 2, + }, + 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.push({ name: 'aria-label', value: 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: 0, + dependencies: ['accordion', 'icon'], + }, + 'grid-item': { + tag: 'raqn-grid-item', + script: '/blocks/grid-item/grid-item', + priority: 2, + }, + icon: { + tag: 'raqn-icon', + script: '/blocks/icon/icon', + priority: 0, + }, + card: { + tag: 'raqn-card', + script: '/blocks/card/card', + priority: 2, + }, + header: { + tag: 'raqn-header', + script: '/blocks/header/header', + priority: 1, + }, + footer: { + tag: 'raqn-footer', + script: '/blocks/footer/footer', + priority: 1, + }, + theming: { + tag: 'raqn-theming', + script: '/blocks/theming/theming', + priority: 0, + }, + accordion: { + tag: 'raqn-accordion', + script: '/blocks/accordion/accordion', + priority: 1, + }, + popup: { + tag: 'raqn-popup', + script: '/blocks/popup/popup', + priority: 1, + }, + 'popup-trigger': { + tag: 'raqn-popup-trigger', + script: '/blocks/popup-trigger/popup-trigger', + priority: 0, + }, + 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: [], + }, + { + tag: 'div', + class: ['popup-trigger'], + children: [], + attributes: [], + }, +]; diff --git a/scripts/render/components.js b/scripts/render/dom-reducers.js similarity index 58% rename from scripts/render/components.js rename to scripts/render/dom-reducers.js index ea383e74..7331663e 100644 --- a/scripts/render/components.js +++ b/scripts/render/dom-reducers.js @@ -1,108 +1,61 @@ // eslint-disable-next-line import/prefer-default-export import { loadModule } from '../libs.js'; +import { componentList, injectedComponents } from './component-list.js'; window.loadedComponents = window.loadedComponents || {}; window.inicialization = window.inicialization || []; window.raqnComponents = window.raqnComponents || {}; const { loadedComponents } = window; -export const componentList = { - grid: { - tag: 'raqn-grid', - script: '/blocks/grid/grid', - priority: 2, - }, - 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.push({ name: 'aria-label', value: 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: 0, - dependencies: ['accordion', 'icon'], - }, - 'grid-item': { - tag: 'raqn-grid-item', - script: '/blocks/grid-item/grid-item', - priority: 2, - }, - icon: { - tag: 'raqn-icon', - script: '/blocks/icon/icon', - priority: 0, - }, - card: { - tag: 'raqn-card', - script: '/blocks/card/card', - priority: 2, - }, - header: { - tag: 'raqn-header', - script: '/blocks/header/header', - priority: 1, - }, - footer: { - tag: 'raqn-footer', - script: '/blocks/footer/footer', - priority: 1, - }, - theming: { - tag: 'raqn-theming', - script: '/blocks/theming/theming', - priority: 0, - }, - accordion: { - tag: 'raqn-accordion', - script: '/blocks/accordion/accordion', - priority: 1, - }, - 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 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.inicialIndex = i; + filtered.push(node); + } + } + return filtered; }; -export const injectedComponents = [ - { - tag: 'div', - class: ['theming'], - children: [], - attributes: [], - }, -]; +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'); -// eslint-disable-next-line prefer-destructuring + grids.map((grid, i) => { + const inicial = 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 > inicial && itemIndex < nextGridIndex) { + const children = node.children.splice(inicial + 1, itemIndex - inicial); + 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) { @@ -156,12 +109,13 @@ export const loadModules = (nodes) => { return nodes; }; -export const templating = (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) { @@ -176,6 +130,7 @@ export const cleanEmptyTextNodes = (node) => { 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]; diff --git a/scripts/render/dom.js b/scripts/render/dom.js index fbf43226..8d591b8c 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -1,9 +1,18 @@ import { classToFlat } from '../libs.js'; -import { cleanEmptyNodes, cleanEmptyTextNodes, loadModules, templating, toWebComponent } from './components.js'; -import { prepareGrid, recursive } from './rules.js'; +import { + prepareGrid, + recursive, + cleanEmptyNodes, + cleanEmptyTextNodes, + inject, + loadModules, + toWebComponent, +} 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) { @@ -12,6 +21,7 @@ export const recursiveParent = (node) => { return current; }; +// proxy object to enhance virtual dom node object. export const nodeProxy = (node) => { const p = new Proxy(node, { get(target, prop) { @@ -59,6 +69,7 @@ export const nodeProxy = (node) => { return p; }; +// extract the virtual dom from the real dom export const generateDom = (virtualdom) => { const dom = []; // eslint-disable-next-line no-plusplus @@ -82,7 +93,7 @@ export const generateDom = (virtualdom) => { } return dom; }; - +// render the virtual dom into real dom export const renderVirtualDom = (virtualdom) => { const dom = []; // eslint-disable-next-line no-plusplus @@ -124,20 +135,22 @@ export const renderVirtualDom = (virtualdom) => { 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), - templating, + inject, recursive(toWebComponent), recursive(prepareGrid), loadModules, ]); - +// preset manipulation for framents and external HTML export const generalManipulation = curryManipulation([ recursive(cleanEmptyTextNodes), recursive(cleanEmptyNodes), diff --git a/scripts/render/rules.js b/scripts/render/rules.js deleted file mode 100644 index 5aa034fb..00000000 --- a/scripts/render/rules.js +++ /dev/null @@ -1,46 +0,0 @@ -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.inicialIndex = i; - filtered.push(node); - } - } - return filtered; -}; - -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 inicial = 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 > inicial && itemIndex < nextGridIndex) { - const children = node.children.splice(inicial + 1, itemIndex - inicial); - 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); - }); From fd15ee94a4f9d92ae98066afe44315125715f598 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 15:28:51 +0200 Subject: [PATCH 04/22] Feature virtual dom and dom reducers --- blocks/header/header.js | 1 - blocks/layout/layout.js | 13 ------------- blocks/theming/theming.js | 3 --- 3 files changed, 17 deletions(-) delete mode 100644 blocks/layout/layout.js diff --git a/blocks/header/header.js b/blocks/header/header.js index ca3d1d87..335f228c 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -33,7 +33,6 @@ export default class Header extends ComponentBase { } connected() { - console.log('Header connected'); eagerImage(this, 1); } } diff --git a/blocks/layout/layout.js b/blocks/layout/layout.js deleted file mode 100644 index d5d87aee..00000000 --- a/blocks/layout/layout.js +++ /dev/null @@ -1,13 +0,0 @@ -import ComponentBase from '../../scripts/component-base.js'; - -export default class Layout extends ComponentBase { - static observedAttributes = [ - 'data-reverse', - 'data-columns', - 'data-rows', // value can be any valid css value or a number which creates as many equal rows - ]; - - connected() { - console.log('connected', this); - } -} diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index f1face9e..29cc48fe 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -21,7 +21,6 @@ export default class Theming extends ComponentBase { variations = {}; setDefaults() { - console.log('Theming setDefaults'); super.setDefaults(); this.scapeDiv = document.createElement('div'); this.themeJson = {}; @@ -169,8 +168,6 @@ export default class Theming extends ComponentBase { async loadFragment() { const themeConfigs = getMetaGroup(metaTags.themeConfig.metaNamePrefix); const base = getBaseUrl(); - console.log('loadFragment', base); - await Promise.allSettled( themeConfigs.map(async ({ name, content }) => fetch(`${name !== 'fontface' ? base : ''}${content}.json`).then((response) => From 25e8fe3d893fe95fb2bc18f3bdae5789f37ff3e7 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 15:29:36 +0200 Subject: [PATCH 05/22] Feature virtual dom and dom reducers --- blocks/navigation/navigation.js | 1 - scripts/editor-preview.js | 9 --------- 2 files changed, 10 deletions(-) diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index a5f22165..bab70441 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -35,7 +35,6 @@ export default class Navigation extends ComponentBase { }; setDefaults() { - console.log('Navigation setDefaults'); super.setDefaults(); this.active = {}; this.isActive = false; diff --git a/scripts/editor-preview.js b/scripts/editor-preview.js index 78a95249..9ef18877 100644 --- a/scripts/editor-preview.js +++ b/scripts/editor-preview.js @@ -3,14 +3,6 @@ import { deepMerge } from './libs.js'; import { publish } from './pubsub.js'; import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; -export const onlyNodeWithUUID = (uuid) => (node) => { - console.log('onlyNodeWithId', node, node.uuid, uuid); - if (node.uuid !== uuid && node.parentNode) { - node.parentNode.children = node.parentNode.children.splice(node.indexInParent, 1); - } - return node; -}; - export default async function preview(component, classes, uuid) { document.body.innerHTML = ''; const main = document.createElement('main'); @@ -18,7 +10,6 @@ export default async function preview(component, classes, uuid) { webComponent.overrideExternalConfig = true; webComponent.innerHTML = component.html; main.appendChild(webComponent); - const virtualdom = generateDom(main.childNodes); virtualdom[0].attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues); From a628b1453c4985389b0c352f399a1f4896021996 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 15:40:01 +0200 Subject: [PATCH 06/22] Feature virtual dom and dom reducers --- scripts/render/component-list.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index da64b262..c137d596 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -112,10 +112,4 @@ export const injectedComponents = [ children: [], attributes: [], }, - { - tag: 'div', - class: ['popup-trigger'], - children: [], - attributes: [], - }, ]; From acaf7282a8fa9e06b5b6cac54d4bc56345eb9b2b Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 16:03:22 +0200 Subject: [PATCH 07/22] Feature virtual dom and dom reducers --- blocks/grid/grid.js | 17 ++++++++++++++++- blocks/theming/theming.js | 4 +++- head.html | 23 +++++++++++++++++------ scripts/component-base.js | 21 ++++++++++++--------- scripts/libs.js | 3 ++- scripts/render/component-list.js | 4 ++-- scripts/render/dom-reducers.js | 3 +++ 7 files changed, 55 insertions(+), 20 deletions(-) diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index 3408d3bf..608435c7 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -11,7 +11,7 @@ export default class Grid extends ComponentBase { // set props directly as css variables instead of data-attributes template: { columns: '1fr 1fr 1fr', - rows: '1fr 1fr', + rows: '1fr', }, gap: '20px', }, @@ -28,6 +28,21 @@ export default class Grid extends ComponentBase { }); } + // for backwards compatibility + applyData(data) { + if (data.columns) { + // data.columns is deprecated, use grid-template-columns + if (data.template) { + if (!data.template.columns) { + data.template.columns = data.columns; + } + } else { + data.template = { columns: data.columns }; + } + } + this.applyGrid(data); + } + async onAttributeReverseChanged({ oldValue, newValue }) { // await for initialization because access to this.gridItems is required; await this.initialization; diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index 29cc48fe..eeaecb4b 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -178,6 +178,8 @@ export default class Theming extends ComponentBase { this.defineVariations(); this.styles(); - document.body.style.display = 'block'; + setTimeout(() => { + document.body.style.display = 'block'; + }); } } diff --git a/head.html b/head.html index 28f62426..f6a4ce33 100644 --- a/head.html +++ b/head.html @@ -1,13 +1,24 @@ - - + - - - + + diff --git a/scripts/component-base.js b/scripts/component-base.js index 0d582a49..851bbfc8 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -182,16 +182,16 @@ export default class ComponentBase extends HTMLElement { if (!this.dataAttributesKeys.length) { this.setDataAttributesKeys(); } - this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { - const value = this.dataset[noDataCamelCase]; + // this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { + // const value = this.dataset[noDataCamelCase]; - if (typeof value === 'undefined') return {}; - const initialValue = unFlat({ [noData]: value }); - if (initialAttributesValues.all && initialAttributesValues.all.data) { - initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue); - } - return initialAttributesValues; - }); + // if (typeof value === 'undefined') return {}; + // const initialValue = unFlat({ [noData]: value }); + // if (initialAttributesValues.all && initialAttributesValues.all.data) { + // initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue); + // } + // return initialAttributesValues; + // }); this.attributesValues = deepMerge( {}, @@ -199,6 +199,7 @@ export default class ComponentBase extends HTMLElement { this.initOptions?.attributesValues || {}, initialAttributesValues, ); + console.log(this.attributesValues); } async connectComponent() { @@ -286,6 +287,8 @@ export default class ComponentBase extends HTMLElement { // for anything else set them flatten as they come from from external config const configs = unFlat(await externalConfig.getConfig(this.componentName, values.config)); + console.log('external', configs); + if (this.overrideExternalConfig) { // Used for preview functionality values = deepMerge({}, configs, this.attributesValues, values); diff --git a/scripts/libs.js b/scripts/libs.js index 1c10416c..bd33ba89 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -27,7 +27,7 @@ export const globalConfig = { export const metaTags = { basepath: { metaName: 'basepath', - fallbackContent: '/', + fallbackContent: window.location.origin, }, breadcrumbRoot: { metaName: 'breadcrumb-root', @@ -359,6 +359,7 @@ export function mergeUniqueArrays(...arrays) { export function getBaseUrl() { const basepath = getMeta(metaTags.basepath.metaName); + console.log('basepath', basepath); const base = document.head.querySelector('base'); if (!base) { diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index c137d596..46f39672 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -59,7 +59,7 @@ export const componentList = { header: { tag: 'raqn-header', script: '/blocks/header/header', - priority: 1, + priority: 0, }, footer: { tag: 'raqn-footer', @@ -69,7 +69,7 @@ export const componentList = { theming: { tag: 'raqn-theming', script: '/blocks/theming/theming', - priority: 0, + priority: 3, }, accordion: { tag: 'raqn-accordion', diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 7331663e..2f3d96a6 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -135,6 +135,9 @@ 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 && From 59fe2fa830d2dd632d84e7a404d9e6ad3fd762d3 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 16:58:15 +0200 Subject: [PATCH 08/22] updates --- blocks/grid/grid.js | 20 +++++++------------- scripts/component-base.js | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index 608435c7..3845b786 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -8,11 +8,6 @@ export default class Grid extends ComponentBase { attributesValues = { all: { grid: { - // set props directly as css variables instead of data-attributes - template: { - columns: '1fr 1fr 1fr', - rows: '1fr', - }, gap: '20px', }, }, @@ -30,16 +25,15 @@ export default class Grid extends ComponentBase { // for backwards compatibility applyData(data) { - if (data.columns) { - // data.columns is deprecated, use grid-template-columns - if (data.template) { - if (!data.template.columns) { - data.template.columns = data.columns; + ['columns', 'rows'].forEach((key) => { + if (data[key]) { + if (data.template) { + data.template[key] = data[key]; + } else { + data.template = { [key]: data[key] }; } - } else { - data.template = { columns: data.columns }; } - } + }); this.applyGrid(data); } diff --git a/scripts/component-base.js b/scripts/component-base.js index 851bbfc8..8df04a7d 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -179,19 +179,21 @@ export default class ComponentBase extends HTMLElement { inicial.unshift(); // remove the component name this.initialAttributesValues = classToFlat(inicial.splice(1)); const initialAttributesValues = this.initialAttributesValues || { all: { data: {} } }; - if (!this.dataAttributesKeys.length) { + if (this.dataAttributesKeys && !this.dataAttributesKeys.length) { this.setDataAttributesKeys(); + } else { + this.dataAttributesKeys = this.dataAttributesKeys || []; } - // this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { - // const value = this.dataset[noDataCamelCase]; + this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { + const value = this.dataset[noDataCamelCase]; - // if (typeof value === 'undefined') return {}; - // const initialValue = unFlat({ [noData]: value }); - // if (initialAttributesValues.all && initialAttributesValues.all.data) { - // initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue); - // } - // return initialAttributesValues; - // }); + if (typeof value === 'undefined') return {}; + const initialValue = unFlat({ [noData]: value }); + if (initialAttributesValues.all && initialAttributesValues.all.data) { + initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue); + } + return initialAttributesValues; + }); this.attributesValues = deepMerge( {}, @@ -199,7 +201,6 @@ export default class ComponentBase extends HTMLElement { this.initOptions?.attributesValues || {}, initialAttributesValues, ); - console.log(this.attributesValues); } async connectComponent() { @@ -287,8 +288,6 @@ export default class ComponentBase extends HTMLElement { // for anything else set them flatten as they come from from external config const configs = unFlat(await externalConfig.getConfig(this.componentName, values.config)); - console.log('external', configs); - if (this.overrideExternalConfig) { // Used for preview functionality values = deepMerge({}, configs, this.attributesValues, values); From 6ea38cfe168b6893fe2733028823a6cbba4c70bc Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 17:25:33 +0200 Subject: [PATCH 09/22] updates --- scripts/index.js | 8 ++++++-- scripts/render/component-list.js | 3 ++- scripts/render/dom-reducers.js | 22 +++++++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/scripts/index.js b/scripts/index.js index 8699c0d1..d0f6a2a4 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,5 +1,6 @@ import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; +const start = performance.now(); // extract all the nodes from the body window.raqnVirtualDom = manipulation(generateDom(document.body.childNodes)); // clear the body @@ -8,6 +9,9 @@ document.body.innerHTML = ''; document.body.append(...renderVirtualDom(window.raqnVirtualDom)); // callback to loadModules -await Promise.allSettled(window.inicialization).finally(() => { - console.log('All modules loaded initialized'); +console.log('Loading modules', window.inicialization); +await Promise.all(window.inicialization).then((item) => { + const end = performance.now(); + console.log('Loading modules', window.inicialization, item); + console.log('All modules loaded initialized', end - start); }); diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index 46f39672..ad96d1cb 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -14,7 +14,7 @@ export const componentList = { grid: { tag: 'raqn-grid', script: '/blocks/grid/grid', - priority: 2, + priority: 0, }, picture: { tag: 'raqn-image', @@ -59,6 +59,7 @@ export const componentList = { header: { tag: 'raqn-header', script: '/blocks/header/header', + dependencies: ['navigation', 'grid'], priority: 0, }, footer: { diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 2f3d96a6..4420106f 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -93,18 +93,22 @@ export const loadModules = (nodes) => { } return 0; }) - .map(async (component) => { + .map((component) => { const { script, tag } = loadedComponents[component]; if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; - const { js } = await loadModule(script); - const mod = await js; - if (mod.default.prototype instanceof HTMLElement) { - if (!window.customElements.get(tag)) { - window.customElements.define(tag, mod.default); - window.raqnComponents[tag] = mod.default; + return (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; + } } - } - return js; + return { tag, mod, style }; + })(); }); return nodes; }; From 2c20c207ede41b2cb4309a7c8fe15621010ece1d Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 17:28:00 +0200 Subject: [PATCH 10/22] updates --- scripts/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/index.js b/scripts/index.js index d0f6a2a4..d1f2a3c0 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -9,9 +9,7 @@ document.body.innerHTML = ''; document.body.append(...renderVirtualDom(window.raqnVirtualDom)); // callback to loadModules -console.log('Loading modules', window.inicialization); -await Promise.all(window.inicialization).then((item) => { +await Promise.all(window.inicialization).then(() => { const end = performance.now(); - console.log('Loading modules', window.inicialization, item); console.log('All modules loaded initialized', end - start); }); From ac8453d5db7686216a70782c5dd9347323210299 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 17:30:11 +0200 Subject: [PATCH 11/22] updates --- scripts/render/component-list.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index ad96d1cb..77fb6536 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -15,6 +15,7 @@ export const componentList = { tag: 'raqn-grid', script: '/blocks/grid/grid', priority: 0, + dependencies: ['grid-item'], }, picture: { tag: 'raqn-image', @@ -44,7 +45,7 @@ export const componentList = { 'grid-item': { tag: 'raqn-grid-item', script: '/blocks/grid-item/grid-item', - priority: 2, + priority: 0, }, icon: { tag: 'raqn-icon', From c1133b71aa283687c95c6e6eb47e8a235db9ba50 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 17:40:58 +0200 Subject: [PATCH 12/22] updates --- blocks/theming/theming.js | 9 ++++--- scripts/render/component-list.js | 18 +++++++------- scripts/render/dom-reducers.js | 41 ++++++++++++++++---------------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index eeaecb4b..b5be7d94 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -169,11 +169,10 @@ export default class Theming extends ComponentBase { const themeConfigs = getMetaGroup(metaTags.themeConfig.metaNamePrefix); const base = getBaseUrl(); await Promise.allSettled( - themeConfigs.map(async ({ name, content }) => - fetch(`${name !== 'fontface' ? base : ''}${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(); diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index 77fb6536..b7cec314 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -39,18 +39,18 @@ export const componentList = { navigation: { tag: 'raqn-navigation', script: '/blocks/navigation/navigation', - priority: 0, + priority: 1, dependencies: ['accordion', 'icon'], }, 'grid-item': { tag: 'raqn-grid-item', script: '/blocks/grid-item/grid-item', - priority: 0, + priority: -1, }, icon: { tag: 'raqn-icon', script: '/blocks/icon/icon', - priority: 0, + priority: 1, }, card: { tag: 'raqn-card', @@ -60,13 +60,13 @@ export const componentList = { header: { tag: 'raqn-header', script: '/blocks/header/header', - dependencies: ['navigation', 'grid'], - priority: 0, + dependencies: ['navigation', 'grid', 'grid-item'], + priority: 1, }, footer: { tag: 'raqn-footer', script: '/blocks/footer/footer', - priority: 1, + priority: 3, }, theming: { tag: 'raqn-theming', @@ -76,17 +76,17 @@ export const componentList = { accordion: { tag: 'raqn-accordion', script: '/blocks/accordion/accordion', - priority: 1, + priority: 2, }, popup: { tag: 'raqn-popup', script: '/blocks/popup/popup', - priority: 1, + priority: 3, }, 'popup-trigger': { tag: 'raqn-popup-trigger', script: '/blocks/popup-trigger/popup-trigger', - priority: 0, + priority: 3, }, a: { tag: 'raqn-button', diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 4420106f..acd93df9 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -83,32 +83,31 @@ export const toWebComponent = (node) => { }; export const loadModules = (nodes) => { + console.log( + 'loadModules', + Object.keys(loadedComponents).sort((a, b) => loadedComponents[a].priority - loadedComponents[b].priority), + ); window.inicialization = Object.keys(loadedComponents) - .sort((a, b) => { - if (loadedComponents[a].priority > loadedComponents[b].priority) { - return 1; - } - if (loadedComponents[a].priority < loadedComponents[b].priority) { - return -1; - } - return 0; - }) + .sort((a, b) => loadedComponents[a].priority - loadedComponents[b].priority) .map((component) => { - const { script, tag } = loadedComponents[component]; + const { script, tag, priority } = loadedComponents[component]; if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; - return (async () => { - const { js, css } = loadModule(script); + 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; + 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; + } } - } - return { tag, mod, style }; - })(); + console.log('loaded', tag, 'delay', 10 + priority); + resolve({ tag, mod, style }); + }, 10 + priority); + }); }); return nodes; }; From 9c1826b890c9ed7a89b0b7109b554da099357004 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 17:57:48 +0200 Subject: [PATCH 13/22] updates --- scripts/component-base.js | 118 +++++++++++++++++++-------------- scripts/render/dom-reducers.js | 5 -- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/scripts/component-base.js b/scripts/component-base.js index 8df04a7d..ebb6ce4d 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -21,6 +21,72 @@ export default class ComponentBase extends HTMLElement { 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,57 +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(); - - // 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) => { diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index acd93df9..406043f9 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -83,10 +83,6 @@ export const toWebComponent = (node) => { }; export const loadModules = (nodes) => { - console.log( - 'loadModules', - Object.keys(loadedComponents).sort((a, b) => loadedComponents[a].priority - loadedComponents[b].priority), - ); window.inicialization = Object.keys(loadedComponents) .sort((a, b) => loadedComponents[a].priority - loadedComponents[b].priority) .map((component) => { @@ -104,7 +100,6 @@ export const loadModules = (nodes) => { window.raqnComponents[tag] = mod.default; } } - console.log('loaded', tag, 'delay', 10 + priority); resolve({ tag, mod, style }); }, 10 + priority); }); From c54de58f0de7db133f928b747417318bf7a00926 Mon Sep 17 00:00:00 2001 From: Felipe Caldas Date: Fri, 27 Sep 2024 19:07:54 +0200 Subject: [PATCH 14/22] Delete scripts/libs/render.js --- scripts/libs/render.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 scripts/libs/render.js diff --git a/scripts/libs/render.js b/scripts/libs/render.js deleted file mode 100644 index e69de29b..00000000 From be7be6cce51a0fbfc3f3de982d97e115e8a547c4 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 19:12:11 +0200 Subject: [PATCH 15/22] updates --- scripts/libs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/libs.js b/scripts/libs.js index bd33ba89..6cbaec32 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -359,7 +359,6 @@ export function mergeUniqueArrays(...arrays) { export function getBaseUrl() { const basepath = getMeta(metaTags.basepath.metaName); - console.log('basepath', basepath); const base = document.head.querySelector('base'); if (!base) { From b7d5a9d8cdf906fbf32945e611f37dced033e44b Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 19:13:22 +0200 Subject: [PATCH 16/22] updates --- scripts/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/index.js b/scripts/index.js index d1f2a3c0..d941dbdf 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,6 +1,4 @@ import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; - -const start = performance.now(); // extract all the nodes from the body window.raqnVirtualDom = manipulation(generateDom(document.body.childNodes)); // clear the body @@ -8,8 +6,7 @@ document.body.innerHTML = ''; // append the nodes to the body after manipulation document.body.append(...renderVirtualDom(window.raqnVirtualDom)); -// callback to loadModules +// EG callback to loadModules await Promise.all(window.inicialization).then(() => { - const end = performance.now(); - console.log('All modules loaded initialized', end - start); + // some after main modules loaded }); From e285ff97c1ee9d3742233f4177c0dc845e6d5154 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Fri, 27 Sep 2024 19:17:49 +0200 Subject: [PATCH 17/22] updates --- scripts/render/dom-reducers.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 406043f9..6f12fc9e 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -82,11 +82,13 @@ export const toWebComponent = (node) => { return node; }; -export const loadModules = (nodes) => { - window.inicialization = Object.keys(loadedComponents) - .sort((a, b) => loadedComponents[a].priority - loadedComponents[b].priority) +// load modules in order of priority + +export const loadModules = (nodes, modules = loadedComponents) => { + window.inicialization = Object.keys(modules) + .sort((a, b) => modules[a].priority - modules[b].priority) .map((component) => { - const { script, tag, priority } = loadedComponents[component]; + const { script, tag, priority } = modules[component]; if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; return new Promise((resolve) => { setTimeout(async () => { From 17b8d9cba889d88d9eb59ea0aa036fc98463e315 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 7 Oct 2024 09:49:29 +0200 Subject: [PATCH 18/22] updates --- scripts/render/dom-reducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 6f12fc9e..8aaf23d6 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -83,8 +83,8 @@ export const toWebComponent = (node) => { }; // load modules in order of priority - export const loadModules = (nodes, modules = loadedComponents) => { + // load all modules and return a promise for queuing window.inicialization = Object.keys(modules) .sort((a, b) => modules[a].priority - modules[b].priority) .map((component) => { From d7db927ee5cd7c989ae7194995aeae4c4d72ec44 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 7 Oct 2024 10:02:22 +0200 Subject: [PATCH 19/22] updates --- scripts/index.js | 1 + scripts/render/dom-reducers.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/index.js b/scripts/index.js index d941dbdf..1b1d858e 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -9,4 +9,5 @@ document.body.append(...renderVirtualDom(window.raqnVirtualDom)); // EG callback to loadModules await Promise.all(window.inicialization).then(() => { // some after main modules loaded + console.log('All modules loaded'); }); diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 8aaf23d6..c500ac85 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -83,8 +83,9 @@ export const toWebComponent = (node) => { }; // load modules in order of priority -export const loadModules = (nodes, modules = loadedComponents) => { - // load all modules and return a promise for queuing + +export const loadModules = (nodes, extra = {}) => { + const modules = { ...loadedComponents, ...extra }; window.inicialization = Object.keys(modules) .sort((a, b) => modules[a].priority - modules[b].priority) .map((component) => { From c1027661b2b385264ef0e5da1aff2cc4971caca1 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 7 Oct 2024 10:07:46 +0200 Subject: [PATCH 20/22] updates --- blocks/grid/grid.editor.js | 10 ++-------- scripts/component-base.js | 6 +++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/blocks/grid/grid.editor.js b/blocks/grid/grid.editor.js index 5aeca4bf..d640b509 100644 --- a/blocks/grid/grid.editor.js +++ b/blocks/grid/grid.editor.js @@ -26,23 +26,17 @@ export default function config() { }, }, data: { - level: { - type: 'text', - label: 'Level', - helpText: 'The level of the grid.', - value: '1', - }, height: { type: 'text', label: 'Height', helpText: 'The height of the grid.', - value: '100%', + value: 'inicial', }, width: { type: 'text', label: 'Width', helpText: 'The width of the grid.', - value: '100%', + value: 'auto', }, reverse: { type: 'select', diff --git a/scripts/component-base.js b/scripts/component-base.js index ebb6ce4d..db05d9a4 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -191,9 +191,9 @@ export default class ComponentBase extends HTMLElement { * use the data attr values as default for attributesValues */ setInitialAttributesValues() { - const inicial = [...this.classList]; - inicial.unshift(); // remove the component name - this.initialAttributesValues = classToFlat(inicial.splice(1)); + 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(); From d63e68fe1ac90164d08fb0d51ed4ceb4e1c0b9a6 Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 7 Oct 2024 10:13:17 +0200 Subject: [PATCH 21/22] updates --- blocks/grid/grid.editor.js | 2 +- scripts/index.js | 2 +- scripts/render/component-list.js | 2 +- scripts/render/dom-reducers.js | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/blocks/grid/grid.editor.js b/blocks/grid/grid.editor.js index d640b509..c4dad629 100644 --- a/blocks/grid/grid.editor.js +++ b/blocks/grid/grid.editor.js @@ -30,7 +30,7 @@ export default function config() { type: 'text', label: 'Height', helpText: 'The height of the grid.', - value: 'inicial', + value: 'initial', }, width: { type: 'text', diff --git a/scripts/index.js b/scripts/index.js index 1b1d858e..51f6f85b 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -7,7 +7,7 @@ document.body.innerHTML = ''; document.body.append(...renderVirtualDom(window.raqnVirtualDom)); // EG callback to loadModules -await Promise.all(window.inicialization).then(() => { +await Promise.all(window.initialization).then(() => { // some after main modules loaded console.log('All modules loaded'); }); diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index b7cec314..1e85e641 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -45,7 +45,7 @@ export const componentList = { 'grid-item': { tag: 'raqn-grid-item', script: '/blocks/grid-item/grid-item', - priority: -1, + priority: 0, }, icon: { tag: 'raqn-icon', diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index c500ac85..735741f9 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -4,7 +4,7 @@ import { loadModule } from '../libs.js'; import { componentList, injectedComponents } from './component-list.js'; window.loadedComponents = window.loadedComponents || {}; -window.inicialization = window.inicialization || []; +window.initialization = window.initialization || []; window.raqnComponents = window.raqnComponents || {}; const { loadedComponents } = window; @@ -15,7 +15,7 @@ export const filterNodes = (nodes, tag, className) => { const node = nodes[i]; if (node.tag === tag && (className ? node.class.includes(className) : true)) { - node.inicialIndex = i; + node.initialIndex = i; filtered.push(node); } } @@ -28,13 +28,13 @@ export const prepareGrid = (node) => { const gridItems = filterNodes(node.children, 'raqn-grid-item'); grids.map((grid, i) => { - const inicial = node.children.indexOf(grid); + 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 > inicial && itemIndex < nextGridIndex) { - const children = node.children.splice(inicial + 1, itemIndex - inicial); + 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); @@ -86,7 +86,7 @@ export const toWebComponent = (node) => { export const loadModules = (nodes, extra = {}) => { const modules = { ...loadedComponents, ...extra }; - window.inicialization = Object.keys(modules) + window.initialization = Object.keys(modules) .sort((a, b) => modules[a].priority - modules[b].priority) .map((component) => { const { script, tag, priority } = modules[component]; @@ -104,7 +104,7 @@ export const loadModules = (nodes, extra = {}) => { } } resolve({ tag, mod, style }); - }, 10 + priority); + }, priority || 0); }); }); return nodes; From 9ad279e71dee19cd61e76e763aef4b2ff6045c8f Mon Sep 17 00:00:00 2001 From: Felipe Simoes Date: Mon, 7 Oct 2024 11:34:45 +0200 Subject: [PATCH 22/22] updates --- blocks/theming/theming.js | 14 ++++++++++---- scripts/render/component-list.js | 2 +- scripts/render/dom-reducers.js | 19 ++++++++++++++++++- scripts/render/dom.js | 22 +++++++++++++++++----- styles/styles.css | 31 +++++++++++++++++++++---------- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index b5be7d94..d8af4307 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -39,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(''); diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js index 1e85e641..b5fa0332 100644 --- a/scripts/render/component-list.js +++ b/scripts/render/component-list.js @@ -25,7 +25,7 @@ export const componentList = { const nextSibling = { ...node.nextSibling }; if (nextSibling && nextSibling.tag === 'a') { const { aria } = nextSibling.children[0].text; - node.attributes.push({ name: 'aria-label', value: aria }); + node.attributes['aria-label'] = aria; nextSibling.children = [node]; node.parentNode.children.splice(nextSibling.indexInParent, 1, { tag: 'textNode', diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 735741f9..ebfd21f7 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -1,6 +1,6 @@ // eslint-disable-next-line import/prefer-default-export -import { loadModule } from '../libs.js'; +import { getMeta, loadModule } from '../libs.js'; import { componentList, injectedComponents } from './component-list.js'; window.loadedComponents = window.loadedComponents || {}; @@ -22,6 +22,23 @@ export const filterNodes = (nodes, tag, className) => { 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'); diff --git a/scripts/render/dom.js b/scripts/render/dom.js index 8d591b8c..3901e283 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -7,6 +7,7 @@ import { inject, loadModules, toWebComponent, + eagerImage, } from './dom-reducers.js'; // define instances for web components @@ -26,7 +27,7 @@ export const nodeProxy = (node) => { const p = new Proxy(node, { get(target, prop) { if (prop === 'hasAttributes') { - return () => target.attributes.length > 0; + return () => Object.keys(target.attributes).length > 0; } if (prop === 'path') { return recursiveParent(target); @@ -78,6 +79,15 @@ export const generateDom = (virtualdom) => { 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', @@ -85,7 +95,7 @@ export const generateDom = (virtualdom) => { class: classList, attributesValues: classToFlat(classList), id: element.id, - attributes: element.hasAttributes && element.hasAttributes() ? element.attributes : [], + attributes, text: !element.tagName ? element.textContent : null, reference: element, }), @@ -114,10 +124,10 @@ export const renderVirtualDom = (virtualdom) => { } if (element.attributes) { // eslint-disable-next-line no-plusplus - for (let j = 0; j < element.attributes.length; j++) { - const { name, value } = element.attributes[j]; + Object.keys(element.attributes).forEach((name) => { + const value = element.attributes[name]; el.setAttribute(name, value); - } + }); } element.initialAttributesValues = classToFlat(element.class); if (element.text) { @@ -145,9 +155,11 @@ export const curryManipulation = export const manipulation = curryManipulation([ recursive(cleanEmptyTextNodes), recursive(cleanEmptyNodes), + recursive(eagerImage), inject, recursive(toWebComponent), recursive(prepareGrid), + loadModules, ]); // preset manipulation for framents and external HTML diff --git a/styles/styles.css b/styles/styles.css index d4176f36..cff1972f 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -82,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); @@ -119,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); @@ -127,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); @@ -135,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); @@ -143,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); @@ -151,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); @@ -159,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); @@ -167,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); @@ -221,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); @@ -239,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);