UNPKG

peepee

Version:

Visual Programming Language Where You Connect Ports Of One EventEmitter to Ports Of Another EventEmitter

218 lines (184 loc) 7.69 kB
import { Signal } from 'signals'; /** * Base class for all components */ export class Component { // Constants static ElementWidth = 128; static ElementHeight = 32; static FontSize = 10; static ContainerWidth = 320; static ContainerHeight = 200; constructor(attributes = {}, engine) { this.id = attributes.id??'comp-' + Math.random().toString(36).substr(2, 9); this.engine = engine; this.subscriptions = new Set(); this.element = null; this.attributes = {}; //console.info('EEE', attributes) this.installAttributeSignals(attributes); this.children = []; } parseValue(value) { if (typeof value === 'string') { if (value.includes('%')) { return { value: parseFloat(value), unit: '%' }; } if (!isNaN(value)) { return parseFloat(value); } } return value; } addChild(child) { this.children.push(child); child.parent = this; } // onPropertyChange(propertyName) { // // Re-render when reactive properties change // if (this.element && this.element.parentNode) { // this.render(this.element.parentNode); // } // } render(parentComponent, parentElement) { //console.warn('Override in subclasses'); return null; } layout(containerWidth, containerHeight) { // Override in subclasses this.children.forEach(child => { child.layout(containerWidth, containerHeight); }); } query(path=['some-name', 'another-name'], lookupFn=(child,currentValue)=>child.name.value==currentValue) { return path.reduce( (accumulator, currentValue) => accumulator.children.find((child,currentValue)=>lookupFn(child,currentValue)), this.children, ); } installAttributeSignals(rawAttributes, {override}={override:true}){ for(const [attributeName, attributeValue] of Object.entries(rawAttributes)){ //console.info('EEE', {attributeName, attributeValue}) if(this.attributes[attributeName]){ // update value if(override){ this.attributes[attributeName].value = attributeValue; }else{ //console.log('EEE Skipped assigning', attributeName, attributeValue) } }else{ // add missing signal this.attributes[attributeName] = new Signal(attributeValue); } } } // listenToAttributeSignals(input, fn){ // let names; // if(typeof input === 'string'){ // names = [input]; // }else if(Array.isArray(input)){ // names = input; // }else{ // throw new Error('Only array or string is supported as input') // } // for(const name of names){ // if(typeof name !== 'string') throw new Error('Attribute name must be a string.') // const subscription = this.attributes[name].subscribe(()=>fn(...names.map(name=>this.attributes[name].value))); // this.subscriptions.add(subscription); // cleared on stop // } // } // listenToAttributeSignals /** * Listens to attribute signals and executes a callback function when any of the specified attributes change * @param {string|string[]} input - Single attribute name or array of attribute names to listen to * @param {Function} fn - Callback function to execute when attributes change * @throws {TypeError} When input type is invalid or callback is not a function * @throws {Error} When attribute names are invalid or attributes don't exist */ listenToAttributeSignals(input, fn) { // Validate callback function first if (typeof fn !== 'function') { throw new TypeError('Callback must be a function, received: ' + typeof fn); } // Validate and normalize input to array let names; if (typeof input === 'string') { if (input.trim() === '') { throw new Error('Attribute name cannot be empty or whitespace-only'); } names = [input.trim()]; } else if (Array.isArray(input)) { if (input.length === 0) { throw new Error('Attribute names array cannot be empty'); } names = input; } else { throw new TypeError( `Invalid input type: expected string or array, received: ${typeof input}. ` + 'Please provide either a single attribute name as a string or an array of attribute names.' ); } // Validate this.attributes exists if (!this.attributes || typeof this.attributes !== 'object') { throw new Error('Attributes object is not properly initialized. Ensure this.attributes is defined.'); } // Validate this.subscriptions exists if (!this.subscriptions || typeof this.subscriptions.add !== 'function') { throw new Error('Subscriptions manager is not properly initialized. Ensure this.subscriptions has an add() method.'); } // Validate all attribute names and their existence for (const [index, name] of names.entries()) { if (typeof name !== 'string') { throw new TypeError( `Attribute name at index ${index} must be a string, received: ${typeof name}. ` + 'All attribute names must be strings.' ); } if (name.trim() === '') { throw new Error(`Attribute name at index ${index} cannot be empty or whitespace-only`); } const trimmedName = name.trim(); if (!this.attributes.hasOwnProperty(trimmedName)) { throw new Error( `Attribute "${trimmedName}" does not exist. ` + `Available attributes: [${Object.keys(this.attributes).join(', ')}]` ); } if (!this.attributes[trimmedName] || typeof this.attributes[trimmedName].subscribe !== 'function') { throw new Error( `Attribute "${trimmedName}" is not a valid signal object. ` + 'Expected an object with a subscribe() method.' ); } } // Normalize names by trimming whitespace const normalizedNames = names.map(name => name.trim()); // Set up subscriptions for each attribute try { for (const name of normalizedNames) { const subscription = this.attributes[name].subscribe(() => { try { // Get current values and execute callback const currentValues = normalizedNames.map(attrName => { const attr = this.attributes[attrName]; return attr?.value; }); fn(...currentValues); } catch (callbackError) { console.error(`Error in attribute signal callback for [${normalizedNames.join(', ')}]:`, callbackError); // Re-throw to allow proper error handling by the caller throw new Error(`Callback execution failed: ${callbackError.message}`); } }); this.subscriptions.add(subscription); } } catch (subscriptionError) { throw new Error( `Failed to set up attribute subscriptions for [${normalizedNames.join(', ')}]: ${subscriptionError.message}` ); } } setAttributeSignal(element, attributeName, signalName){ signalName ??= attributeName; this.listenToAttributeSignals(signalName, v=>element.setAttribute(attributeName, v)); } }