// Elementor Element Manipulation Service - Find, update, delete, move elements
import { logger } from '../../utils/logger.js';
import { ErrorHandler, MCPError, ErrorCategory } from '../../utils/error-handler.js';
import { ElementorElement, ElementorData, ElementorWidget } from '../../types/elementor.js';
export class ElementorManipulationService {
/**
* Find an element by ID in the Elementor data tree
*/
findElementById(
elements: ElementorElement[],
elementId: string
): { element: ElementorElement | null; parent: ElementorElement | null; index: number } {
const search = (
els: ElementorElement[],
parent: ElementorElement | null = null
): { element: ElementorElement | null; parent: ElementorElement | null; index: number } => {
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (!el) continue;
if (el.id === elementId) {
return { element: el, parent, index: i };
}
if (el.elements && el.elements.length > 0) {
const found = search(el.elements, el);
if (found.element) return found;
}
}
return { element: null, parent: null, index: -1 };
};
return search(elements);
}
/**
* Find all elements of a specific type
*/
findElementsByType(elements: ElementorElement[], elType: string): ElementorElement[] {
const found: ElementorElement[] = [];
const search = (els: ElementorElement[]) => {
for (const el of els) {
if (el.elType === elType) {
found.push(el);
}
if (el.elements && el.elements.length > 0) {
search(el.elements);
}
}
};
search(elements);
return found;
}
/**
* Find widgets by widget type
*/
findWidgetsByType(elements: ElementorElement[], widgetType: string): ElementorWidget[] {
const found: ElementorWidget[] = [];
const search = (els: ElementorElement[]) => {
for (const el of els) {
if (el.elType === 'widget' && el.widgetType === widgetType) {
found.push(el as ElementorWidget);
}
if (el.elements && el.elements.length > 0) {
search(el.elements);
}
}
};
search(elements);
return found;
}
/**
* Update element settings
*/
updateElement(
elements: ElementorElement[],
elementId: string,
settings: Record<string, any>
): boolean {
const { element } = this.findElementById(elements, elementId);
if (!element) {
logger.warn(`Element ${elementId} not found for update`);
return false;
}
element.settings = {
...element.settings,
...settings
};
logger.debug(`Updated element ${elementId}`, { settings });
return true;
}
/**
* Delete an element
*/
deleteElement(elements: ElementorElement[], elementId: string): boolean {
const { parent, index } = this.findElementById(elements, elementId);
if (index === -1) {
logger.warn(`Element ${elementId} not found for deletion`);
return false;
}
if (parent && parent.elements) {
parent.elements.splice(index, 1);
} else {
elements.splice(index, 1);
}
logger.info(`Deleted element ${elementId}`);
return true;
}
/**
* Clone an element
*/
cloneElement(element: ElementorElement): ElementorElement {
const cloned = JSON.parse(JSON.stringify(element));
// Generate new IDs for all elements
const regenerateIds = (el: ElementorElement) => {
el.id = Math.random().toString(36).substring(2, 10);
if (el.elements && el.elements.length > 0) {
el.elements.forEach(regenerateIds);
}
};
regenerateIds(cloned);
logger.debug(`Cloned element`, { originalId: element.id, newId: cloned.id });
return cloned;
}
/**
* Move element to a new position
*/
moveElement(
elements: ElementorElement[],
elementId: string,
targetParentId: string | null,
position: number
): boolean {
const { element, parent, index } = this.findElementById(elements, elementId);
if (!element) {
logger.warn(`Element ${elementId} not found for move`);
return false;
}
// Remove from current location
if (parent && parent.elements) {
parent.elements.splice(index, 1);
} else {
elements.splice(index, 1);
}
// Add to new location
if (targetParentId) {
const { element: targetParent } = this.findElementById(elements, targetParentId);
if (targetParent && targetParent.elements) {
targetParent.elements.splice(position, 0, element);
} else {
logger.error(`Target parent ${targetParentId} not found`);
return false;
}
} else {
elements.splice(position, 0, element);
}
logger.info(`Moved element ${elementId} to position ${position}`);
return true;
}
/**
* Reorder elements
*/
reorderElements(
elements: ElementorElement[],
parentId: string | null,
newOrder: string[]
): boolean {
let targetElements: ElementorElement[];
if (parentId) {
const { element: parent } = this.findElementById(elements, parentId);
if (!parent || !parent.elements) {
logger.error(`Parent ${parentId} not found for reorder`);
return false;
}
targetElements = parent.elements;
} else {
targetElements = elements;
}
// Create a map of elements by ID
const elementMap = new Map<string, ElementorElement>();
targetElements.forEach(el => elementMap.set(el.id, el));
// Reorder according to newOrder array
const reordered: ElementorElement[] = [];
for (const id of newOrder) {
const el = elementMap.get(id);
if (el) {
reordered.push(el);
elementMap.delete(id);
}
}
// Add any remaining elements that weren't in newOrder
elementMap.forEach(el => reordered.push(el));
// Replace elements
if (parentId) {
const { element: parent } = this.findElementById(elements, parentId);
if (parent) {
parent.elements = reordered;
}
} else {
elements.length = 0;
elements.push(...reordered);
}
logger.info(`Reordered ${reordered.length} elements`);
return true;
}
/**
* Copy settings from one element to another
*/
copyElementSettings(
elements: ElementorElement[],
sourceId: string,
targetId: string,
settingsKeys?: string[]
): boolean {
const { element: source } = this.findElementById(elements, sourceId);
const { element: target } = this.findElementById(elements, targetId);
if (!source || !target) {
logger.error('Source or target element not found for settings copy');
return false;
}
if (settingsKeys && settingsKeys.length > 0) {
// Copy only specified settings
settingsKeys.forEach(key => {
if (source.settings[key] !== undefined) {
target.settings[key] = JSON.parse(JSON.stringify(source.settings[key]));
}
});
} else {
// Copy all settings
target.settings = JSON.parse(JSON.stringify(source.settings));
}
logger.info(`Copied settings from ${sourceId} to ${targetId}`);
return true;
}
/**
* Get page structure (flattened tree)
*/
getPageStructure(
elements: ElementorElement[],
includeSettings: boolean = false
): any[] {
const structure: any[] = [];
const traverse = (els: ElementorElement[], level: number = 0) => {
for (const el of els) {
const item: any = {
id: el.id,
type: el.elType,
widgetType: el.widgetType || null,
level
};
if (includeSettings) {
item.settings = el.settings;
}
if (el.elements && el.elements.length > 0) {
item.childCount = el.elements.length;
structure.push(item);
traverse(el.elements, level + 1);
} else {
structure.push(item);
}
}
};
traverse(elements);
return structure;
}
}