diff --git a/blocks/button/button.js b/blocks/button/button.js index 5a3fef09..43fade07 100644 --- a/blocks/button/button.js +++ b/blocks/button/button.js @@ -7,11 +7,7 @@ export default class Button extends ComponentBase { selectorTest: (el) => el.childNodes.length === 1, }; - nestedComponentsConfig = { - popupTrigger: { - componentName: 'popup-trigger', - }, - }; + nestedComponentsConfig = {}; extendConfig() { return [ diff --git a/blocks/card/card.css b/blocks/card/card.css index d3face91..12738afc 100644 --- a/blocks/card/card.css +++ b/blocks/card/card.css @@ -28,7 +28,7 @@ raqn-card > div img { } /* Make entire item clickable */ -raqn-card div > div:first-child > p > em:only-child > a:only-child { +raqn-card div > div:first-child > em:first-child > a:only-child { position: absolute; inset-block-end: 0; inset-inline-end: 0; @@ -39,10 +39,6 @@ raqn-card div > div:first-child > p > em:only-child > a:only-child { z-index: 1; } -raqn-card div > div:first-child > p:has(> em:only-child > a:only-child) { - margin: 0; -} - raqn-card div > div { display: flex; flex-direction: column; @@ -51,12 +47,12 @@ raqn-card div > div { inset-inline-end: 0; } -raqn-card div > div p:last-child:has(> raqn-button, raqn-icon) { +raqn-card div > div > :where(raqn-button, raqn-icon):last-child { flex-grow: 1; display: flex; align-items: flex-end; } -raqn-card div > div p:last-child:has(> raqn-icon) { +raqn-card div > div > raqn-icon:last-child { justify-content: flex-end; } diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 9be87f1b..067f7ddc 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,7 +1,5 @@ footer { background: var(--background-color); - width: var(--max-width); - margin: 0 auto; } raqn-footer { diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 54798d84..1b271aa4 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -1,5 +1,6 @@ raqn-grid { /* Set to initial to prevent inheritance for nested grids */ + --grid-gap: initial; --grid-height: initial; --grid-width: 100%; --grid-justify-items: initial; @@ -29,12 +30,5 @@ raqn-grid { height: var(--grid-height); background: var(--grid-background); color: var(--grid-color); -} - -/* - * First level grids will (as any other block) will act as a container - * and width should not be applied. - */ -raqn-grid:not(main > div > raqn-grid) { width: var(--grid-width); } diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index 1710677d..6e5a5c25 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -8,12 +8,11 @@ export default class Grid extends ComponentBase { attributesValues = { all: { grid: { - gap: '80px', template: { columns: 'repeat(auto-fill, 200px)', rows: 'auto', }, - heigth: 'initial', + height: 'initial', }, }, }; diff --git a/blocks/layout/layout.css b/blocks/layout/layout.css deleted file mode 100644 index 5a38840a..00000000 --- a/blocks/layout/layout.css +++ /dev/null @@ -1,3 +0,0 @@ -raqn-grid { - display: grid; -} diff --git a/blocks/popup-trigger/popup-trigger.css b/blocks/popup-trigger/popup-trigger.css deleted file mode 100644 index 354a0567..00000000 --- a/blocks/popup-trigger/popup-trigger.css +++ /dev/null @@ -1 +0,0 @@ -/* NOP */ \ No newline at end of file diff --git a/blocks/popup-trigger/popup-trigger.js b/blocks/popup-trigger/popup-trigger.js index 9af48384..3fceafad 100644 --- a/blocks/popup-trigger/popup-trigger.js +++ b/blocks/popup-trigger/popup-trigger.js @@ -1,10 +1,10 @@ import ComponentBase from '../../scripts/component-base.js'; - +import { componentList } from '../../scripts/component-list/component-list.js'; import { popupState } from '../../scripts/libs.js'; -import { generalManipulation, generateDom, renderVirtualDom } from '../../scripts/render/dom.js'; +import { loadModules } from '../../scripts/render/dom-reducers.js'; export default class PopupTrigger extends ComponentBase { - static observedAttributes = ['data-active', 'data-url']; + static observedAttributes = ['data-active', 'data-action']; static loaderConfig = { ...ComponentBase.loaderConfig, @@ -12,8 +12,6 @@ export default class PopupTrigger extends ComponentBase { targetsAsContainers: true, }; - dependencies = ['popup']; - nestedComponentsConfig = {}; get isActive() { @@ -24,8 +22,9 @@ export default class PopupTrigger extends ComponentBase { return [ ...super.extendConfig(), { - elements: { - popupBtn: 'raqn-button', + selectors: { + popupBtn: 'button', + triggerIcon: 'raqn-icon', }, closePopupIdentifier: '#popup-close', }, @@ -40,53 +39,33 @@ export default class PopupTrigger extends ComponentBase { } onInit() { - this.createButton(); - this.popupBtn.append(...this.childNodes); - this.append(this.popupBtn); - const dom = generalManipulation(generateDom(this.childNodes)); - this.innerHTML = ''; - this.append(...renderVirtualDom(dom)); - this.processTargetAnchor(); + this.setAction(); + this.queryElements(); + // console.error('🚀 ~ this.elements:', this.elements); + // console.error('🚀 ~ this.children:', this.children); } - processTargetAnchor() { - const { target: anchor } = this.initOptions; + setAction() { const { closePopupIdentifier } = this.config; - const anchorUrl = new URL(anchor.href); + const anchorUrl = new URL(this.dataset.action, window.location.origin); if (anchorUrl.hash === closePopupIdentifier) { this.isClosePopupTrigger = true; - } else { - this.dataset.url = anchorUrl.pathname; - } - - if (anchor.hasAttribute('aria-label')) { - this.ariaLabel = anchor.getAttribute('aria-label'); - this.popupBtn.setAttribute('aria-label', this.ariaLabel); + this.dataset.action = anchorUrl.hash; } } - addContentFromTarget() { - const { target } = this.initOptions; - this.popupBtn.append(...target.childNodes); - } - - createButton() { - this.popupBtn = document.createElement('raqn-button'); - this.popupBtn.setAttribute('aria-expanded', 'false'); - this.popupBtn.setAttribute('aria-haspopup', 'true'); - this.popupBtn.setAttribute('type', 'button'); - } - addListeners() { - this.popupBtn.addEventListener('click', (e) => { + this.elements.popupBtn.addEventListener('click', (e) => { e.preventDefault(); this.dataset.active = !this.isActive; }); } - onAttributeUrlChanged({ oldValue, newValue }) { - if (this.isClosePopupTrigger) return; + onAttributeActionChanged({ oldValue, newValue }) { + if (this.isClosePopupTrigger) { + return; + } if (oldValue === newValue) return; let sourceUrl; @@ -127,7 +106,7 @@ export default class PopupTrigger extends ComponentBase { this.popup = await this.createPopup(); this.addPopupToPage(); // the icon is initialize async by page loader - this.triggerIcon = this.querySelector('raqn-icon'); + // this.triggerIcon = this.querySelector('raqn-icon'); // Reassign to just toggle after the popup is created; this.loadPopup = this.togglePopup; @@ -135,22 +114,25 @@ export default class PopupTrigger extends ComponentBase { } async createPopup() { - const popup = document.createElement('raqn-popup'); - popup.dataset.url = this.popupSourceUrl; - popup.dataset.active = true; + const { popup } = componentList; + loadModules(null, { popup }); + + const popupEl = document.createElement('raqn-popup'); + popupEl.dataset.action = this.popupSourceUrl; + popupEl.dataset.active = true; // Set the popupTrigger property of the popup component to this trigger instance - popup.popupTrigger = this; - return popup; + popupEl.popupTrigger = this; + return popupEl; } togglePopup() { this.popup.dataset.active = this.isActive; - this.popupBtn.setAttribute('aria-expanded', this.isActive); - if (this.triggerIcon) { - this.triggerIcon.dataset.active = this.isActive; + this.elements.popupBtn.setAttribute('aria-expanded', this.isActive); + if (this.elements.triggerIcon) { + this.elements.triggerIcon.dataset.active = this.isActive; } if (!this.isActive) { - this.popupBtn.focus(); + this.elements.popupBtn.focus(); } } diff --git a/blocks/section-metadata/section-metadata.css b/blocks/section-metadata/section-metadata.css deleted file mode 100644 index 25579c74..00000000 --- a/blocks/section-metadata/section-metadata.css +++ /dev/null @@ -1,3 +0,0 @@ -raqn-section-metadata { - display: none; -} diff --git a/blocks/section-metadata/section-metadata.js b/blocks/section-metadata/section-metadata.js deleted file mode 100644 index 18994efc..00000000 --- a/blocks/section-metadata/section-metadata.js +++ /dev/null @@ -1,30 +0,0 @@ -import ComponentBase from '../../scripts/component-base.js'; -import { stringToArray } from '../../scripts/libs.js'; - -export default class SectionMetadata extends ComponentBase { - static observedAttributes = ['class']; - - extendConfig() { - return [ - ...super.extendConfig(), - { - classes: { - section: 'section', - }, - }, - ]; - } - - ready() { - this.parentElement.classList.add(this.config.classes.section, ...this.classList.values()); - } - - onAttributeClassChanged({ oldValue, newValue }) { - if (!this.initialized) return; - if (oldValue === newValue) return; - - const opts = { divider: ' ' }; - this.parentElement.classList.remove(...stringToArray(oldValue, opts)); - this.parentElement.classList.add(...stringToArray(newValue, opts)); - } -} diff --git a/blocks/wrapper/wrapper.css b/blocks/wrapper/wrapper.css deleted file mode 100644 index 885a7686..00000000 --- a/blocks/wrapper/wrapper.css +++ /dev/null @@ -1,8 +0,0 @@ -raqn-wrapper { - --wrapper-background: var(--scope-background, black); - --wrapper-color: var(--scope-color, white); - - display: grid; - background: var(--wrapper-background); - color: var(--wrapper-color); -} \ No newline at end of file diff --git a/blocks/wrapper/wrapper.js b/blocks/wrapper/wrapper.js deleted file mode 100644 index 4cd11bd1..00000000 --- a/blocks/wrapper/wrapper.js +++ /dev/null @@ -1,4 +0,0 @@ -import ComponentBase from '../../scripts/component-base.js'; - -export default class Wrapper extends ComponentBase { -} diff --git a/scripts/component-base.js b/scripts/component-base.js index db05d9a4..237c1fef 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -12,7 +12,8 @@ import { mergeUniqueArrays, } from './libs.js'; import { externalConfig } from './libs/external-config.js'; -import { generalManipulation, generateDom, renderVirtualDom } from './render/dom.js'; +import { generateVirtualDom, renderVirtualDom } from './render/dom.js'; +import { generalManipulation } from './render/dom-manipulations.js'; export default class ComponentBase extends HTMLElement { // All supported data attributes must be added to observedAttributes @@ -233,7 +234,7 @@ export default class ComponentBase extends HTMLElement { // Build-in method called after the element is added to the DOM. async connectedCallback() { // Common identifier for raqn web components - this.setAttribute('raqnWebComponent', ''); + this.setAttribute('raqnwebcomponent', ''); this.setAttribute('isloading', ''); try { this.initialized = this.getAttribute('initialized'); @@ -482,7 +483,10 @@ export default class ComponentBase extends HTMLElement { async addFragmentContent() { const element = document.createElement('div'); element.innerHTML = this.fragmentContent; - this.append(...renderVirtualDom(generalManipulation(generateDom(element.childNodes)))); + const virtualDom = generateVirtualDom(element.childNodes); + const transformDom = generalManipulation(virtualDom); + const realDom = renderVirtualDom(transformDom); + this.append(...realDom); } queryElements() { diff --git a/scripts/component-list/component-list.js b/scripts/component-list/component-list.js new file mode 100644 index 00000000..e9f5cbd9 --- /dev/null +++ b/scripts/component-list/component-list.js @@ -0,0 +1,239 @@ +import { forPreview } from '../libs.js'; + +const forPreviewList = await forPreview('componentList', import.meta); + +/* +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 + if function returns a node it uses the method to process the new node, + otherwise if nothing is returned all the transformation must be done manually + dependencies: [string], => list of dependencies that will be loaded before the script +} +*/ +export const componentList = { + theming: { + tag: 'raqn-theming', + method: 'replace', + module: { + path: '/blocks/theming/theming', + priority: 1, + }, + }, + breadcrumbs: { + tag: 'raqn-breadcrumbs', + method: 'replace', + module: { + path: '/blocks/breadcrumbs/breadcrumbs', + priority: 1, + }, + }, + header: { + tag: 'raqn-header', + method: 'append', + queryLevel: 1, + module: { path: '/blocks/header/header' }, + dependencies: ['navigation', 'grid', 'grid-item'], + priority: 1, + }, + footer: { + tag: 'raqn-footer', + method: 'append', + queryLevel: 1, + module: { + path: '/blocks/footer/footer', + priority: 3, + }, + }, + section: { + tag: 'raqn-section', + filterNode(node) { + if (node.tag === 'div' && ['main', 'virtualDom'].includes(node.parentNode.tag)) return true; + return false; + }, + transform(node) { + node.tag = this.tag; + + // Set options from section metadata to section. + const metaBlock = 'section-metadata'; + const [sectionMetaData] = node.queryAll((n) => n.class.includes(metaBlock)); + if (sectionMetaData) { + node.class = [...sectionMetaData.class.filter((c) => c !== metaBlock)]; + sectionMetaData.remove(); + } + + // Handle sections with multiple grids + const sectionGrids = node.queryAll((n) => n.class.includes('grid'), { queryLevel: 1 }); + if (sectionGrids.length > 1) { + if (forPreviewList) { + forPreviewList.section.transform(node); + } else { + node.remove(); + } + } + }, + }, + navigation: { + tag: 'raqn-navigation', + method: 'replace', + module: { + path: '/blocks/navigation/navigation', + priority: 1, + }, + // dependencies: ['accordion', 'icon'], + }, + icon: { + tag: 'raqn-icon', + method: 'replace', + module: { + path: '/blocks/icon/icon', + priority: 1, + }, + }, + picture: { + tag: 'raqn-image', + filterNode(node) { + if (node.tag === 'p' && node.hasOnlyChild('picture')) return true; + return false; + }, + transform(node) { + node.tag = this.tag; + + // Generate linked images based on html structure convention + const { nextSibling, firstChild: picture } = node; + if (nextSibling.tag === 'p' && nextSibling.firstChild?.tag === 'em') { + const anchor = nextSibling?.firstChild?.firstChild; + + if (anchor?.tag === 'a') { + anchor.attributes['aria-label'] = anchor.firstChild.text; + anchor.firstChild.remove(); + picture.wrapWith(anchor); + nextSibling.remove(); + } + } + }, + }, + card: { + tag: 'raqn-card', + method: 'replace', + module: { + path: '/blocks/card/card', + priority: 2, + }, + }, + accordion: { + tag: 'raqn-accordion', + method: 'replace', + module: { + path: '/blocks/accordion/accordion', + priority: 2, + }, + }, + button: { + tag: 'raqn-button', + method: 'replace', + filterNode(node) { + if (node.tag === 'p' && node.hasOnlyChild('a')) return true; + return false; + }, + module: { + path: '/blocks/button/button', + priority: 0, + }, + }, + 'popup-trigger': { + tag: 'raqn-popup-trigger', + method: 'replaceWith', + filterNode(node) { + if (node.tag === 'a') { + if (node.parentNode.tag === 'raqn-button') { + const { href } = node.attributes; + const hash = href.substring(href.indexOf('#')); + if (['#popup-trigger', '#popup-close'].includes(hash)) return true; + } + } + return false; + }, + transform(node) { + const { href } = node.attributes; + const hash = href.substring(href.indexOf('#')); + + return { + tag: 'raqn-popup-trigger', + attributes: { + 'data-action': hash, + }, + children: [ + { + tag: 'button', + attributes: { + 'aria-expanded': 'false', + 'aria-haspopup': 'true', + type: 'button', + }, + children: [...node.children], + }, + ], + }; + }, + module: { + path: '/blocks/popup-trigger/popup-trigger', + loadCSS: false, + priority: 3, + }, + }, + popup: { + tag: 'raqn-popup', + method: 'replace', + module: { + path: '/blocks/popup/popup', + priority: 4, + }, + }, + grid: { + tag: 'raqn-grid', + method: 'replace', + module: { + path: '/blocks/grid/grid', + priority: 0, + }, + dependencies: ['grid-item'], + }, + 'grid-item': { + method: 'replace', + tag: 'raqn-grid-item', + module: { + path: '/blocks/grid-item/grid-item', + priority: 0, + }, + }, + 'developers-content': { + tag: 'raqn-developers-content', + method: 'replace', + module: { + path: '/blocks/developers-content/developers-content', + priority: 4, + }, + }, + 'sidekick-tools-palette': { + tag: 'raqn-sidekick-tools-palette', + method: 'replace', + module: { + path: '/blocks/sidekick-tools-palette/sidekick-tools-palette', + priority: 4, + }, + }, +}; + +export const injectedComponents = [ + { + tag: 'div', + class: ['theming'], + children: [], + attributes: [], + }, +]; diff --git a/scripts/component-list/component-list.preview.js b/scripts/component-list/component-list.preview.js new file mode 100644 index 00000000..8ee7c83a --- /dev/null +++ b/scripts/component-list/component-list.preview.js @@ -0,0 +1,13 @@ +/* eslint-disable import/prefer-default-export */ + +export const componentList = { + section: { + transform(node) { + node.class.push('error-message-box'); + node.newChildren({ + tag: 'textNode', + text: 'The content of this section is hidden because it contains more than 1 grid which is not supported. Please fix.', + }); + }, + }, +}; diff --git a/scripts/component-loader.js b/scripts/component-loader.js deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/editor-preview.js b/scripts/editor-preview.js index 9ef18877..fd19ab57 100644 --- a/scripts/editor-preview.js +++ b/scripts/editor-preview.js @@ -1,7 +1,8 @@ // import { publish } from './pubsub.js'; import { deepMerge } from './libs.js'; import { publish } from './pubsub.js'; -import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; +import { generateVirtualDom, renderVirtualDom } from './render/dom.js'; +import { pageManipulation } from './render/dom-manipulations.js'; export default async function preview(component, classes, uuid) { document.body.innerHTML = ''; @@ -10,12 +11,12 @@ 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); + const virtualDom = generateVirtualDom(main.childNodes); + virtualDom[0].attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues); main.innerHTML = ''; document.body.append(main); - await main.append(...renderVirtualDom(manipulation(virtualdom))); + await main.append(...renderVirtualDom(pageManipulation(virtualDom))); webComponent.style.display = 'inline-grid'; webComponent.style.width = 'auto'; diff --git a/scripts/editor.js b/scripts/editor.js index fd48f26d..ad08d877 100644 --- a/scripts/editor.js +++ b/scripts/editor.js @@ -1,6 +1,6 @@ import { deepMerge, flat, getBaseUrl, loadModule } from './libs.js'; import { publish } from './pubsub.js'; -import { generateDom } from './render/dom.js'; +import { generateVirtualDom } from './render/dom.js'; window.raqnEditor = window.raqnEditor || {}; let watcher = false; @@ -77,7 +77,7 @@ export function getComponentValues(dialog, element) { }, {}); const cleanData = Object.fromEntries(Object.entries(element)); const { attributesValues, webComponentName, componentName, uuid } = cleanData; - const children = generateDom(element.children, false); + const children = generateVirtualDom(element.children, false); const editor = { ...dialog, attributes }; return { attributesValues, webComponentName, componentName, uuid, domRect, dialog, editor, html, children }; } @@ -91,7 +91,7 @@ export default function initEditor(listeners = true) { try { const fn = window.raqnComponents[componentName]; const name = fn.name.toLowerCase(); - const component = await loadModule(`/blocks/${name}/${name}.editor`, false); + const component = await loadModule(`/blocks/${name}/${name}.editor`, { loadCSS: 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 427c0979..abd12bf5 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,4 +1,6 @@ -import { generateDom, manipulation, renderVirtualDom } from './render/dom.js'; +import { generateVirtualDom, renderVirtualDom } from './render/dom.js'; +import { pageManipulation, templateManipulation } from './render/dom-manipulations.js'; +import { getMeta, metaTags } from './libs.js'; // init editor if message from parent window.addEventListener('message', async (e) => { @@ -38,14 +40,50 @@ window.addEventListener('message', async (e) => { } }); -// 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)); +export default { + async init() { + this.renderPage(); + }, -// EG callback to loadModules -await Promise.all(window.initialization).then(() => { - // some after main modules loaded -}); + async renderPage() { + window.raqnVirtualDom = generateVirtualDom(document.body.childNodes); + + pageManipulation(window.raqnVirtualDom); + + await this.templateLoad(); // this will also process window.raqnVirtualDom if template is configured + + const renderedDOM = renderVirtualDom(window.raqnVirtualDom); + + if (renderedDOM) { + document.body.innerHTML = ''; + document.body.append(...renderedDOM); + } + + // EG callback to loadModules + await Promise.allSettled(window.initialization).then(() => { + // some after main modules loaded + }); + }, + async templateLoad() { + let tpl = getMeta(metaTags.template.metaName, { getFallback: true }); + if (!tpl) return null; + if (!tpl.includes('/')) { + tpl = metaTags.template.fallbackContent.concat(tpl); + } + + const path = tpl.concat('.plain.html'); + if (typeof path !== 'string') return null; + const response = await fetch( + `${path}`, + window.location.pathname.endsWith(path) ? { cache: this.fragmentCache } : {}, + ); + + if (!response.ok) return null; + + const templateContent = await response.text(); + const element = document.createElement('div'); + element.innerHTML = templateContent; + window.raqnTplVirtualDom = generateVirtualDom(element.childNodes); + return templateManipulation(window.raqnTplVirtualDom); + }, +}.init(); diff --git a/scripts/libs.js b/scripts/libs.js index 6cbaec32..34d510a0 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -22,6 +22,11 @@ export const globalConfig = { classes: { noScroll: 'no-scroll', }, + previewHosts: { + localhost: 'localhost', + review: 'aem.page', + }, + isPreview: undefined, }; export const metaTags = { @@ -54,7 +59,8 @@ export const metaTags = { }, template: { metaName: 'template', - // contentType: 'string template name', + fallbackContent: '/templates/', + // contentType: 'string template name and path defaults to fallbackContent - or the full path including template name', }, lcp: { metaName: 'lcp', @@ -101,6 +107,21 @@ export const metaTags = { }, }; +export const isPreview = () => { + if (typeof globalConfig.isPreview !== 'undefined') return globalConfig.isPreview; + + const { hostname, searchParams } = new URL(window.location); + const isDisabled = searchParams.has('raqnPreviewOff'); + if (isDisabled) { + globalConfig.isPreview = !isDisabled; + return globalConfig.isPreview; + } + const { previewHosts } = globalConfig; + + globalConfig.isPreview = Object.values(previewHosts).some((host) => hostname.endsWith(host)); + return globalConfig.isPreview; +}; + export const camelCaseAttr = (val) => val.replace(/-([a-z])/g, (k) => k[1].toUpperCase()); export const capitalizeCaseAttr = (val) => camelCaseAttr(val.replace(/^[a-z]/g, (k) => k.toUpperCase())); @@ -323,33 +344,56 @@ export function deepMerge(origin, ...toMerge) { return deepMerge(origin, ...toMerge); } -export function loadModule(urlWithoutExtension, loadCSS = true) { +export function loadModule(urlWithoutExtension, { loadCSS = true, loadJS = true }) { + const modules = { js: Promise.resolve(), css: Promise.resolve() }; + if (!urlWithoutExtension) return modules; try { - const js = import(`${urlWithoutExtension}.js`); - if (!loadCSS) return { js, css: Promise.resolve() }; - const css = new Promise((resolve, reject) => { - const cssHref = `${urlWithoutExtension}.css`; - if (!document.querySelector(`head > link[href="${cssHref}"]`)) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = cssHref; - link.onload = resolve; - link.onerror = reject; - document.head.append(link); - } else { - resolve(); - } - }).catch((error) => - // eslint-disable-next-line no-console - console.log('Could not load module style', urlWithoutExtension, error), - ); + if (loadJS) { + modules.js = import(`${urlWithoutExtension}.js`); + } - return { css, js }; + if (loadCSS) { + modules.css = new Promise((resolve, reject) => { + const cssHref = `${urlWithoutExtension}.css`; + const style = document.querySelector(`head > link[href="${cssHref}"]`); + if (!style) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = cssHref; + link.onload = () => resolve(link); + link.onerror = reject; + document.head.append(link); + } else { + resolve(style); + } + }).catch((error) => + // eslint-disable-next-line no-console + console.log('Could not load module style', urlWithoutExtension, error), + ); + } + + return modules; } catch (error) { // eslint-disable-next-line no-console console.log('Could not load module', urlWithoutExtension, error); } - return { css: Promise.resolve(), js: Promise.resolve() }; + return modules; +} + +export async function loadAndDefine(componentConfig) { + const { tag, module: { path, loadJS, loadCSS } = {} } = componentConfig; + const { js, css } = loadModule(path, { loadJS, loadCSS }); + + const module = await js; + const style = await css; + + if (module?.default.prototype instanceof HTMLElement) { + if (!window.customElements.get(tag)) { + window.customElements.define(tag, module.default); + window.raqnComponents[tag] = module.default; + } + } + return { tag, module, style }; } export function mergeUniqueArrays(...arrays) { @@ -544,3 +588,30 @@ export function blockBodyScroll(boolean) { const { noScroll } = globalConfig.classes; document.body.classList.toggle(noScroll, boolean); } + +// export const forPreview = async (manipulation) => { +// if (!isPreview()) return null; +// const reducers = await import('./dom-reducers.preview.js'); +// return reducers[manipulation]; +// }; + +// export const onDemandPreviewModule = async ({name, path}) => { +// if (!isPreview()) return null; +// let localPath = path || import.meta.url. +// if (!path) + +// const reducers = await import('./dom-reducers.preview.js'); +// return; +// }; + +export const forPreview = async (manipulation, path) => { + if (!isPreview()) return null; + let newPath = path; + if (path.url) { + const localPath = path.url.split('.js'); + localPath.splice(1, 1, '.preview.js'); + newPath = localPath.join(''); + } + const reducers = await import(newPath); + return reducers[manipulation]; +}; \ No newline at end of file diff --git a/scripts/render/component-list.js b/scripts/render/component-list.js deleted file mode 100644 index bf40cb90..00000000 --- a/scripts/render/component-list.js +++ /dev/null @@ -1,129 +0,0 @@ -/* -list of components that will are available to be set in the dom - - [class or tag]: { => class or tag that will be replaced by the tag - tag: string, => tag that will replace the class or tag - script: string, => path to the script that will be loaded - priority: number, => priority to load the script - transform: function, => function that will transform the node - dependencies: [string], => list of dependencies that will be loaded before the script -} - -*/ -export const componentList = { - grid: { - tag: 'raqn-grid', - script: '/blocks/grid/grid', - priority: 0, - dependencies: ['grid-item'], - }, - picture: { - tag: 'raqn-image', - script: '/blocks/image/image', - priority: 0, - transform: (node) => { - const nextSibling = { ...node.nextSibling }; - if (nextSibling && nextSibling.tag === 'a') { - const { aria } = nextSibling.children[0].text; - node.attributes['aria-label'] = aria; - nextSibling.children = [node]; - node.parentNode.children.splice(nextSibling.indexInParent, 1, { - tag: 'textNode', - text: '', - }); - return nextSibling; - } - return node; - }, - }, - navigation: { - tag: 'raqn-navigation', - script: '/blocks/navigation/navigation', - priority: 1, - dependencies: ['accordion', 'icon'], - }, - 'grid-item': { - tag: 'raqn-grid-item', - script: '/blocks/grid-item/grid-item', - priority: 0, - }, - icon: { - tag: 'raqn-icon', - script: '/blocks/icon/icon', - priority: 1, - }, - card: { - tag: 'raqn-card', - script: '/blocks/card/card', - priority: 2, - }, - header: { - tag: 'raqn-header', - script: '/blocks/header/header', - dependencies: ['navigation', 'grid', 'grid-item'], - priority: 1, - }, - footer: { - tag: 'raqn-footer', - script: '/blocks/footer/footer', - priority: 3, - }, - theming: { - tag: 'raqn-theming', - script: '/blocks/theming/theming', - priority: 3, - }, - accordion: { - tag: 'raqn-accordion', - script: '/blocks/accordion/accordion', - priority: 2, - }, - popup: { - tag: 'raqn-popup', - script: '/blocks/popup/popup', - priority: 3, - }, - a: { - tag: 'raqn-button', - priority: 0, - script: '/blocks/button/button', - transform: (node) => { - if (node.attributes.href && node.attributes.href.includes('#popup-trigger')) { - node.tag = 'popup-trigger'; - [node.attributes['data-url']] = node.attributes.href.split('#popup-trigger'); - delete node.attributes.href; - const button = { - tag: 'raqn-button', - children: [node], - class: ['button-popup-trigger'], - }; - return button; - } - - 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; - }, - }, - 'popup-trigger': { - tag: 'raqn-popup-trigger', - script: '/blocks/popup-trigger/popup-trigger', - priority: 3, - }, -}; - -export const injectedComponents = [ - { - tag: 'div', - class: ['theming'], - children: [], - attributes: [], - }, -]; diff --git a/scripts/render/dom-manipulations.js b/scripts/render/dom-manipulations.js new file mode 100644 index 00000000..2c4367c0 --- /dev/null +++ b/scripts/render/dom-manipulations.js @@ -0,0 +1,42 @@ +import { + prepareGrid, + cleanEmptyNodes, + cleanEmptyTextNodes, + inject, + loadModules, + toWebComponent, + eagerImage, + replaceTemplatePlaceholders, + forPreviewManipulation, +} from './dom-reducers.js'; +import { curryManipulation, recursive } from './dom-utils.js'; + +// preset manipulation for main page +export const pageManipulation = curryManipulation([ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + recursive(eagerImage), + inject, + toWebComponent, + recursive(prepareGrid), + loadModules, + await forPreviewManipulation('highlightTemplatePlaceholders'), +]); + +// preset manipulation for fragments and external HTML +export const generalManipulation = curryManipulation([ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + toWebComponent, + recursive(prepareGrid), + loadModules, +]); + +export const templateManipulation = curryManipulation([ + recursive(cleanEmptyTextNodes), + recursive(cleanEmptyNodes), + toWebComponent, + recursive(prepareGrid), + loadModules, + replaceTemplatePlaceholders, +]); diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index ebfd21f7..5476c600 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -1,13 +1,15 @@ // eslint-disable-next-line import/prefer-default-export - -import { getMeta, loadModule } from '../libs.js'; -import { componentList, injectedComponents } from './component-list.js'; +import { deepMerge, getMeta, loadAndDefine, forPreview } from '../libs.js'; +import { recursive, tplPlaceholderCheck, getTplPlaceholder } from './dom-utils.js'; +import { componentList, injectedComponents } from '../component-list/component-list.js'; window.loadedComponents = window.loadedComponents || {}; window.initialization = window.initialization || []; window.raqnComponents = window.raqnComponents || {}; const { loadedComponents } = window; +export const forPreviewManipulation = (manipulation) => forPreview(manipulation, import.meta); + export const filterNodes = (nodes, tag, className) => { const filtered = []; // eslint-disable-next-line no-plusplus @@ -15,7 +17,7 @@ export const filterNodes = (nodes, tag, className) => { const node = nodes[i]; if (node.tag === tag && (className ? node.class.includes(className) : true)) { - node.initialIndex = i; + // node.initialIndex = i; filtered.push(node); } } @@ -36,91 +38,93 @@ export const eagerImage = (node) => { window.raqnEagerImages -= 1; } } - return node; }; export const prepareGrid = (node) => { - if (node.children && node.children.length > 0) { - const grids = filterNodes(node.children, 'raqn-grid'); - const gridItems = filterNodes(node.children, 'raqn-grid-item'); - - grids.map((grid, i) => { - const initial = node.children.indexOf(grid); - const nextGridIndex = grids[i + 1] ? node.children.indexOf(grids[i + 1]) : node.children.length; - gridItems.map((item) => { - const itemIndex = node.children.indexOf(item); - // get elements between grid and item and insert into grid - if (itemIndex > initial && itemIndex < nextGridIndex) { - const children = node.children.splice(initial + 1, itemIndex - initial); - const gridItem = children.pop(); // remove grid item from children - gridItem.children = children; - grid.children.push(gridItem); - } - }); - return grid; + if (node.children && node.children.length > 0 && node.tag === 'raqn-section') { + const [grid, ...gridItems] = node.queryAll((n) => ['raqn-grid', 'raqn-grid-item'].includes(n.tag), { + queryLevel: 1, + }); + + if (!grid) return; + gridItems.forEach((item) => { + const currentChildren = [...node.children]; + const initial = currentChildren.indexOf(grid); + const itemIndex = currentChildren.indexOf(item); + const gridItemChildren = currentChildren.splice(initial + 1, itemIndex - initial - 1); + item.append(...gridItemChildren); + grid.append(item); }); } - 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); +const addToLoadComponents = (blockSelector, config) => { + const { dependencies } = config; + + const toLoad = [blockSelector, ...(dependencies || [])]; + + toLoad.forEach((load) => { + if (!loadedComponents[load]) { + loadedComponents[load] = componentList[load]; } - return fn(node, level); }); +}; -// eslint-disable-next-line prefer-destructuring -export const toWebComponent = (node) => { - Object.keys(componentList).forEach((componentClass) => { - if ((node.class && node.class.includes(componentClass)) || node.tag === componentClass) { - const { dependencies } = componentList[componentClass]; - if (componentList[componentClass].transform) { - // eslint-disable-next-line no-param-reassign - node = componentList[componentClass].transform(node); - } else { - node.tag = componentList[componentClass].tag; - } +export const toWebComponent = (virtualDom) => { + const componentConfig = deepMerge({}, componentList); + const componentConfigList = Object.entries(componentConfig); + + const { replaceBlocks, queryBlocks } = componentConfigList.reduce( + (acc, item) => { + const [, config] = item; + if (config.method === 'replace') { + acc.replaceBlocks.push(item); + } else acc.queryBlocks.push(item); + return acc; + }, + { replaceBlocks: [], queryBlocks: [] }, + ); - if (!loadedComponents[componentClass]) { - loadedComponents[componentClass] = componentList[componentClass]; + // Simple and fast in place tag replacement + recursive((node) => { + replaceBlocks.forEach(([blockSelector, config]) => { + if (node?.class?.includes?.(blockSelector) || config.filterNode?.(node)) { + node.tag = config.tag; + addToLoadComponents(blockSelector, config); } - if (dependencies) { - dependencies.forEach((dependency) => { - if (!loadedComponents[dependency]) { - loadedComponents[dependency] = componentList[dependency]; - } - }); + }); + })(virtualDom); + + // More complex transformation need to be done in order based on a separate query for each component. + queryBlocks.forEach(([blockSelector, config]) => { + const filter = + config.filterNode || ((node) => node?.class?.includes?.(blockSelector) || node.tag === blockSelector); + const nodes = virtualDom.queryAll(filter, { queryLevel: config.queryLevel }); + + nodes.forEach((node) => { + const defaultNode = { tag: config.tag }; + const hasTransform = typeof config.transform === 'function'; + const transformNode = config.transform?.(node); + if ((!hasTransform || (hasTransform && transformNode)) && config.method) { + node[config.method](transformNode || defaultNode); } - } + addToLoadComponents(blockSelector, config); + }); }); - return node; }; // load modules in order of priority - export const loadModules = (nodes, extra = {}) => { const modules = { ...loadedComponents, ...extra }; window.initialization = Object.keys(modules) .sort((a, b) => modules[a].priority - modules[b].priority) - .map((component) => { - const { script, tag, priority } = modules[component]; + .flatMap((component) => { + const { tag, priority } = modules[component]; if (window.raqnComponents[tag]) return window.raqnComponents[tag].default; + if (!modules[component]?.module?.path) return []; return new Promise((resolve) => { setTimeout(async () => { - const { js, css } = loadModule(script); - - const mod = await js; - const style = await css; - if (mod.default.prototype instanceof HTMLElement) { - if (!window.customElements.get(tag)) { - window.customElements.define(tag, mod.default); - window.raqnComponents[tag] = mod.default; - } - } - resolve({ tag, mod, style }); + resolve(await loadAndDefine(modules[component])); }, priority || 0); }); }); @@ -129,40 +133,53 @@ export const loadModules = (nodes, extra = {}) => { // Just inject components that are not in the list export const inject = (nodes) => { - const items = nodes.slice(); - items.unshift(...injectedComponents); - return items; + const [header] = nodes.children; + header.before(...injectedComponents); }; + // clear empty text nodes or nodes with only text breaklines and spaces export const cleanEmptyTextNodes = (node) => { - // remove empty text nodes to avoid rendering those - if (node.children) { - node.children = node.children.filter((n) => { - if (n.tag === 'textNode') { - const text = n.text.replace(/ /g, '').replace(/\n/g, ''); - return text !== ''; - } - return true; - }); + if (node.tag === 'textNode') { + const text = node.text.replace(/ /g, '').replace(/\n/g, ''); + if (text === '') node.remove(); } - return node; }; // clear empty nodes that are not necessary to avoid rendering export const cleanEmptyNodes = (node) => { - if (node.tag === 'p' && node.children.length === 1 && ['a', 'picture'].includes(node.children[0].tag)) { - return node.children[0]; + if (node.tag === 'br') { + node.remove(); } - if (node.tag === 'em' && node.children.length === 1 && node.children[0].tag === 'a') { - return node.children[0]; - } - if ( - node.tag === 'div' && - node.class.length === 0 && - node.children.length === 1 && - node.children[0].tag !== 'textNode' - ) { - return node.children[0]; - } - return node; +}; + +export const replaceTemplatePlaceholders = (tplVirtualDom) => { + const pageVirtualDom = window.raqnVirtualDom; + const placeholders = []; + const placeholdersNodes = tplVirtualDom.queryAll((n) => { + if (!tplPlaceholderCheck(n)) return false; + const placeholder = getTplPlaceholder(n); + placeholders.push(placeholder); + return true; + }); + + placeholdersNodes.forEach((node, i) => { + const placeholder = placeholders[i]; + const placeholderContent = pageVirtualDom.queryAll( + (n) => { + if (n.tag !== 'raqn-section') return false; + if (n.class.includes(placeholder)) return true; + // if main content special placeholder is defined any section without a placeholder will be added to the main content. + if (placeholder === 'tpl-content-auto-main' && n.class.every((ph) => !placeholders.includes(ph))) return true; + + return false; + }, + { queryLevel: 4 }, + ); + + if (placeholderContent.length) { + node.replaceWith(...placeholderContent); + } + }); + const [main] = pageVirtualDom.queryAll((n) => n.tag === 'main', { queryLevel: 1 }); + main.prepend(...tplVirtualDom.children); }; diff --git a/scripts/render/dom-reducers.preview.js b/scripts/render/dom-reducers.preview.js new file mode 100644 index 00000000..4404da61 --- /dev/null +++ b/scripts/render/dom-reducers.preview.js @@ -0,0 +1,11 @@ +/* eslint-disable import/prefer-default-export */ + +import { tplPlaceholderCheck } from './dom-utils.js'; + +export const highlightTemplatePlaceholders = (tplVirtualDom) => { + tplVirtualDom.queryAll((n) => { + if (!tplPlaceholderCheck(n)) return false; + n.class.push('template-placeholder'); + return true; + }); +}; diff --git a/scripts/render/dom-utils.js b/scripts/render/dom-utils.js new file mode 100644 index 00000000..90c1604f --- /dev/null +++ b/scripts/render/dom-utils.js @@ -0,0 +1,60 @@ +export const recursive = + (fn) => + (virtualDom, stopLevel, currentLevel = 1) => { + if (stopLevel && stopLevel < currentLevel) return; + const localNodes = [...virtualDom.children]; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < localNodes.length; i++) { + const node = localNodes[i]; + if (node.children.length) { + recursive(fn)(node, stopLevel, currentLevel + 1); + } + fn(node, currentLevel); + } + }; + +export const queryAllNodes = (nodes, fn, settings) => { + const { currentLevel = 1, queryLevel } = settings || {}; + return nodes.reduce((acc, node) => { + const match = fn(node); + if (match) acc.push(node); + if (node.children.length && (!queryLevel || currentLevel < queryLevel)) { + const fromChilds = queryAllNodes(node.children, fn, { currentLevel: currentLevel + 1, queryLevel }); + acc.push(...fromChilds); + } + return acc; + }, []); +}; + +export const queryNode = (nodes, fn) => { + let n = null; + nodes.some((node) => { + const match = fn(node); + if (match) { + n = node; + return true; + } + if (node.children.length) { + const childNodeMatch = queryNode(node.children, fn); + if (childNodeMatch) { + n = childNodeMatch; + return true; + } + } + return false; + }); + return n; +}; + +// receives a array of action to reduce the virtual dom +export const curryManipulation = + (manipulations = []) => + (virtualDom) => + manipulations + .filter((fn) => typeof fn === 'function') + .reduce((acc, manipulation) => manipulation(acc, 0) || acc, virtualDom); + +export const tplPlaceholderCheck = (node) => + node.tag === 'p' && node.hasOnlyChild('textNode') && node.firstChild.text.match(/\$\{tpl-content-[a-zA-Z1-9-]+\}/g); + +export const getTplPlaceholder = (node) => node.firstChild.text.trim().replace(/^\$\{|\}$/g, ''); diff --git a/scripts/render/dom.js b/scripts/render/dom.js index 6b586d16..5e032150 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -1,14 +1,5 @@ import { classToFlat } from '../libs.js'; -import { - prepareGrid, - recursive, - cleanEmptyNodes, - cleanEmptyTextNodes, - inject, - loadModules, - toWebComponent, - eagerImage, -} from './dom-reducers.js'; +import { queryAllNodes } from './dom-utils.js'; // define instances for web components window.raqnInstances = window.raqnInstances || {}; @@ -23,118 +14,293 @@ export const recursiveParent = (node) => { }; // proxy object to enhance virtual dom node object. -export const nodeProxy = (node) => { - const p = new Proxy(node, { +export function nodeProxy(rawNode) { + const proxyNode = new Proxy(rawNode, { get(target, prop) { if (prop === 'hasAttributes') { return () => Object.keys(target.attributes).length > 0; } + if (prop === 'path') { return recursiveParent(target); } + if (prop === 'uuid') { - return node.reference.uuid; + return rawNode.reference.uuid; } if (prop === 'nextSibling') { - if (target.parentNode) { - return target.parentNode.children[target.indexInParent + 1]; - } - return false; + const { siblings } = target; + return siblings[siblings.indexOf(proxyNode) + 1]; } + if (prop === 'previousSibling') { - if (target.parentNode) { - return target.parentNode.children[target.indexInParent - 1]; - } - return false; + const { siblings } = target; + return siblings[siblings.indexOf(proxyNode) - 1]; + } + + if (prop === 'indexInSiblings') { + return target.siblings.indexOf(proxyNode); + } + + if (prop === 'firstChild') { + return target.children.at(0); + } + + if (prop === 'lastChild') { + return target.children.at(-1); + } + + if (prop === 'hasOnlyChild') { + return (tagName) => target.children.length === 1 && target.children[0].tag === tagName; + } + + // mehod + if (prop === 'remove') { + return () => { + const { siblings } = target; + target.parentNode = null; + target.siblings = []; + siblings.splice(siblings.indexOf(proxyNode), 1); + }; + } + + if (prop === 'removeChildren') { + return () => { + const { children } = target; + children.forEach((node) => { + node.parentNode = null; + node.siblings = []; + }); + children.splice(0, children.length); + }; + } + + // mehod + if (prop === 'replaceWith') { + return (...nodes) => { + const { siblings } = target; + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings, parentNode: target.parentNode }); + target.parentNode = null; + target.siblings = []; + siblings.splice(siblings.indexOf(proxyNode), 1, ...newNodes); + }; + } + + // mehod + if (prop === 'wrapWith') { + return (node) => { + node.children = [proxyNode]; + proxyNode.replaceWith(node); + }; + } + + // mehod + if (prop === 'after') { + return (...nodes) => { + const { siblings } = target; + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings, parentNode: target.parentNode }); + siblings.splice(siblings.indexOf(proxyNode) + 1, 0, ...newNodes); + }; + } + + // mehod + if (prop === 'before') { + return (...nodes) => { + const { siblings } = target; + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings, parentNode: target.parentNode }); + siblings.splice(siblings.indexOf(proxyNode), 0, ...newNodes); + }; + } + + // mehod + if (prop === 'append') { + return (...nodes) => { + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings: target.children, parentNode: proxyNode }); + target.children.push(...newNodes); + }; + } + + if (prop === 'prepend') { + return (...nodes) => { + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings: target.children, parentNode: proxyNode }); + target.children.unshift(...newNodes); + }; + } + + // mehod + if (prop === 'newChildren') { + return (...nodes) => { + const { children } = target; + proxyNode.removeChildren(); + // eslint-disable-next-line no-use-before-define + const newNodes = createNodes({ nodes, siblings: children, parentNode: proxyNode }); + children.push(...newNodes); + }; } + + // mehod + if (prop === 'queryAll') { + return (fn, settings) => queryAllNodes(target.children, fn, settings); + } + + if (prop === 'isProxy') { + return true; + } + 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; - } + // if (prop === 'children') { + // target.children = value; + // target.children.forEach((child, i) => { + // child.indexInParent = i; + // child.parentNode = p; + // console.log('chidren is read only property'); + // }); + // return value; + // } + + // if (['children'].includes(prop)) { + // // set value only once, then mutate the object + // if (!target[prop] && Array.isArray(value)) { + // target[prop] = value; + // } + // } + + // if (prop === 'parentNode') { + // target.parentNode = value; + // } target[prop] = value; return true; }, }); - return p; -}; + return proxyNode; +} + +function createNodes({ nodes, siblings = [], parentNode = null }) { + return nodes.map((n) => { + if (n.isProxy) { + n.remove(); + } + const node = + (n.isProxy && n) || + nodeProxy({ + class: [], + attributes: [], + children: [], + ...n, + }); + node.siblings = siblings; + node.parentNode = parentNode; + + if (node.children.length) { + const children = []; + const newNodes = [...node.children]; + node.removeChildren(); + + children.push( + ...createNodes({ + nodes: newNodes, + parentNode: node, + siblings: node.children, + }), + ); + node.append(...children); + } + + return node; + }); +} // extract the virtual dom from the real dom -export const generateDom = (virtualdom, reference = true) => { - const dom = []; +export const generateVirtualDom = (realDomNodes, { reference = true, parentNode = 'virtualDom' } = {}) => { + const isRoot = parentNode === 'virtualDom'; + const virtualDom = isRoot + ? nodeProxy({ + isRoot: true, + tag: parentNode, + parentNode: null, + siblings: [], + children: [], + }) + : { + children: [], + }; + // 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, reference) : []; + for (let i = 0; i < realDomNodes.length; i++) { + const element = realDomNodes[i]; const classList = element.classList && element.classList.length > 0 ? [...element.classList] : []; - const attributes = {}; - if (element.hasAttributes && element.hasAttributes()) { + if (element.hasAttributes?.()) { // eslint-disable-next-line no-plusplus for (let j = 0; j < element.attributes.length; j++) { const { name, value } = element.attributes[j]; attributes[name] = value; } } - dom.push( - nodeProxy({ - tag: element.tagName ? element.tagName.toLowerCase() : 'textNode', - children: child, - class: classList, - attributesValues: classToFlat(classList), - id: element.id, - attributes, - text: !element.tagName ? element.textContent : null, - reference: reference ? element : null, // no referent for stringfying the dom - }), - ); + + const node = nodeProxy({ + parentNode: isRoot ? virtualDom : parentNode, + siblings: virtualDom.children, + tag: element.tagName ? element.tagName.toLowerCase() : 'textNode', + class: classList, + attributesValues: classToFlat(classList), + id: element.id, + attributes, + text: !element.tagName ? element.textContent : null, + reference: reference ? element : null, // no referent for stringfying the dom + }); + + const { childNodes } = element; + node.children = childNodes.length ? generateVirtualDom(childNodes, { reference, parentNode: node }).children : []; + virtualDom.children.push(node); } - return dom; + return virtualDom; }; + // render the virtual dom into real dom export const renderVirtualDom = (virtualdom) => { + const siblings = virtualdom.isRoot ? virtualdom.children : virtualdom; const dom = []; // eslint-disable-next-line no-plusplus - for (let i = 0; i < virtualdom.length; i++) { - const element = virtualdom[i]; - const { children } = element; + for (let i = 0; i < siblings.length; i++) { + const virtualNode = siblings[i]; + const { children } = virtualNode; const child = children ? renderVirtualDom(children) : null; - if (element.tag !== 'textNode') { - const el = document.createElement(element.tag); - if (element.tag.indexOf('raqn-') === 0) { - if (!window.raqnInstances[element.tag]) { - window.raqnInstances[element.tag] = []; + if (virtualNode.tag !== 'textNode') { + const el = document.createElement(virtualNode.tag); + if (virtualNode.tag.indexOf('raqn-') === 0) { + el.setAttribute('raqnwebcomponent', ''); + if (!window.raqnInstances[virtualNode.tag]) { + window.raqnInstances[virtualNode.tag] = []; } - window.raqnInstances[element.tag].push(el); + window.raqnInstances[virtualNode.tag].push(el); } - if (element.class.length > 0) { - el.classList.add(...element.class); + if (virtualNode.class?.length > 0) { + el.classList.add(...virtualNode.class); } - if (element.id) { - el.id = element.id; + if (virtualNode.id) { + el.id = virtualNode.id; } - if (element.attributes) { + if (virtualNode.attributes) { // eslint-disable-next-line no-plusplus - Object.keys(element.attributes).forEach((name) => { - const value = element.attributes[name]; + Object.keys(virtualNode.attributes).forEach((name) => { + const value = virtualNode.attributes[name]; el.setAttribute(name, value); }); } - element.initialAttributesValues = classToFlat(element.class); - if (element.text) { - el.textContent = element.text; + virtualNode.initialAttributesValues = classToFlat(virtualNode.class); + if (virtualNode.text) { + el.textContent = virtualNode.text; } if (child) { @@ -142,34 +308,8 @@ export const renderVirtualDom = (virtualdom) => { } dom.push(el); } else { - dom.push(document.createTextNode(element.text)); + dom.push(document.createTextNode(virtualNode.text)); } } return dom; }; - -// receives a array of action to reduce the virtual dom -export const curryManipulation = - (items = []) => - (virtualdom) => - items.reduce((acc, m) => m(acc, 0), virtualdom); - -// preset manipulation for main page -export const manipulation = curryManipulation([ - recursive(cleanEmptyTextNodes), - recursive(cleanEmptyNodes), - recursive(eagerImage), - inject, - recursive(toWebComponent), - recursive(prepareGrid), - - loadModules, -]); -// preset manipulation for framents and external HTML -export const generalManipulation = curryManipulation([ - recursive(cleanEmptyTextNodes), - recursive(cleanEmptyNodes), - recursive(toWebComponent), - recursive(prepareGrid), - loadModules, -]); diff --git a/styles/styles.css b/styles/styles.css index eed57aef..a5f2f3ee 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -69,7 +69,8 @@ body { padding: 0; margin: 0; width: 100%; - padding-top: var(--header-height, 110px); + + /* padding-top: var(--header-height, 110px); */ } body.no-scroll { @@ -200,7 +201,7 @@ img { /* Set default block style to all raqn web components Use :where() to give lower specificity in order to not overwrite any display option set on the web component tag */ -:where([raqnWebComponent]) { +:where([raqnwebcomponent]) { display: block; } @@ -209,6 +210,12 @@ main > div > *:not(.full-width) { margin-inline: var(--container-width); } +:where(main, raqn-header, raqn-footer) > raqn-section > *:not(.full-width) { + margin-inline: var(--container-width); + width: initial; +} + + /* Change the above behavior by setting the full-with class on the block. This will make the background take the full width of the page */ .full-width { max-width: var(--max-width); @@ -266,6 +273,26 @@ button { pointer-events: none; } +.error-message-box { + background: red; + padding-block: 20px; + padding-inline: 20px; + border: 5px solid #000; + margin-block: 20px; + + & * { + color: white; + } +} + +.template-placeholder { + min-height: 100px; + border: 3px solid #000; + background: #f1f6fb; + align-content: center; + text-align: center; +} + #franklin-svg-sprite { display: none; }