diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 3948be1..89825c2 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -7,7 +7,7 @@ raqn-grid { --grid-align-items: initial; --grid-justify-content: initial; --grid-align-content: initial; - --grid-template-columns: initial; + --grid-template-columns: 1fr 1fr; --grid-template-rows: initial; --grid-background: var(--background, black); --grid-color: var(--text, white); diff --git a/blocks/grid/grid.editor.js b/blocks/grid/grid.editor.js index ac1c10a..2f13749 100644 --- a/blocks/grid/grid.editor.js +++ b/blocks/grid/grid.editor.js @@ -5,57 +5,59 @@ export default function config() { inputs: [], }, attributes: { - grid: { - 'template-rows': { + data: { + 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.', + }, + }, + style: { + '--grid-template-rows': { type: 'text', label: 'Row', helpText: 'The row number.', value: '1fr', }, - 'template-columns': { + '--grid-template-columns': { type: 'text', label: 'Columns', helpText: 'The column number.', value: '1fr 1fr', }, - gap: { + '--grid-gap': { type: 'text', label: 'Gap', helpText: 'The gap between the grid items.', value: '20px', }, - height: { + '--grid-height': { type: 'text', label: 'Height', helpText: 'The height of the grid.', value: 'initial', }, - width: { + '--grid-width': { type: 'text', label: 'Width', helpText: 'The width of the grid.', value: 'auto', }, - reverse: { - type: 'select', - options: [ - { - label: 'Default', - value: 'default', - }, - { - label: 'True', - value: 'true', - }, - { - label: 'Alternate', - value: 'alternate', - }, - ], - label: 'Reverse', - helpText: 'Reverse the order of the grid items.', - }, - 'justify-items': { + '--grid-justify-items': { type: 'select', options: [ { @@ -78,7 +80,7 @@ export default function config() { label: 'Justify Items', helpText: 'The alignment of the items along the inline (row) axis.', }, - 'align-items': { + '--grid-align-items': { type: 'select', options: [ { @@ -101,7 +103,7 @@ export default function config() { label: 'Align Items', helpText: 'The alignment of the items along the block (column) axis.', }, - 'justify-content': { + '--grid-justify-content': { type: 'select', options: [ { @@ -136,7 +138,7 @@ export default function config() { label: 'Justify Content', helpText: 'The alignment of the grid along the inline (row) axis.', }, - 'align-content': { + '--grid-align-content': { type: 'select', options: [ { diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index 13bbb73..18383e3 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -8,6 +8,24 @@ export default class Grid extends ComponentBase { dependencies = componentList.grid.module.dependencies; + attributesValues = { + all :{ + style: { + '--grid-gap': 'initial', + '--grid-height': 'initial', + '--grid-width': 'initial', + '--grid-justify-items':' initial', + '--grid-align-items':' initial', + '--grid-justify-content':' initial', + '--grid-align-content':' initial', + '--grid-template-columns':' 1fr 1fr', + '--grid-template-rows':' initial', + '--grid-background': 'var(--background, black)', + '--grid-color': 'var(--text, white)', + }, + }, + }; + async onAttributeReverseChanged({ oldValue, newValue }) { await this.initialization; diff --git a/blocks/popup/popup.js b/blocks/popup/popup.js index 2fb59ae..34fec23 100644 --- a/blocks/popup/popup.js +++ b/blocks/popup/popup.js @@ -57,7 +57,8 @@ export default class Popup extends ComponentBase { this.closeOnEsc = this.closeOnEsc.bind(this); } - onInit() { + init() { + console.log('Popup initialized'); this.showPopup(false); this.createPopupHtml(); this.setUrlFromTarget(); @@ -103,12 +104,13 @@ export default class Popup extends ComponentBase { } addListeners() { - this.elements.popupCloseBtn.addEventListener('click', () => { - this.dataset.active = false; - }); - this.elements.popupOverlay.addEventListener('click', () => { - this.dataset.active = false; - }); + console.log('Popup listeners added'); + // this.elements.popupCloseBtn.addEventListener('click', () => { + // this.dataset.active = false; + // }); + // this.elements.popupOverlay.addEventListener('click', () => { + // this.dataset.active = false; + // }); } connected() { diff --git a/blocks/theming/theming.editor.js b/blocks/theming/theming.editor.js index 3d67b00..a4a3b41 100644 --- a/blocks/theming/theming.editor.js +++ b/blocks/theming/theming.editor.js @@ -10,7 +10,8 @@ export default function config() { if (!listener) { const name = 'raqn-theming'; [themeInstance] = window.raqnInstances[name]; - + themeInstance.finish = () => {}; + publish( MessagesEvents.theme, { name: 'theme', data: themeInstance.themeJson }, @@ -26,7 +27,7 @@ export default function config() { const { data } = params; const row = Object.keys(data).map((key) => data[key]); readValue(row, themeInstance.variations); - themeInstance.defineVariations(readValue(row, themeInstance.variations)); + themeInstance.defineVariations(); themeInstance.styles(); } } diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index 78cdce7..ad35503 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -10,8 +10,8 @@ import { unFlat, getBaseUrl, runTasks, + isPreview, } from '../../scripts/libs.js'; -import { externalConfig } from '../../scripts/libs/external-config.js'; import { publish } from '../../scripts/pubsub.js'; const k = Object.keys; @@ -19,6 +19,10 @@ const k = Object.keys; export default class Theming extends ComponentBase { variations = {}; + themeJson = {}; + + fonts = ''; + setDefaults() { super.setDefaults(); this.scapeDiv = document.createElement('div'); @@ -34,7 +38,6 @@ export default class Theming extends ComponentBase { fontFaceTemplate(data) { const names = Object.keys(data); - this.fontFace = names .map((key) => { // files @@ -58,6 +61,17 @@ export default class Theming extends ComponentBase { .join(''); } + get fontFace() { + if (!this.fonts) { + this.fontFaceTemplate(this.themeJson.fontface); + } + return this.fonts; + } + + set fontFace(value) { + this.fonts = value; + } + escapeHtml(unsafe) { this.scapeDiv.textContent = unsafe; return this.scapeDiv.innerHTML; @@ -74,7 +88,8 @@ export default class Theming extends ComponentBase { return ` @media ${query} { ${callback(obj[bp], options.byName[bp])} -}`; +} +`; } // regular return callback(obj[bp], 'all'); @@ -95,21 +110,9 @@ export default class Theming extends ComponentBase { async processFragment(response, type = 'color') { if (response.ok) { - const isComponent = type === 'component'; - - const responseData = await (isComponent ? response : response.json()); + const responseData = await response.json(); this.themeJson[type] = responseData; - if (type === 'fontface') { - this.fontFaceTemplate(responseData); - } else if (isComponent) { - Object.keys(responseData).forEach((key) => { - if (key.indexOf(':') === 0 || responseData[key].data.length === 0) return; - this.componentsConfig[key] ??= {}; - this.componentsConfig[key] = readValue(responseData[key].data, this.componentsConfig[key]); - }); - } else { - this.variations = readValue(responseData.data, this.variations); - } + this.variations = readValue(responseData.data, this.variations); return this.themeJson[type]; } return false; @@ -174,6 +177,7 @@ ${k(f) async loadFragment() { const { themeConfig } = metaTags; + // no component config required in this file only color font layout and fontface const themeConfigs = getMetaGroup(themeConfig.metaNamePrefix); const base = getBaseUrl(); await runTasks.call( @@ -190,10 +194,7 @@ ${k(f) return {}; } - const response = - name === 'component' - ? externalConfig.loadConfig(true) // use the loader to prevent duplicated calls - : await fetch(`${name !== 'fontface' ? base : ''}${content}.json`); + const response = await fetch(`${name !== 'fontface' ? base : ''}${content}.json`); return this.processFragment(response, name); }), ); @@ -202,9 +203,13 @@ ${k(f) this.styles, ); - setTimeout(() => { - document.body.style.display = 'block'; + setTimeout(() => this.finish()); + } + + finish() { + document.body.style.display = 'block'; + if (isPreview() && !window.location.search.includes('previewOf')) { publish('raqn:page:load', {}, { usePostMessage: true, targetOrigin: '*' }); - }); + } } } diff --git a/scripts/component-base.js b/scripts/component-base.js index 04adaf4..90cc9c0 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -289,8 +289,11 @@ export default class ComponentBase extends HTMLElement { } applyClass({ oldValue, newValue }) { + if (oldValue === newValue) return; + if (Array.isArray(newValue)) this.classList.add(...newValue); + if (typeof newValue === 'string' && newValue.includes(' ')) this.classList.add(...newValue.split(' ')); if (oldValue?.length) this.classList.remove(...oldValue); - if (newValue?.length) this.classList.add(...newValue); + if (newValue?.length) this.classList.add(newValue); } applyAttribute({ oldValue, newValue }) { @@ -386,16 +389,20 @@ export default class ComponentBase extends HTMLElement { this, null, function fragmentVirtualDom() { - const element = document.createElement('div'); - element.innerHTML = this.fragmentContent; - return generateVirtualDom(element.childNodes); + const placeholder = document.createElement('div'); + placeholder.innerHTML = this.fragmentContent; + const virtualDom = generateVirtualDom(placeholder); + virtualDom.isRoot = true; + this.innerHTML = ''; + return virtualDom; }, // eslint-disable-next-line prefer-arrow-callback async function fragmentVirtualDomManipulation({ fragmentVirtualDom }) { await generalManipulation(fragmentVirtualDom); }, function renderFragment({ fragmentVirtualDom }) { - this.append(...renderVirtualDom(fragmentVirtualDom)); + console.log(fragmentVirtualDom); + this.append(...fragmentVirtualDom.children.map(dom => renderVirtualDom(dom))); }, ); } diff --git a/scripts/component-list/component-list.js b/scripts/component-list/component-list.js index 24020b9..0c86710 100644 --- a/scripts/component-list/component-list.js +++ b/scripts/component-list/component-list.js @@ -1,5 +1,6 @@ import { previewModule, getMeta, metaTags } from '../libs.js'; import { setPropsAndAttributes, getClassWithPrefix } from '../render/dom-utils.js'; +import { createNode } from '../render/dom.js'; const forPreviewList = await previewModule(import.meta, 'componentList'); @@ -95,13 +96,16 @@ export const componentList = { }, section: { tag: 'raqn-section', + queryLevel: 3, filterNode(node) { - if (node.tag === 'div' && ['main', 'virtualDom'].includes(node.parentNode.tag)) return true; + if (node.tag === 'div' && ['main', 'body'].includes(node.parentNode?.tag) + || node.tag === 'div' && node.parentNode?.isRoot) { + return true; + } return false; }, transform(node) { node.tag = this.tag; - // Handle sections with multiple grids const sectionGrids = node.queryAll((n) => n.hasClass('grid'), { queryLevel: 1 }); if (sectionGrids.length > 1) { @@ -110,7 +114,7 @@ export const componentList = { } else { node.remove(); } - return; + return node; } // Set options from section metadata to section. @@ -121,6 +125,7 @@ export const componentList = { setPropsAndAttributes(node); sectionMetaData.remove(); } + return node; }, }, navigation: { @@ -145,30 +150,37 @@ export const componentList = { }, picture: { tag: 'raqn-image', + // replace the current with the new one + method: 'replaceWith', filterNode(node) { - if (node.tag === 'p' && node.hasOnlyChild('picture')) return true; + if (node.tag === 'picture') return true; return false; }, transform(node) { - node.tag = this.tag; - + const webComponent = createNode({ tag: 'raqn-image' }); // 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(); + const { parentNode } = node; + if (parentNode?.nextSibling?.tag === 'p' && parentNode?.nextSibling?.firstChild?.tag === 'em') { + const link = parentNode?.nextSibling?.firstChild?.firstChild; + if (link?.tag === 'a') { + // crate a new link node and wrap the image with it + // so it's not reference on the old tree + const linkCopy = link.clone(); + // wrap the picture with the link + node.wrapWith(linkCopy); + // wrap the link with webcomponent + linkCopy.wrapWith(webComponent); + // remove the original link and paragraphs + parentNode.nextSibling.remove(); + return webComponent; } } + return webComponent; }, }, card: { tag: 'raqn-card', - method: 'replace', + method: 'replaceWith', module: { path: '/blocks/card/card', priority: 2, @@ -188,7 +200,7 @@ export const componentList = { tag: 'raqn-button', method: 'replace', filterNode(node) { - if (node.tag === 'p' && node.hasOnlyChild('a')) return true; + if (node.tag === 'p' && node.children.length === 1 && node.children[0].tag === 'a') return true; return false; }, module: { @@ -199,41 +211,26 @@ export const componentList = { }, 'popup-trigger': { tag: 'raqn-popup-trigger', - method: 'replaceWith', + method: 'replace', 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; - } + if (node.tag === 'a' && node.attributes.href.includes('#popup-trigger')) { + console.log('popup-trigger', node.attributes.href); + 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], - }, - ], - }, - { processChildren: true }, - ]; + const hash = href.substring(href.indexOf('#popup-trigger')); + const popupTrigger = createNode({ tag: 'raqn-popup-trigger', attributes: { 'data-action': hash } }); + const button = createNode({ tag: 'button', attributes: { 'aria-expanded': 'false', 'aria-haspopup': 'true', type: 'button' } }); + // create a clone of the node + const clone = node.clone(); + // wrap the clone with the popup trigger + clone.wrapWith(button); + button.wrapWith(popupTrigger); + // replace the original node with the clone + return popupTrigger; }, module: { path: '/blocks/popup-trigger/popup-trigger', diff --git a/scripts/component-preview.js b/scripts/component-preview.js new file mode 100644 index 0000000..07389b8 --- /dev/null +++ b/scripts/component-preview.js @@ -0,0 +1,50 @@ + +import { createNode, generateVirtualDom, renderVirtualDom } from './render/dom.js'; + +import { publish, subscribe } from './pubsub.js'; +import { generalManipulation } from './render/dom-manipulations.js'; + +export default { + init() { + const urlParams = new URLSearchParams(window.location.search); + const currentUUID = urlParams.get('previewOf'); + + const main = document.querySelector('main'); + window.raqnVirtualDom = generateVirtualDom(main.childNodes); + // wait to preview a specific component + subscribe('raqn:editor:preview:component', async (params) => { + const { component, uuid } = params; + + // @TODO virtual dom not usable anymore for this + + if (currentUUID === uuid) { + const section = createNode({ + tag: 'raqn-section', + }); + const theme = createNode({ + tag: 'div', + class: ['theming'], + children: [], + }); + + document.body.innerHTML = ''; + window.raqnVirtualDom.children = [theme, section]; + const manipulation = await generalManipulation(window.raqnVirtualDom); + document.body.append(...renderVirtualDom(manipulation)); + document.body.classList.add('color-default','font-default'); + setTimeout(async () => { + const currentComponent = document.querySelector(`${component.webComponentName}`); + currentComponent.attributesValues = component.attributesValues; + currentComponent.runConfigsByViewport(); + const bodyRect = await document.body.getBoundingClientRect(); + publish('raqn:editor:preview:render', { bodyRect, uuid }, { usePostMessage: true, targetOrigin: '*' }); + }, 1000); + } + }); + + publish('raqn:editor:preview:loaded', { uuid:currentUUID }, { + usePostMessage: true, + targetOrigin: '*', + }); + }, +}.init(); diff --git a/scripts/editor-preview.js b/scripts/editor-preview.js deleted file mode 100644 index a4e5edc..0000000 --- a/scripts/editor-preview.js +++ /dev/null @@ -1,41 +0,0 @@ -// import { publish } from './pubsub.js'; -import { deepMerge } from './libs.js'; -import { publish } from './pubsub.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 = ''; - const main = document.createElement('main'); - const webComponent = document.createElement(component.webComponentName); - webComponent.overrideExternalConfig = true; - webComponent.innerHTML = component.html; - main.appendChild(webComponent); - const virtualDom = generateVirtualDom(main.childNodes); - virtualDom[0].attributesValues = deepMerge({}, webComponent.attributesValues, component.attributesValues); - - main.innerHTML = ''; - document.body.append(main); - await main.append(...renderVirtualDom(await pageManipulation(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) => { - e.preventDefault(); - e.stopImmediatePropagation(); - }, - true, - ); - setTimeout(async () => { - const bodyRect = await document.body.getBoundingClientRect(); - publish('raqn:editor:preview:render', { bodyRect, uuid }, { usePostMessage: true, targetOrigin: '*' }); - }, 250); -} diff --git a/scripts/editor.js b/scripts/editor.js index 989fced..e901a13 100644 --- a/scripts/editor.js +++ b/scripts/editor.js @@ -1,8 +1,10 @@ import { deepMerge, getBaseUrl, loadModule, runTasks } from './libs.js'; -import { publish } from './pubsub.js'; +import { publish, subscribe } from './pubsub.js'; import { raqnComponents, raqnComponentsList } from './render/dom-reducers.js'; +import { raqnInstances } from './render/dom.js'; window.raqnEditor = window.raqnEditor || {}; + let watcher = false; export const MessagesEvents = { @@ -37,24 +39,29 @@ export default { }); watcher = true; } + subscribe(MessagesEvents.select, this.updateIntance.bind(this)); }, // alias to get all components mods: () => Object.keys(raqnComponents), // get values from component and sizes getComponentValues (dialog, element) { + const {webComponentName} = element; const domRect = element.getBoundingClientRect(); return { attributesValues: element.attributesValues, + webComponentName, uuid: element.uuid, domRect, + virtualNode: element.virtualNode?.toJSON(), dialog, }; }, // refresh all components refresh() { - this.mods().forEach((k) => { - this.refreshComponents(k); - }); + // this.mods().filter(k => window.raqnEditor[k]).forEach((k) => { + // this.refreshWebComponent(k); + // }); + this.sendUpdatedRender(); }, // send updated to editor interface sendUpdatedRender(uuid) { @@ -67,16 +74,16 @@ export default { }, // refresh one type of web components refreshWebComponent(k) { - const instancesOrdered = Array.from(document.querySelectorAll(k)); - window.raqnComponents[k].instances = instancesOrdered; - window.raqnEditor[k].instances = instancesOrdered.map((item) => + console.log('refreshWebComponent', k); + window.raqnEditor[k].instances = raqnInstances[k].map((item) => this.getComponentValues(window.raqnEditor[k].dialog, item), ); }, // refresh one instance of web component updateIntance(component) { const { webComponentName, uuid } = component; - const instance = window.raqnComponents[webComponentName].instances.find((element) => element.uuid === uuid); + const instance = raqnInstances[webComponentName].find((element) => element.uuid === uuid); + console.log('updateIntance', component, instance); if (!instance) return; instance.attributesValues = deepMerge({}, instance.attributesValues, component.attributesValues); instance.runConfigsByViewport(); @@ -111,9 +118,9 @@ export default { const masterConfig = window.raqnComponentsMasterConfig; const variations = masterConfig[name]; dialog.selection = variations; - window.raqnEditor[name] = { dialog, instances: [], name }; + window.raqnEditor[k] = { dialog, instances: [], name }; const instancesOrdered = Array.from(document.querySelectorAll(k)); - window.raqnEditor[name].instances = instancesOrdered.map((item) => + window.raqnEditor[k].instances = instancesOrdered.map((item) => this.getComponentValues(dialog, item), ); } diff --git a/scripts/index.js b/scripts/index.js index 9138547..376d5bb 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,12 +1,15 @@ import { generateVirtualDom, renderVirtualDom } from './render/dom.js'; -import { pageManipulation, templateManipulation } from './render/dom-manipulations.js'; -import { getMeta, metaTags, runTasks, isTemplatePage, previewModule } from './libs.js'; +import { pageManipulation, templateManipulation } from './render/dom-manipulations.js'; +import { getMeta, metaTags, runTasks, isTemplatePage, previewModule, isPreview } from './libs.js'; import { subscribe } from './pubsub.js'; await previewModule(import.meta); export default { init() { + if (isPreview() && window.location.search.includes('previewOf')) { + return runTasks.call(this, null, this.componentPreview); + } return runTasks.call( this, // all the tasks bellow will be bound to this object when called. null, @@ -16,9 +19,11 @@ export default { this.renderPage, ); }, - + componentPreview() { + import('./component-preview.js'); + }, generatePageVirtualDom() { - window.raqnVirtualDom = generateVirtualDom(document.body.childNodes); + window.raqnVirtualDom = generateVirtualDom(document.body); document.body.innerHTML = ''; }, @@ -27,7 +32,7 @@ export default { }, renderPage() { - const renderedDOM = renderVirtualDom(window.raqnVirtualDom); + const renderedDOM = window.raqnVirtualDom.children.map(n => renderVirtualDom(n)); if (renderedDOM) { document.body.append(...renderedDOM); @@ -92,4 +97,25 @@ export default { }, }.init().then(() => { subscribe('raqn:page:editor:load', () => import('./editor.js')); -}); \ No newline at end of file +}); + +// // example of usage +// const dom = document.createElement('raqn-section'); + +// // consistency with virtual dom interface without the need to use createNode +// /* +// * createNode({ +// * tag: 'raqn-section', +// * children: [] +// * }) +// * +// */ + +// // remove await from manipulate function +// // use a subscription on async subjects to wait if needed. + +// // avoid N to N + +// console.log(generateVirtualDom([dom]),await generalManipulation(generateVirtualDom([dom]))); + +// console.log(renderVirtualDom(generalManipulation(generateVirtualDom([dom])))); \ No newline at end of file diff --git a/scripts/index.preview.js b/scripts/index.preview.js index 8779836..73e6f9d 100644 --- a/scripts/index.preview.js +++ b/scripts/index.preview.js @@ -1,50 +1,15 @@ import { isPreview, loadModule } from './libs.js'; - -// 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; - } - } - } -}); +import { subscribe } from './pubsub.js'; export default { async init() { - this.loadPreviewStyles(); + return this.loadPreviewStyles(); }, async loadPreviewStyles() { if (!isPreview()) return; loadModule('/styles/styles.preview', { loadJS: false, loadCss: true }); }, -}.init(); +}.init().then(() => { + subscribe('raqn:page:editor:load', () => import('./editor.js')); +}); diff --git a/scripts/libs.js b/scripts/libs.js index 4d1a3ec..cee0943 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -92,8 +92,8 @@ export const metaTags = { fallbackContent: '/configs/layout', // contentType: 'path without extension', }, - themeConfigComponent: { - metaName: 'theme-config-component', + componentsConfigs: { + metaName: 'config-component', fallbackContent: '/configs/components-config', // contentType: 'path without extension', }, diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js index 960a431..a83f964 100644 --- a/scripts/libs/external-config.js +++ b/scripts/libs/external-config.js @@ -18,14 +18,14 @@ export const externalConfig = { async loadConfig(rawConfig) { window.raqnComponentsConfig ??= (async () => { const { - themeConfigComponent: { metaName }, - themeConfig, + componentsConfigs: { metaName }, + componentsConfigs, } = metaTags; const metaConfigPath = getMeta(metaName); - if (!metaConfigPath.includes(`${themeConfig.fallbackContent}`)) { + if (!metaConfigPath.includes(`${componentsConfigs.fallbackContent}`)) { // eslint-disable-next-line no-console console.error( - `The configured "${metaName}" config url is not containing a "${themeConfig.fallbackContent}" folder.`, + `The configured "${metaName}" config url is not containing a "${componentsConfigs.fallbackContent}" folder.`, ); return {}; } diff --git a/scripts/render/dom-manipulations.js b/scripts/render/dom-manipulations.js index 3c7a0cd..e6ebdd4 100644 --- a/scripts/render/dom-manipulations.js +++ b/scripts/render/dom-manipulations.js @@ -21,7 +21,7 @@ export const pageManipulation = curryManipulation([ recursive(eagerImage), isPreview() && recursive(buildTplPlaceholder), inject, - toWebComponent, + recursive(toWebComponent), // fase 1 recursive(prepareGrid), loadModules, tplPageDuplicatedPlaceholder, @@ -31,7 +31,8 @@ export const pageManipulation = curryManipulation([ // preset manipulation for fragments and external HTML export const generalManipulation = curryManipulation([ recursive(cleanEmptyNodes), - toWebComponent, + recursive(eagerImage), + recursive(toWebComponent), recursive(prepareGrid), loadModules, ]); diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 4abbfb7..9986769 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -1,7 +1,8 @@ // eslint-disable-next-line import/prefer-default-export import { deepMerge, getMeta, loadAndDefine, previewModule } from '../libs.js'; -import { recursive, tplPlaceholderCheck, queryTemplatePlaceholders, setPropsAndAttributes } from './dom-utils.js'; +import { tplPlaceholderCheck, queryTemplatePlaceholders, setPropsAndAttributes } from './dom-utils.js'; import { componentList, injectedComponents } from '../component-list/component-list.js'; +import { createNode } from './dom.js'; window.raqnComponentsList ??= {}; window.raqnOnComponentsLoaded ??= []; @@ -31,21 +32,27 @@ export const eagerImage = (node) => { }; export const prepareGrid = (node) => { - 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); + if (node.children && node.children.length > 0) { + const grids = node.children.filter((child) => child.tag === 'raqn-grid'); + const gridItems = node.children.filter((child) => child.tag === 'raqn-grid-item'); + + grids.map((grid, i) => { + const initial = node.children.indexOf(grid); + const nextGridIndex = grids[i + 1] ? node.children.indexOf(grids[i + 1]) : node.children.length; + gridItems.map((item) => { + const itemIndex = node.children.indexOf(item); + // get elements between grid and item and insert into grid + if (itemIndex > initial && itemIndex < nextGridIndex) { + const children = node.children.splice(initial + 1, itemIndex - initial); + const gridItem = children.pop(); // remove grid item from children + gridItem.children = children; + grid.children.push(gridItem); + } + }); + return grid; }); } + return node; }; const addToLoadComponents = (blockSelector, config) => { @@ -63,48 +70,16 @@ const addToLoadComponents = (blockSelector, config) => { 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: [] }, - ); - // Simple and fast in place tag replacement - recursive((node) => { - replaceBlocks.forEach(([blockName, config]) => { - if (node?.class?.includes?.(blockName) || config.filterNode?.(node)) { - node.tag = config.tag; - setPropsAndAttributes(node); - addToLoadComponents(blockName, config); - } - }); - })(virtualDom); - - // More complex transformation need to be done in order based on a separate query for each component. - queryBlocks.forEach(([blockName, config]) => { - const filter = - config.filterNode?.bind(config) || ((node) => node?.class?.includes?.(blockName) || node.tag === blockName); - 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?.length)) && config.method) { - const newNode = transformNode || defaultNode; - newNode[0].class ??= []; - newNode[0].class.push(...node.class); - setPropsAndAttributes(newNode[0]); - node[config.method](...newNode); - } + // recursive((node) => { + componentConfigList.forEach(([blockName, config]) => { + const { method = 'replace', tag, filterNode } = config; + if (virtualDom.tag === blockName || virtualDom?.class?.includes?.(blockName) || filterNode?.bind(config)(virtualDom)) { + const transformNode = config?.transform?.bind(config)(virtualDom) || { tag }; + virtualDom[method](transformNode); + setPropsAndAttributes(virtualDom); addToLoadComponents(blockName, config); - }); + } }); }; @@ -137,8 +112,8 @@ export const loadModules = (nodes, extra = {}) => { // Just inject components that are not in the list export const inject = (nodes) => { - const [header] = nodes.children; - header.before(...injectedComponents); + const injects = injectedComponents.map((component) => createNode(component)); + nodes.children = [...injects, ...nodes.children]; }; // clear empty text nodes or nodes with only text breaklines and spaces diff --git a/scripts/render/dom-reducers.preview.js b/scripts/render/dom-reducers.preview.js index b8d4117..0558ff8 100644 --- a/scripts/render/dom-reducers.preview.js +++ b/scripts/render/dom-reducers.preview.js @@ -9,6 +9,7 @@ export const highlightTemplatePlaceholders = (tplVirtualDom) => { node.class.push('template-placeholder'); return true; }); + return tplVirtualDom; }; export const noContentPlaceholder = (node) => { diff --git a/scripts/render/dom-utils.js b/scripts/render/dom-utils.js index 2a61cde..413d561 100644 --- a/scripts/render/dom-utils.js +++ b/scripts/render/dom-utils.js @@ -16,7 +16,7 @@ export const recursive = }; export const queryAllNodes = (nodes, fn, settings) => { - const { currentLevel = 1, queryLevel } = settings || {}; + const { currentLevel = 1, queryLevel = 1 } = settings || {}; return nodes.reduce((acc, node) => { const hasParent = node.hasParentNode; const match = fn(node); diff --git a/scripts/render/dom.js b/scripts/render/dom.js index ac6a361..795be0c 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -3,6 +3,8 @@ import { queryAllNodes } from './dom-utils.js'; // define instances for web components window.raqnInstances = window.raqnInstances || {}; +export const { raqnInstances } = window; + // recursive apply the path of the parent / current node export const recursiveParent = (node) => { const current = `${node.tag}${node.class.length > 0 ? `.${[...node.class].join('.')}` : ''}`; @@ -16,12 +18,11 @@ const getSettings = (nodes) => (nodes.length > 1 && Object.hasOwn(nodes.at(-1), 'processChildren') && nodes.pop()) || {}; const nodeDefaults = () => ({ - isRoot: null, + isRoot: true, tag: null, class: [], id: null, parentNode: null, - siblings: [], children: [], customProps: {}, attributes: {}, @@ -30,8 +31,8 @@ const nodeDefaults = () => ({ // proxy object to enhance virtual dom node object. export function nodeProxy(rawNode) { - const proxyNode = new Proxy(rawNode, { - get(target, prop) { + return new Proxy(rawNode, { + get(target, prop, receiver) { if (prop === 'hasAttributes') { return () => Object.keys(target.attributes).length > 0; } @@ -41,21 +42,17 @@ export function nodeProxy(rawNode) { } if (prop === 'uuid') { - return rawNode.reference.uuid; + return target.reference.uuid; } if (prop === 'nextSibling') { const { siblings } = target; - return siblings[siblings.indexOf(proxyNode) + 1]; + return siblings[siblings.indexOf(receiver) + 1]; } if (prop === 'previousSibling') { const { siblings } = target; - return siblings[siblings.indexOf(proxyNode) - 1]; - } - - if (prop === 'indexInSiblings') { - return target.siblings.indexOf(proxyNode); + return siblings[siblings.indexOf(receiver) - 1]; } if (prop === 'firstChild') { @@ -94,61 +91,80 @@ export function nodeProxy(rawNode) { // mehod if (prop === 'remove') { return () => { - const { siblings } = target; - target.parentNode = null; - target.siblings = []; - siblings.splice(siblings.indexOf(proxyNode), 1); + const { parentNode } = target; + parentNode.children = parentNode.children.filter((child) => child !== receiver); }; } if (prop === 'removeChildren') { return () => { - const { children } = target; - children.forEach((node) => { - node.parentNode = null; - node.siblings = []; + receiver.children = []; + }; + } + + if (prop === 'replace') { + return (node) => { + Object.entries(node).forEach(([key, value]) => { + target[key] = value; }); - children.splice(0, children.length); + // eslint-disable-next-line no-param-reassign }; } // mehod if (prop === 'replaceWith') { return (...nodes) => { - const { siblings } = target; + // a tree node, so we need to replace it in the parent + if (target.parentNode?.children.length > 0) { // eslint-disable-next-line no-use-before-define - const newNodes = createNodes({ nodes, siblings, parentNode: target.parentNode, ...getSettings(nodes) }); - target.parentNode = null; - target.siblings = []; - siblings.splice(siblings.indexOf(proxyNode), 1, ...newNodes); + const newNodes = createNodes({ nodes, ...getSettings(nodes) }); + target.parentNode.children = [...target.parentNode.children].splice(target.indexInSiblings, 1, ...newNodes); + } else { + console.log('replaceWith: node has no parent', target.tag, nodes); + console.error('replaceWith: node has no parent'); + } }; } // mehod if (prop === 'wrapWith') { - return (node) => { - node.children = [proxyNode]; - proxyNode.replaceWith(node); + return (wrapper) => { + + // it's a single node with siblings, so we need to wrap it and replace it in the parent + if (target.parentNode?.children.length > 0) { + const arrayCopy = [...target.parentNode.children]; + arrayCopy.splice(target.indexInSiblings, 1, wrapper); + target.parentNode.children = arrayCopy; + } + // it's a single lose node, so we need to wrap it and return the wrapper + wrapper.children = [target]; + return wrapper; + }; + } + + if (prop === 'clone') { + return () => { + const clone = nodeProxy({ ...target }); + return clone; }; } // 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, ...getSettings(nodes) }); - siblings.splice(siblings.indexOf(proxyNode) + 1, 0, ...newNodes); + const newNodes = createNodes({ nodes, ...getSettings(nodes) }); + receiver.parentNode.children = [...receiver.parentNode.children].splice(receiver.indexInSiblings + 1, 0, ...newNodes); + return 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, ...getSettings(nodes) }); - siblings.splice(siblings.indexOf(proxyNode), 0, ...newNodes); + const newNodes = createNodes({ nodes, ...getSettings(nodes) }); + target.parentNode.children = [...receiver.parentNode.children].splice(receiver.indexInSiblings, 0, ...newNodes); }; } @@ -158,11 +174,10 @@ export function nodeProxy(rawNode) { // eslint-disable-next-line no-use-before-define const newNodes = createNodes({ nodes, - siblings: target.children, - parentNode: proxyNode, ...getSettings(nodes), }); - target.children.push(...newNodes); + // trigger setter + receiver.children = [...target.children, ...newNodes]; }; } @@ -171,26 +186,24 @@ export function nodeProxy(rawNode) { // eslint-disable-next-line no-use-before-define const newNodes = createNodes({ nodes, - siblings: target.children, - parentNode: proxyNode, ...getSettings(nodes), }); - target.children.unshift(...newNodes); + // trigger setter + receiver.children = [...target.children,...newNodes]; }; } // mehod if (prop === 'newChildren') { return (...nodes) => { - const { children } = target; - proxyNode.removeChildren(); + receiver.removeChildren(); // eslint-disable-next-line no-use-before-define - const newNodes = createNodes({ nodes, siblings: children, parentNode: proxyNode, ...getSettings(nodes) }); - children.push(...newNodes); + const newNodes = createNodes({ nodes, ...getSettings(nodes) }); + receiver.children = [...receiver.children,...newNodes]; }; } - // mehod + // mehod if (prop === 'queryAll') { return (fn, settings) => queryAllNodes(target.children, fn, settings); } @@ -212,131 +225,116 @@ export function nodeProxy(rawNode) { return target[prop]; }, + set(target, prop, value, receiver) { + // children setter handler and cleanup in one place + if (prop === 'children' && Array.isArray(value)) { + target.children.forEach((child) => { + child.parentNode = null; + child.siblings = []; + child.indexInSiblings = null; + }); + value.forEach((child) => { + child.parentNode = receiver; + child.siblings = value; + child.indexInSiblings = value.indexOf(child); + }); + target.children = value; + return true; + } + if (prop === 'parentNode') { + const oldParent = target.parentNode; + if (oldParent) { + // don't trigger setter + oldParent.children.filter((child) => child !== receiver); + } + target.parentNode = value; + } + target[prop] = value; + return true; + }, }); - return proxyNode; } // This method ensure new nodes added to the virtual dom are using the proxy. // Any plain object node will be wrapped in the proxy and parent/children dependencies will be handled. -function createNodes({ nodes, siblings = [], parentNode = null, processChildren = false } = {}) { +function createNodes({ nodes } = {}) { return nodes.map((n) => { - if (n.isProxy) { - n.remove(); - } - const node = - (n.isProxy && n) || - nodeProxy({ + if (n.isProxy) return n; + return nodeProxy({ ...nodeDefaults(), ...n, }); - node.siblings = siblings; - node.parentNode = parentNode; - - if (node.children.length && processChildren) { - const children = []; - const newNodes = [...node.children]; - const newChildren = []; - children.push( - ...createNodes({ - nodes: newNodes, - parentNode: node, - siblings: newChildren, - }), - ); - node.children = newChildren; - node.append(...children); - } - - return node; }); } // extract the virtual dom from the real dom -export const generateVirtualDom = (realDomNodes, { reference = true, parentNode = 'virtualDom' } = {}) => { - const isRoot = parentNode === 'virtualDom'; - const virtualDom = isRoot - ? nodeProxy({ - ...nodeDefaults(), - isRoot: true, - tag: parentNode, - reference: realDomNodes[0].parentNode, - }) - : { - children: [], - }; - +export const generateVirtualDom = (dom, { reference = true } = {}) => { // eslint-disable-next-line no-plusplus - 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?.()) { - // eslint-disable-next-line no-plusplus - for (let j = 0; j < element.attributes.length; j++) { - const { name, value } = element.attributes[j]; - if (!['id', 'class'].includes(name)) { - attributes[name] = value; - } + const element = dom; + const classList = element.classList && element.classList.length > 0 ? [...element.classList] : []; + const attributes = {}; + if (element.hasAttributes?.()) { + // eslint-disable-next-line no-plusplus + for (let j = 0; j < element.attributes.length; j++) { + const { name, value } = element.attributes[j]; + if (!['id', 'class'].includes(name)) { + attributes[name] = value; } } - - const node = nodeProxy({ - ...nodeDefaults(), - tag: element.tagName ? element.tagName.toLowerCase() : 'textNode', - parentNode: isRoot ? virtualDom : parentNode, - siblings: virtualDom.children, - class: 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 virtualDom; + const { childNodes } = element; + const childrenNodes = childNodes.length ? Array.from(childNodes).map(child => generateVirtualDom(child, { reference })) : []; + + const node = nodeProxy({ + ...nodeDefaults(), + }); + node.isRoot = false; + node.tag = element.tagName ? element.tagName.toLowerCase() : 'textNode'; + node.class = classList; + node.id = element.id; + node.attributes = attributes; + node.text = element.textContent; + node.children = childrenNodes; + + return node; }; // render the virtual dom into real dom -export const renderVirtualDom = (virtualdom) => { - const siblings = virtualdom.isRoot ? virtualdom.children : virtualdom; - const dom = []; +export const renderVirtualDom = (virtualNode) => { // eslint-disable-next-line no-plusplus - for (let i = 0; i < siblings.length; i++) { - const virtualNode = siblings[i]; - const children = virtualNode.children ? renderVirtualDom(virtualNode.children) : null; - if (virtualNode.tag !== 'textNode') { - const el = document.createElement(virtualNode.tag); - if (virtualNode.tag.indexOf('raqn-') === 0) { - el.setAttribute('raqnwebcomponent', ''); - window.raqnInstances[virtualNode.tag] ??= []; - window.raqnInstances[virtualNode.tag].push(el); - } - if (virtualNode.class?.length > 0) el.classList.add(...virtualNode.class); - if (virtualNode.id) el.id = virtualNode.id; - if (virtualNode.text?.length) el.textContent = virtualNode.text; - if (children) el.append(...children); + if (virtualNode.tag !== 'textNode') { + const el = document.createElement(virtualNode.tag); + el.virtualNode = virtualNode; + + if (virtualNode.tag.indexOf('raqn-') === 0) { + el.setAttribute('raqnwebcomponent', ''); + window.raqnInstances[virtualNode.tag] ??= []; + window.raqnInstances[virtualNode.tag].push(el); + } - Object.entries(virtualNode.attributes).forEach(([name, value]) => { - el.setAttribute(name, value); - }); + if (virtualNode.class?.length > 0) el.classList.add(...virtualNode.class); + if (virtualNode.id) el.id = virtualNode.id; + + Object.entries(virtualNode.attributes).forEach(([name, value]) => { + el.setAttribute(name, value); + }); - Object.entries(virtualNode.customProps).forEach(([name, value]) => { - el[name] = value; - }); + Object.entries(virtualNode.customProps).forEach(([name, value]) => { + el[name] = value; + }); + + virtualNode.reference = el; + const children = virtualNode.children ? virtualNode.children.map(node => renderVirtualDom(node)) : null; + if (children) el.append(...children); + return el; + } + + const textNode = document.createTextNode(virtualNode.text); + virtualNode.reference = textNode; + textNode.virtualNode = virtualNode; + return textNode; - virtualNode.reference = el; - el.virtualNode = virtualNode; - dom.push(el); - } else if (virtualNode.text?.length) { - const textNode = document.createTextNode(virtualNode.text); - virtualNode.reference = textNode; - textNode.virtualNode = virtualNode; - dom.push(textNode); - } - } - return dom; }; + +export const createNode = (node) => nodeProxy({ ...nodeDefaults(), ...node }); \ No newline at end of file