UNPKG

@silexlabs/grapesjs-data-source

Version:
662 lines 28.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setPreviewIndex = setPreviewIndex; exports.isComponentVisible = isComponentVisible; exports.restoreOriginalRender = restoreOriginalRender; exports.renderPreview = renderPreview; exports.doRender = doRender; const state_1 = require("../model/state"); const types_1 = require("../types"); const token_1 = require("../model/token"); const expressionEvaluator_1 = require("../model/expressionEvaluator"); const dataSourceRegistry_1 = require("../model/dataSourceRegistry"); const dataSourceManager_1 = require("../model/dataSourceManager"); const commands_1 = require("../commands"); // DOM diffing function to compare and update nodes only when needed function updateNodeContent(oldNode, newNode) { var _a, _b; // If node types differ, we need to replace if (oldNode.nodeType !== newNode.nodeType) { (_a = oldNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(newNode.cloneNode(true), oldNode); return; } // Text nodes: compare and update text content if (oldNode.nodeType === Node.TEXT_NODE) { if (oldNode.textContent !== newNode.textContent) { oldNode.textContent = newNode.textContent; } return; } // Element nodes: compare tag names if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) { const oldEl = oldNode; const newEl = newNode; // If tag names differ, replace the entire node if (oldEl.tagName !== newEl.tagName) { (_b = oldNode.parentNode) === null || _b === void 0 ? void 0 : _b.replaceChild(newNode.cloneNode(true), oldNode); return; } // Update attributes // Remove old attributes that don't exist in new element const oldAttrs = oldEl.attributes; for (let i = oldAttrs.length - 1; i >= 0; i--) { const attrName = oldAttrs[i].name; if (!newEl.hasAttribute(attrName)) { oldEl.removeAttribute(attrName); } } // Set/update attributes from new element const newAttrs = newEl.attributes; for (let i = 0; i < newAttrs.length; i++) { const attr = newAttrs[i]; if (oldEl.getAttribute(attr.name) !== attr.value) { oldEl.setAttribute(attr.name, attr.value); } } // Recursively update children const oldChildren = Array.from(oldEl.childNodes); const newChildren = Array.from(newEl.childNodes); // Build a map of old children by ID for efficient lookup // Note: Multiple children can have the same ID (loop clones), so store arrays const oldChildrenById = new Map(); const oldChildrenWithoutId = []; oldChildren.forEach(child => { if (child.nodeType === Node.ELEMENT_NODE) { const id = child.id; if (id) { if (!oldChildrenById.has(id)) { oldChildrenById.set(id, []); } oldChildrenById.get(id).push(child); } else { oldChildrenWithoutId.push(child); } } else { oldChildrenWithoutId.push(child); } }); // Track which old children have been matched const matchedOldChildren = new Set(); // Process new children in order let lastInsertedNode = null; newChildren.forEach(newChild => { if (newChild.nodeType === Node.ELEMENT_NODE) { const newId = newChild.id; // Try to find matching old child by ID if (newId && oldChildrenById.has(newId)) { const candidates = oldChildrenById.get(newId); const oldChild = candidates.shift(); // Take first unmatched element if (!oldChild) { // All elements with this ID already matched, treat as no match const clonedNode = newChild.cloneNode(true); if (lastInsertedNode) { oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling); } else { oldEl.insertBefore(clonedNode, oldEl.firstChild); } lastInsertedNode = clonedNode; return; } matchedOldChildren.add(oldChild); // Update the existing element in place updateNodeContent(oldChild, newChild); // Ensure it's in the correct position if (lastInsertedNode) { if (lastInsertedNode.nextSibling !== oldChild) { oldEl.insertBefore(oldChild, lastInsertedNode.nextSibling); } } else { if (oldEl.firstChild !== oldChild) { oldEl.insertBefore(oldChild, oldEl.firstChild); } } lastInsertedNode = oldChild; } else { // No matching old child found, insert new element const clonedNode = newChild.cloneNode(true); if (lastInsertedNode) { oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling); } else { oldEl.insertBefore(clonedNode, oldEl.firstChild); } lastInsertedNode = clonedNode; } } else { // Text node or other - try to match with old children without IDs const matchIndex = oldChildrenWithoutId.findIndex(oldChild => !matchedOldChildren.has(oldChild) && oldChild.nodeType === newChild.nodeType); if (matchIndex >= 0) { const oldChild = oldChildrenWithoutId[matchIndex]; matchedOldChildren.add(oldChild); updateNodeContent(oldChild, newChild); if (lastInsertedNode) { if (lastInsertedNode.nextSibling !== oldChild) { oldEl.insertBefore(oldChild, lastInsertedNode.nextSibling); } } else { if (oldEl.firstChild !== oldChild) { oldEl.insertBefore(oldChild, oldEl.firstChild); } } lastInsertedNode = oldChild; } else { const clonedNode = newChild.cloneNode(true); if (lastInsertedNode) { oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling); } else { oldEl.insertBefore(clonedNode, oldEl.firstChild); } lastInsertedNode = clonedNode; } } }); // Remove old children that weren't matched oldChildren.forEach(oldChild => { if (!matchedOldChildren.has(oldChild)) { oldEl.removeChild(oldChild); } }); } } function getPrivateState(component, stateId) { return (0, state_1.getState)(component, stateId, false); } // Helper function to evaluate expressions with internal API function evaluateExpression(expression, component, resolvePreviewIndex = true) { try { // Convert StoredTokens to full Tokens first, like main branch did const tokens = expression.map(token => { var _a; return (0, token_1.fromStored)(token, ((_a = component.getId) === null || _a === void 0 ? void 0 : _a.call(component)) || null); }); const context = { dataSources: (0, dataSourceRegistry_1.getAllDataSources)(), filters: (0, dataSourceManager_1.getFilters)(), previewData: (0, dataSourceManager_1.getPreviewData)(), component, resolvePreviewIndex, }; return (0, expressionEvaluator_1.evaluateExpressionTokens)(tokens, context); } catch (e) { console.warn('Error evaluating expression:', e); return null; } } // Pure function to evaluate a single condition function evaluateCondition(expression, component) { return evaluateExpression(expression, component, true); } // Pure function to render innerHTML for a component at a specific loop index function renderInnerHTML(component, loopIndex) { const innerHTML = getPrivateState(component, types_1.Properties.innerHTML); if (innerHTML === null) { return null; } try { // Set preview index for loop context if (typeof loopIndex === 'number') { setPreviewIndexToLoopData(component, loopIndex); } const value = evaluateCondition(innerHTML.expression, component); return value !== null && value !== undefined ? String(value) : null; } catch (e) { console.warn('Error rendering innerHTML:', e); return null; } } // Pure function to set preview index on all tokens in a component // Exprorted for API function setPreviewIndexToLoopData(component, index) { const privateStates = component.get('privateStates') || []; privateStates.forEach((state) => { if (state.expression && state.expression.length > 0) { setPreviewIndex(state.expression, index); } }); } function setPreviewIndex(expression, index, group) { expression.forEach((token) => { if (token.type === 'state' && token.storedStateId === '__data' || (token.type === 'state' && token.storedStateId === 'items')) { token.previewIndex = index; token.previewGroup = group; } else if (token.type === 'property' || token.type === 'filter') { token.previewIndex = index; token.previewGroup = group; } }); } function getPreviewIndex(component) { const privateStates = component.get('privateStates') || []; for (const state of privateStates) { if (state.expression && state.expression.length > 0) { for (const token of state.expression) { if ((token.type === 'state' && token.storedStateId === '__data') || token.type === 'property' || token.type === 'filter') { return token.previewIndex; } } } } return undefined; } function renderLoopData(component) { try { const __data = getPrivateState(component, types_1.Properties.__data); if (__data === null) { return null; } const result = evaluateExpression(__data.expression, component, false); // Get full array return Array.isArray(result) ? JSON.parse(JSON.stringify(result)) : null; } catch (e) { console.warn('Error getting loop item:', e); return null; } } // Export for tests function isComponentVisible(component) { const condition1State = getPrivateState(component, types_1.Properties.condition); const condition2State = getPrivateState(component, types_1.Properties.condition2); const conditionOperator = component.get('conditionOperator'); // If no condition is set, component is visible if (!condition1State || !(condition1State === null || condition1State === void 0 ? void 0 : condition1State.expression) || (condition1State === null || condition1State === void 0 ? void 0 : condition1State.expression.length) === 0) { return true; } let condition1Value; try { condition1Value = evaluateExpression(condition1State.expression, component, true); } catch (e) { console.warn('Error evaluating condition1:', e); // If condition evaluation fails but no operator is set, default to visible if (conditionOperator === undefined || conditionOperator === null) { return true; } // For explicit operators, treat failed evaluation as falsy condition1Value = null; } // For unary operators, only condition1 is needed switch (conditionOperator) { case types_1.UnariOperator.TRUTHY: return !!condition1Value; case types_1.UnariOperator.FALSY: return !condition1Value; case types_1.UnariOperator.EMPTY_ARR: return Array.isArray(condition1Value) && condition1Value.length === 0; case types_1.UnariOperator.NOT_EMPTY_ARR: return Array.isArray(condition1Value) && condition1Value.length > 0; case undefined: case null: // If no operator is specified but condition1 exists, default to TRUTHY behavior return !!condition1Value; default: } // For binary operators, we need condition2 if (!condition2State || !condition2State.expression || condition2State.expression.length === 0) { return false; } let condition2Value; try { condition2Value = evaluateExpression(condition2State.expression, component, true); } catch (e) { console.warn('Error evaluating condition2:', e); // If condition2 evaluation fails, treat as falsy condition2Value = null; } // Apply binary operator switch (conditionOperator) { case types_1.BinaryOperator.EQUAL: return condition1Value == condition2Value; case types_1.BinaryOperator.NOT_EQUAL: return condition1Value !== condition2Value; case types_1.BinaryOperator.GREATER_THAN: return Number(condition1Value) > Number(condition2Value); case types_1.BinaryOperator.LESS_THAN: return Number(condition1Value) < Number(condition2Value); case types_1.BinaryOperator.GREATER_THAN_OR_EQUAL: return Number(condition1Value) >= Number(condition2Value); case types_1.BinaryOperator.LESS_THAN_OR_EQUAL: return Number(condition1Value) <= Number(condition2Value); default: throw new Error(`Unknown operator ${conditionOperator}`); } } function renderAttributes(component) { const privateStates = component.get('privateStates') || []; privateStates.forEach((state) => { var _a; // Skip condition states and internal data states - they should not become HTML attributes if (state.id && state.id !== types_1.Properties.innerHTML && state.id !== types_1.Properties.__data && state.id !== types_1.Properties.condition && state.id !== types_1.Properties.condition2 && state.expression) { try { const value = evaluateExpression(state.expression, component, true); if (value !== null && value !== undefined) { (_a = component.view) === null || _a === void 0 ? void 0 : _a.el.setAttribute(state.label || state.id, String(value)); } } catch (e) { console.warn(`Error evaluating attribute ${state.id}:`, e); } } }); } // // Helper to extend a component instance // function extendComponent(comp: Component, onRender: (c: Component) => void) { // // Extend view // if (comp.view) { // const origOnRender = comp.view.onRender?.bind(comp.view) // comp.view.onRender = function (opts: ClbObj) { // if (origOnRender) origOnRender(opts) // onRender(comp) // } // } // } // // /** // * Applies extended model/view logic to all existing components in the editor. // * @param editor The GrapesJS editor instance // */ // function extendAllComponents(editor: Editor, onRender: (c: Component) => void, parents: Components = editor.getComponents()) { // parents.forEach((comp) => { // extendComponent(comp, onRender) // extendAllComponents(editor, onRender, comp.components()) // }) // } // function renderContent(comp, deep) { const innerHtml = renderInnerHTML(comp); if (innerHtml === null) { comp.components() .forEach(c => renderPreview(c, deep + 1)); } else { const el = comp.view.el; // Parse new HTML into a temporary container const temp = document.createElement('div'); temp.innerHTML = innerHtml; // Get children from both old and new const oldChildren = Array.from(el.childNodes); const newChildren = Array.from(temp.childNodes); // Update existing children const minLength = Math.min(oldChildren.length, newChildren.length); for (let i = 0; i < minLength; i++) { updateNodeContent(oldChildren[i], newChildren[i]); } // Remove extra old children for (let i = oldChildren.length - 1; i >= minLength; i--) { el.removeChild(oldChildren[i]); } // Add new children for (let i = minLength; i < newChildren.length; i++) { el.appendChild(newChildren[i].cloneNode(true)); } } } // Restore original GrapesJS rendering without preview data function restoreOriginalRender(comp) { const view = comp.view; if (!view) { return; } // Force standard GrapesJS render view.render(); // Recursively restore all children comp.components().forEach(child => restoreOriginalRender(child)); } // exported for unit tests only function renderPreview(comp, deep = 0) { const view = comp.view; if (!view) { return; } const el = view.el; const __data = renderLoopData(comp); if (__data) { if (__data.length === 0) { el.remove(); } else { // Remove all existing loop clones (siblings with same ID from previous render) const componentId = el.id; let nextSibling = el.nextElementSibling; while (nextSibling && nextSibling.id === componentId) { const toRemove = nextSibling; nextSibling = nextSibling.nextElementSibling; toRemove.remove(); } const initialPreviewIndex = getPreviewIndex(comp) || 0; // Render each loop iteration // Render first iteration in the original element // FIXME: as a workaround we need to loop reverse on the __data array, I have no idea why const fromIdx = __data.length - 1; const toIdx = 0; setPreviewIndexToLoopData(comp, fromIdx); const isVisible = isComponentVisible(comp); if (isVisible) { renderContent(comp, deep); renderAttributes(comp); } else { el.remove(); } // For subsequent iterations: clone first, then render into original (without diffing), then repeat for (let idx = fromIdx - 1; idx >= toIdx; idx--) { // Check if this iteration should be visible setPreviewIndexToLoopData(comp, idx); const isVisibleAtIdx = isComponentVisible(comp); // Skip invisible iterations - don't create a clone if (!isVisibleAtIdx) { continue; } // Clone the current state (with previous iteration's content) const clone = el.cloneNode(true); // Remove selection marker from the element and its children clone.classList.remove('gjs-selected'); clone.querySelectorAll('.gjs-selected').forEach(el => el.classList.remove('gjs-selected')); // Remove hidden elements from the clone to prevent them from appearing in the output const hiddenInClone = clone.querySelectorAll('[style*="display: none"]'); hiddenInClone.forEach(hiddenEl => hiddenEl.remove()); // Reset visibility on the ORIGINAL element's children before rendering next iteration // This ensures the next iteration starts with all elements visible const hiddenInOriginal = el.querySelectorAll('[style*="display: none"]'); hiddenInOriginal.forEach(hiddenEl => { if (hiddenEl instanceof HTMLElement && hiddenEl.style) { hiddenEl.style.removeProperty('display'); // Remove empty style attribute if (hiddenEl.style.length === 0) { hiddenEl.removeAttribute('style'); } } }); // Keep the selection mechanism - use GrapesJS component API clone.addEventListener('click', (event) => { const clickedElement = event.target; // Find the corresponding GrapesJS component to select let targetComponent = comp; // Look for the component ID directly in the clicked element if (clickedElement.id) { // Try to find a component with this ID in the entire editor const editor = comp.em; const findComponentById = (comp, id) => { if (comp.getId() === id) return comp; for (const child of comp.components()) { const found = findComponentById(child, id); if (found) return found; } return null; }; // Search from the root of the editor const wrapper = editor === null || editor === void 0 ? void 0 : editor.getWrapper(); const foundComp = wrapper ? findComponentById(wrapper, clickedElement.id) : null; if (foundComp) { targetComponent = foundComp; } else { // Fallback: Use path-based approach const path = []; let current = clickedElement; // Build path from clicked element up to clone, but only count elements with gjs attributes while (current && current !== clone) { const parent = current.parentElement; if (parent) { const siblings = Array.from(parent.children).filter(child => child.hasAttribute('data-gjs-type')); const index = siblings.indexOf(current); if (index >= 0) { path.unshift(index); } current = parent; } else { break; } } // Navigate the component tree using the path let currentComp = comp; for (let i = 0; i < path.length; i++) { const index = path[i]; const children = currentComp.components(); if (index < children.length) { currentComp = children.at(index); } else { break; } } targetComponent = currentComp; } } // Use GrapesJS API to select the component setTimeout(() => { const editor = targetComponent.em; if (editor) { editor.setSelected(targetComponent); } }); event.preventDefault(); event.stopImmediatePropagation(); }); // Add the clone to the canvas el.insertAdjacentElement('afterend', clone); // Render the current iteration into the original element // NOTE: We skip the diffing here because we're in a loop creating initial elements // Diffing only helps on re-renders, not initial renders const innerHtml = renderInnerHTML(comp); if (innerHtml === null) { comp.components() .forEach(c => renderPreview(c, deep + 1)); } else { // Just set innerHTML directly without diffing in the loop el.innerHTML = innerHtml; } renderAttributes(comp); } setPreviewIndexToLoopData(comp, initialPreviewIndex); } } else { const isVisible = isComponentVisible(comp); if (isVisible) { // Make sure the element is visible (in case it was hidden before) if (el.style && el.style.display === 'none') { el.style.removeProperty('display'); // Remove empty style attribute if (el.style.length === 0) { el.removeAttribute('style'); } } renderContent(comp, deep); renderAttributes(comp); } else { // Don't remove the element, just hide it // This prevents breaking loop rendering where the same element // is referenced across multiple iterations if (el.parentElement && el.style) { el.style.display = 'none'; } } } } function doRender(editor) { var _a, _b; if (!((_b = (_a = editor.getWrapper()) === null || _a === void 0 ? void 0 : _a.view) === null || _b === void 0 ? void 0 : _b.el)) { return; } try { editor.trigger(types_1.PREVIEW_RENDER_START); renderPreview(editor.getWrapper()); requestAnimationFrame(() => { editor.trigger(types_1.PREVIEW_RENDER_END); }); } catch (err) { editor.trigger(types_1.PREVIEW_RENDER_ERROR, err); console.error('Error during preview render:', err); } } let renderTimeoutId = null; let debounceDelay = 500; function debouncedRender(editor) { if (renderTimeoutId) { clearTimeout(renderTimeoutId); } renderTimeoutId = setTimeout(() => { doRender(editor); renderTimeoutId = null; }, debounceDelay); } // Helper function to clean up loop clones for a component function cleanupLoopClones(component) { const view = component.view; if (!view || !view.el) { return; } const el = view.el; const componentId = el.id; // Remove all loop clones (siblings with same ID) let nextSibling = el.nextElementSibling; while (nextSibling && nextSibling.id === componentId) { const toRemove = nextSibling; nextSibling = nextSibling.nextElementSibling; toRemove.remove(); } // Recursively clean up clones for all children component.components().forEach(child => cleanupLoopClones(child)); } exports.default = (editor, opts) => { const events = opts.previewRefreshEvents.split(' '); for (const eventName of events) { editor.on(eventName, () => { if ((0, commands_1.getPreviewActive)()) { debouncedRender(editor); } }); } // Clean up loop clones when a component is removed editor.on('component:remove', (component) => { cleanupLoopClones(component); }); setTimeout(() => { debounceDelay = opts.previewDebounceDelay; }, 1000); }; //# sourceMappingURL=canvas.js.map