@silexlabs/grapesjs-data-source
Version:
Grapesjs Data Source
662 lines • 28.5 kB
JavaScript
;
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