diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 4c95a4c..d206d24 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 732228e..5f5dccc 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -5,6 +5,26 @@ export default class Grid extends ComponentBase { // only one attribute is observed rest is set as css variables directly static observedAttributes = ['data-reverse']; + 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/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 ddf22b7..ad35503 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -10,13 +10,19 @@ import { unFlat, getBaseUrl, runTasks, + isPreview, } from '../../scripts/libs.js'; +import { publish } from '../../scripts/pubsub.js'; const k = Object.keys; export default class Theming extends ComponentBase { variations = {}; + themeJson = {}; + + fonts = ''; + setDefaults() { super.setDefaults(); this.scapeDiv = document.createElement('div'); @@ -32,7 +38,6 @@ export default class Theming extends ComponentBase { fontFaceTemplate(data) { const names = Object.keys(data); - this.fontFace = names .map((key) => { // files @@ -56,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; @@ -72,7 +88,8 @@ export default class Theming extends ComponentBase { return ` @media ${query} { ${callback(obj[bp], options.byName[bp])} -}`; +} +`; } // regular return callback(obj[bp], 'all'); @@ -93,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; @@ -172,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( @@ -197,8 +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 d5dc096..40deb7e 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -330,8 +330,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 }) { @@ -415,7 +418,29 @@ export default class ComponentBase extends HTMLElement { ); } } - + /* + async addFragmentContent() { + await runTasks.call( + this, + null, + function fragmentVirtualDom() { + 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 }) { + console.log(fragmentVirtualDom); + this.append(...fragmentVirtualDom.children.map(dom => renderVirtualDom(dom))); + }, + ); + */ fragmentVirtualDom() { const element = document.createElement('div'); element.innerHTML = this.fragmentContent; diff --git a/scripts/component-list/component-list.js b/scripts/component-list/component-list.js index b29cb99..28ac8fc 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'); @@ -45,6 +46,7 @@ export const componentList = { module: { path: '/blocks/theming/theming', priority: 1, + editor: true, }, }, breadcrumbs: { @@ -94,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) { @@ -109,7 +114,7 @@ export const componentList = { } else { node.remove(); } - return; + return node; } // Set options from section metadata to section. @@ -120,6 +125,7 @@ export const componentList = { setPropsAndAttributes(node); sectionMetaData.remove(); } + return node; }, }, navigation: { @@ -144,33 +150,42 @@ 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(); + console.log('linkCopy', linkCopy); + // 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, + editor: true, }, }, accordion: { @@ -185,7 +200,7 @@ export const componentList = { button: { tag: 'raqn-button', 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; }, transform(node) { @@ -206,6 +221,7 @@ export const componentList = { module: { path: '/blocks/button/button', priority: 0, + editor: true, }, }, 'popup-trigger': { @@ -266,6 +282,7 @@ export const componentList = { path: '/blocks/grid/grid', priority: 0, dependencies: ['grid-item'], + editor: true, }, }, 'grid-item': { 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 c19f74a..e901a13 100644 --- a/scripts/editor.js +++ b/scripts/editor.js @@ -1,8 +1,10 @@ -import { deepMerge, flat, getBaseUrl, loadModule } from './libs.js'; -import { publish } from './pubsub.js'; -import { generateVirtualDom } from './render/dom.js'; +import { deepMerge, getBaseUrl, loadModule, runTasks } from './libs.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 = { @@ -17,105 +19,80 @@ export const MessagesEvents = { themeUpdate: 'raqn:editor:theme:update', }; -export function refresh(id) { - Object.keys(window.raqnEditor).forEach((webComponentName) => { - const instancesOrdered = Array.from(document.querySelectorAll(webComponentName)); - window.raqnComponents[webComponentName].instances = instancesOrdered; - window.raqnEditor[webComponentName].instances = instancesOrdered.map((item) => - // eslint-disable-next-line no-use-before-define - getComponentValues(window.raqnEditor[webComponentName].dialog, item), +export default { + async init() { + // no mods no party + if (!this.mods().length) return; + // inicial run tasks + await runTasks.call( + this, + null, + this.loadEditorModules, + this.publishInit, ); - }); - const bodyRect = window.document.body.getBoundingClientRect(); - publish( - MessagesEvents.render, - { components: window.raqnEditor, bodyRect, uuid: id }, - { usePostMessage: true, targetOrigin: '*' }, - ); -} - -export function updateComponent(component) { - const { webComponentName, uuid } = component; - const instance = window.raqnComponents[webComponentName].instances.find((element) => element.uuid === uuid); - if (!instance) return; - instance.attributesValues = deepMerge({}, instance.attributesValues, component.attributesValues); - instance.runConfigsByViewport(); - refresh(uuid); -} -export function getComponentValues(dialog, element) { - const html = element.innerHTML; - window.document.body.style.height = 'auto'; - - const domRect = element.getBoundingClientRect(); - let { variables = {}, attributes = {} } = dialog; - variables = Object.keys(variables).reduce((data, variable) => { - const value = getComputedStyle(element).getPropertyValue(variable); - data[variable] = { ...variables[variable], value }; - return data; - }, {}); - attributes = Object.keys(attributes).reduce((data, attribute) => { - if (attribute === 'data') { - const flatData = flat(element.dataset); - Object.keys(flatData).forEach((key) => { - const value = flatData[key]; - if (attributes[attribute] && attributes[attribute][key]) { - if (data[attribute]) { - const extend = { ...attributes[attribute][key], value }; - data[attribute][key] = extend; - } else { - data[attribute] = { [key]: { ...attributes[attribute][key], value } }; - } - } + // update on resize + if (!watcher) { + window.addEventListener('resize', () => { + this.refresh(); + this.sendUpdatedRender(); }); - return data; + watcher = true; } - - const value = element.getAttribute(attribute); - data[attribute] = { ...attributes[attribute], value }; - return data; - }, {}); - const cleanData = Object.fromEntries(Object.entries(element)); - const { attributesValues, webComponentName, componentName, uuid } = cleanData; - const children = generateVirtualDom(element.children, false); - const editor = { ...dialog, attributes }; - return { attributesValues, webComponentName, componentName, uuid, domRect, dialog, editor, html, children }; -} - -export default function initEditor(listeners = true) { - Promise.all( - Object.keys(window.raqnComponents).map( - (componentName) => - new Promise((resolve) => { - setTimeout(async () => { - try { - const fn = window.raqnComponents[componentName]; - const name = fn.name.toLowerCase(); - const component = loadModule(`/blocks/${name}/${name}.editor`, { loadCSS: false }); - const mod = await component.js; - if (mod && mod.default) { - const dialog = await mod.default(); - - const masterConfig = window.raqnComponentsMasterConfig; - const variations = masterConfig[componentName]; - dialog.selection = variations; - window.raqnEditor[componentName] = { dialog, instances: [], name: componentName }; - const { webComponentName } = window.raqnInstances[componentName][0]; - const instancesOrdered = Array.from(document.querySelectorAll(webComponentName)); - window.raqnEditor[componentName].instances = instancesOrdered.map((item) => - getComponentValues(dialog, item), - ); - } - resolve(); - } catch (error) { - resolve(); - } - }); - }), - ), - ).finally(() => { + 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().filter(k => window.raqnEditor[k]).forEach((k) => { + // this.refreshWebComponent(k); + // }); + this.sendUpdatedRender(); + }, + // send updated to editor interface + sendUpdatedRender(uuid) { + const bodyRect = window.document.body.getBoundingClientRect(); + publish( + MessagesEvents.render, + { components: window.raqnEditor, bodyRect, uuid }, + { usePostMessage: true, targetOrigin: '*' }, + ); + }, + // refresh one type of web components + refreshWebComponent(k) { + 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 = raqnInstances[webComponentName].find((element) => element.uuid === uuid); + console.log('updateIntance', component, instance); + if (!instance) return; + instance.attributesValues = deepMerge({}, instance.attributesValues, component.attributesValues); + instance.runConfigsByViewport(); + this.refresh(); + this.sendUpdatedRender(uuid); + }, + // publish update to editor + publishInit() { const bodyRect = window.document.body.getBoundingClientRect(); - publish( MessagesEvents.loaded, { @@ -126,32 +103,30 @@ export default function initEditor(listeners = true) { }, { usePostMessage: true, targetOrigin: '*' }, ); - - if (!watcher) { - window.addEventListener('resize', () => { - refresh(); - }); - watcher = true; - } - }); - if (listeners) { - // init editor if message from parent - window.addEventListener('message', async (e) => { - if (e && e.data) { - const { message, params } = e.data; - switch (message) { - case MessagesEvents.select: - updateComponent(params); - break; - - case MessagesEvents.updateComponent: - updateComponent(params); - break; - - default: - break; + }, + /* load all editor modules */ + async loadEditorModules() { + // check if all components are loaded and then init the editor + await Promise.allSettled(this.mods().map((k) => new Promise((resolve) => { + raqnComponents[k].then(async (contructor) => { + const name = contructor.name.replace('raqn-', '').toLowerCase(); + if (raqnComponentsList[name] && raqnComponentsList[name].module && raqnComponentsList[name].module.editor) { + const component = loadModule(`/blocks/${name}/${name}.editor`, { loadCSS: false }); + const mod = await component.js; + if (mod && mod.default) { + const dialog = await mod.default(); + const masterConfig = window.raqnComponentsMasterConfig; + const variations = masterConfig[name]; + dialog.selection = variations; + window.raqnEditor[k] = { dialog, instances: [], name }; + const instancesOrdered = Array.from(document.querySelectorAll(k)); + window.raqnEditor[k].instances = instancesOrdered.map((item) => + this.getComponentValues(dialog, item), + ); + } } - } - }); - } -} + resolve(); + }); + }))); + }, +}.init(); \ No newline at end of file diff --git a/scripts/index.js b/scripts/index.js index 72d0e98..a5f8e11 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,12 +1,16 @@ 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() { - runTasks.call( + 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, this.generatePageVirtualDom, @@ -15,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 = ''; }, @@ -26,7 +32,8 @@ export default { }, renderPage() { - const renderedDOM = renderVirtualDom(window.raqnVirtualDom); + console.log('rendering page', window.raqnVirtualDom); + const renderedDOM = window.raqnVirtualDom.children.map(n => renderVirtualDom(n)); if (renderedDOM) { document.body.append(...renderedDOM); @@ -82,11 +89,34 @@ export default { if (!templateContent) return { stopTaskRun: true, value: null }; const element = document.createElement('div'); element.innerHTML = templateContent; - window.raqnTplVirtualDom = generateVirtualDom(element.childNodes); + window.raqnTplVirtualDom = generateVirtualDom(element); return window.raqnTplVirtualDom; }, - async templateVirtualDomManipulation({ templateVirtualDom }) { - await templateManipulation(templateVirtualDom); + async templateVirtualDomManipulation() { + await templateManipulation(window.raqnTplVirtualDom); }, -}.init(); +}.init().then(() => { + subscribe('raqn:page:editor:load', () => import('./editor.js')); +}); + +// // 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 91c91ad..1eb1c2f 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -92,8 +92,8 @@ export const metaTags = { fallbackContent: '/configs/layout', // contentType: 'path without extension', }, - componentsConfig: { - metaName: 'components-config', + componentsConfigs: { + metaName: 'config-component', fallbackContent: '/configs/components-config', // contentType: 'path without extension', }, @@ -474,8 +474,13 @@ export function loadModule(urlWithoutExtension, { loadCSS = true, loadJS = true export async function loadAndDefine(componentConfig) { const { tag, module: { path, loadJS, loadCSS } = {} } = componentConfig; if (window.raqnComponents[tag]) { - return { tag, module: window.raqnComponents[tag] }; + // fix + return { tag, module: await window.raqnComponents[tag] }; } + let resolveModule; + window.raqnComponents[tag] = new Promise((resolve) => { + resolveModule = resolve; + }); const { js } = loadModule(path, { loadJS, loadCSS }); @@ -484,7 +489,7 @@ export async function loadAndDefine(componentConfig) { if (module?.default?.prototype instanceof HTMLElement) { if (!window.customElements.get(tag)) { window.customElements.define(tag, module.default); - window.raqnComponents[tag] = module.default; + resolveModule(module.default); } } return { tag, module }; @@ -706,6 +711,8 @@ export const previewModule = async (path, name) => { return name ? module[name] : module; }; +/* Yield to the main thread */ + export function yieldToMain() { return new Promise((resolve) => { setTimeout(resolve, 0); diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js index d01aa60..12b826e 100644 --- a/scripts/libs/external-config.js +++ b/scripts/libs/external-config.js @@ -20,14 +20,14 @@ export const externalConfig = { async loadConfig(rawConfig) { window.raqnComponentsConfig ??= (async () => { const { - componentsConfig: { 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..8c0cd02 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, ]); @@ -39,7 +40,7 @@ export const generalManipulation = curryManipulation([ export const templateManipulation = curryManipulation([ recursive(cleanEmptyNodes), recursive(buildTplPlaceholder), - toWebComponent, + recursive(toWebComponent), recursive(prepareGrid), loadModules, replaceTemplatePlaceholders, diff --git a/scripts/render/dom-reducers.js b/scripts/render/dom-reducers.js index 38d4594..dae2c5d 100644 --- a/scripts/render/dom-reducers.js +++ b/scripts/render/dom-reducers.js @@ -1,12 +1,16 @@ // 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.raqnLoadedComponents ??= {}; +window.raqnComponentsList ??= {}; window.raqnOnComponentsLoaded ??= []; window.raqnComponents ??= {}; -const { raqnLoadedComponents } = window; +// export those variables to be used in other modules +export const { raqnComponentsList } = window; +export const { raqnComponents } = window; +export const { raqnOnComponentsLoaded } = window; export const forPreviewManipulation = async (manipulation) => (await previewModule(import.meta, manipulation)) || {}; export const { noContentPlaceholder, duplicatedPlaceholder } = await forPreviewManipulation(); @@ -28,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) => { @@ -51,8 +61,8 @@ const addToLoadComponents = (blockSelector, config) => { const toLoad = [blockSelector, ...(dependencies || [])]; toLoad.forEach((load) => { - if (!raqnLoadedComponents[load]) { - raqnLoadedComponents[load] = componentList[load]; + if (!raqnComponentsList[load]) { + raqnComponentsList[load] = componentList[load]; } }); }; @@ -60,54 +70,22 @@ 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); - }); + } }); }; // load modules in order of priority export const loadModules = (nodes, extra = {}) => { - const modules = { ...raqnLoadedComponents, ...extra }; + const modules = { ...raqnComponentsList, ...extra }; window.raqnOnComponentsLoaded = Object.keys(modules) .filter((component) => modules[component]?.module?.path) .sort((a, b) => modules[a].module.priority - modules[b].module.priority) @@ -121,6 +99,7 @@ export const loadModules = (nodes, extra = {}) => { setTimeout(async () => { try { const { module } = await loadAndDefine(modules[component]); + resolve(module); } catch (error) { reject(error); @@ -133,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 @@ -160,43 +139,38 @@ export const cleanEmptyNodes = (node) => { // in some cases when the placeholder is the only content in a block row the text is not placed in a

// wrap the placeholder in a

to normalize placeholder identification. export const buildTplPlaceholder = (node) => { - if (!tplPlaceholderCheck('div', node)) return; - - node.append( - { - tag: 'p', - children: [node.firstChild], - }, - { processChildren: true }, - ); + if (!tplPlaceholderCheck('p', node)) return; + const child = createNode({ tag: 'p'}); + node.wrapWith(child); }; export const replaceTemplatePlaceholders = (tplVirtualDom) => { const pageVirtualDom = window.raqnVirtualDom; - const { placeholders, placeholdersNodes } = queryTemplatePlaceholders(tplVirtualDom); - duplicatedPlaceholder?.(placeholdersNodes, placeholders); - placeholdersNodes.forEach((node, i) => { const placeholder = placeholders[i]; const placeholderContent = pageVirtualDom.queryAll( (n) => { if (n.tag !== 'raqn-section') return false; - if (n.hasClass(placeholder)) return true; - // if main content special placeholder is defined in the template 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; + if (n.hasClass(placeholder) === true) return true; + // console.log('n', n.tag, n.class, n.hasClass(placeholder)); + // // if main content special placeholder is defined in the template 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); + if (placeholderContent.length) { + node.tag = 'raqn-section'; + node.children = placeholderContent; + } else if (noContentPlaceholder) { noContentPlaceholder(node); } else node.remove(); }); - const [main] = pageVirtualDom.queryAll((n) => n.tag === 'main', { queryLevel: 1 }); - main.prepend(...tplVirtualDom.children); + const [main] = pageVirtualDom.queryAll((n) => n.tag === 'main'); + const section = createNode({ tag: 'raqn-section'}); + section.children = tplVirtualDom.firstChild.children; + main.children = [section]; }; diff --git a/scripts/render/dom-reducers.preview.js b/scripts/render/dom-reducers.preview.js index b8d4117..c0ea62a 100644 --- a/scripts/render/dom-reducers.preview.js +++ b/scripts/render/dom-reducers.preview.js @@ -2,6 +2,7 @@ import { tplPlaceholderCheck, queryTemplatePlaceholders } from './dom-utils.js'; import { isTemplatePage } from '../libs.js'; +import { createNode } from './dom.js'; export const highlightTemplatePlaceholders = (tplVirtualDom) => { tplVirtualDom.queryAll((node) => { @@ -9,24 +10,27 @@ export const highlightTemplatePlaceholders = (tplVirtualDom) => { node.class.push('template-placeholder'); return true; }); + console.log('tplVirtualDom', tplVirtualDom); + return tplVirtualDom; }; export const noContentPlaceholder = (node) => { node.class.push('template-placeholder'); - node.append({ + node.append(createNode({ tag: 'span', class: ['error-message-box'], text: "This template placeholder doesn't have content in this page", - }); + })); }; export const duplicatedPlaceholder = (placeholdersNodes, placeholders, markAll = false) => { const duplicatedPlaceholders = []; - + console.log('placeholders', placeholders); // filter duplicated placeholder excluding the first one. let duplicatesNodes = placeholders.flatMap((placeholder, i) => { if (placeholders.indexOf(placeholder) === i) return []; duplicatedPlaceholders.push(...placeholders.splice(i, 1)); + console.log('duplicatedPlaceholders', duplicatedPlaceholders); return placeholdersNodes.splice(i, 1); }); @@ -40,17 +44,20 @@ export const duplicatedPlaceholder = (placeholdersNodes, placeholders, markAll = } duplicatesNodes.forEach((node) => { + console.log('duplicatesNodes', node); node.class.push('template-placeholder'); - node.append({ + node.append(createNode({ tag: 'span', class: ['error-message-box'], text: 'This template placeholder is duplicated in the template', - }); + })); }); }; export const tplPageDuplicatedPlaceholder = (tplVirtualDom) => { + console.log('tplPageDuplicatedPlaceholder', tplVirtualDom); if (!isTemplatePage()) return; + console.log('isTemplatePage . tplPageDuplicatedPlaceholder', tplVirtualDom); const { placeholdersNodes, placeholders } = queryTemplatePlaceholders(tplVirtualDom); duplicatedPlaceholder(placeholdersNodes, placeholders, true); }; diff --git a/scripts/render/dom-utils.js b/scripts/render/dom-utils.js index 2a61cde..0721ab8 100644 --- a/scripts/render/dom-utils.js +++ b/scripts/render/dom-utils.js @@ -16,8 +16,9 @@ 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) => { + console.log('node', node); const hasParent = node.hasParentNode; const match = fn(node); const wasRemoved = !node.hasParentNode && hasParent; @@ -87,15 +88,15 @@ export const curryManipulation = return virtualDom; }; -export const tplPlaceholderCheck = (tag, node) => - tag === node.tag && node.hasOnlyChild('textNode') && node.firstChild.text.match(/\$\{tpl-content-[a-zA-Z1-9-]+\}/g); +export const tplPlaceholderCheck = (node) => + node.text?.match(/\$\{tpl-content-[a-zA-Z1-9-]+\}/g); -export const getTplPlaceholder = (node) => node.firstChild.text.trim().replace(/^\$\{|\}$/g, ''); +export const getTplPlaceholder = (node) => node.text.trim().replace(/^\$\{|\}$/g, ''); export const queryTemplatePlaceholders = (tplVirtualDom) => { const placeholders = []; const placeholdersNodes = tplVirtualDom.queryAll((n) => { - if (!tplPlaceholderCheck('p', n)) return false; + if (!tplPlaceholderCheck(n)) return false; const placeholder = getTplPlaceholder(n); placeholders.push(placeholder); return true; diff --git a/scripts/render/dom.js b/scripts/render/dom.js index 539bb0a..c4a2934 100644 --- a/scripts/render/dom.js +++ b/scripts/render/dom.js @@ -1,8 +1,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 +16,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 +29,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 +40,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 +89,77 @@ 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, 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 = [receiver]; + return wrapper; }; } + if (prop === 'clone') { + return () => nodeProxy({ ...target }); + } + // 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 +169,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,161 +181,151 @@ 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); + return (fn, settings) => { + const isMatch = fn(receiver, settings); + const childs = receiver.children.reduce((matchs,node) => [...matchs,...node.queryAll(fn, settings)],[]); + return [...(isMatch ? [receiver] : []), ...childs]; + }; } if (prop === 'isProxy') { return true; } + if (prop === 'toJSON') { + return () => { + const copy = { ...target }; + delete copy.reference; + delete copy.parentNode; + delete copy.siblings; + const childrenJson = copy.children.map((child) => child.toJSON()); + return { ...copy, children: childrenJson }; ; + }; + } + 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; + } + 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, + ...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, - }) - : { - 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; - el.virtualNode = virtualNode; + 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; - 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