peepee
Version:
Visual Programming Language Where You Connect Ports Of One EventEmitter to Ports Of Another EventEmitter
208 lines (172 loc) • 5.33 kB
JavaScript
import { Signal } from "signals";
import { EventEmitter } from "events";
import { Component } from "./Component.js";
/**
* Main Widget Engine class
*/
export class Engine extends EventEmitter {
constructor(svgElement) {
super();
this.svg = svgElement;
this.componentLayer = svgElement.querySelector("#widgets");
this.portLayer = svgElement.querySelector("#ports");
this.defs = svgElement.querySelector("defs");
// Reactive signals
this.containerWidth = new Signal(Component.ContainerWidth);
this.containerHeight = new Signal(Component.ContainerHeight);
this.scale = new Signal(1);
// Plugin system
this.plugins = [];
this.componentPlugins = new Map();
this.isRunning = false;
// Component instances
this.instances = [];
this.registry = new Map();
// Subscribe to container changes
this.containerWidth.subscribe(() => this.layout());
this.containerHeight.subscribe(() => this.layout());
this.setupResizeObserver();
}
registerComponentPlugin(name, plugin) {
this.componentPlugins.set(name, plugin);
if (this.isRunning) {
plugin.start();
}
}
use(plugin) {
plugin.engine = this;
this.plugins.push(plugin);
if (this.isRunning) {
plugin.start();
}
return this;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.plugins.forEach((plugin) => plugin.start());
this.componentPlugins.forEach((plugin) => plugin.start());
return this;
}
stop() {
if (!this.isRunning) return;
this.isRunning = false;
this.plugins.forEach((plugin) => plugin.stop());
this.componentPlugins.forEach((plugin) => plugin.stop());
return this;
}
append(xmlString) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
// Check for parser errors
const parseError = xmlDoc.querySelector("parsererror");
if (parseError) {
throw new Error("XML Parse Error: " + parseError.textContent);
}
const rootElement = xmlDoc.documentElement;
//console.log("Parsing root element:", rootElement.nodeName);
const component = this.createComponentFromXML(rootElement);
this.instances.push(component);
component.render(this, this.componentLayer);
this.layout();
return component;
} catch (error) {
console.error("Error in append:", error);
throw error;
}
}
createComponentFromXML(xmlElement) {
const componentName = xmlElement.nodeName;
//console.log("Creating component:", componentName);
const plugin = this.componentPlugins.get(componentName);
if (!plugin) {
throw new Error(`Unknown component: ${componentName}. Available: ${Array.from(this.componentPlugins.keys()).join(", ")}`);
}
// Extract attributes
const attributes = {};
for (let i = 0; i < xmlElement.attributes.length; i++) {
const attr = xmlElement.attributes[i];
attributes[attr.name] = this.parseValue(attr.value);
}
//console.log("EEE Component attributes:", attributes);
// Create component instance using plugin
const component = plugin.createComponent(attributes, this);
if(component.id) this.registry.set(component.id, component)
// Process children
for (let child of xmlElement.children) {
const childComponent = this.createComponentFromXML(child);
component.addChild(childComponent);
}
return component;
}
layout() {
this.instances.forEach((instance) => {
instance.layout(this.containerWidth.value, this.containerHeight.value);
});
}
setupResizeObserver() {
const container = this.svg.parentElement;
const resizeObserver = new ResizeObserver((entries) => {
this.layout();
});
resizeObserver.observe(container);
}
clear() {
this.instances.forEach((instance) => {
if (instance.element) {
instance.element.remove();
}
});
this.instances = [];
}
query(xpath) {
return this.instances[0];
}
parseValue(value) {
if (typeof value === "string") {
if (value.includes("%")) {
return { value: parseFloat(value), unit: "%" };
}
if (!isNaN(value)) {
return parseFloat(value);
}
}
return value;
}
loadXML(xmlString) {
try {
// this.widgetEngine.clear();
return this.append(xmlString);
} catch (error) {
console.error("Error loading XML:", error);
throw error;
}
}
async loadXMLFile(url) {
try {
const response = await fetch(url);
const xmlContent = await response.text();
return xmlContent;
} catch (error) {
console.error("Error loading XML file:", error);
// Fallback to inline XML if file loading fails
return null;
}
}
loadStyleSheet(url) {
fetch(url)
.then((response) => {
if (!response.ok) throw new Error(`Failed to load CSS: ${response.statusText}`);
return response.text();
})
.then((cssText) => {
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
})
.catch((error) => {
console.error("Error loading stylesheet:", error);
});
}
}