UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

1,030 lines (833 loc) 24.5 kB
// MusPE Core Framework - MVC Architecture Implementation import { MusPE } from './muspe.js'; // ============ MODEL LAYER ============ class MusPEModel extends MusPE { constructor(data = {}, options = {}) { super(); this.options = { validateOnSet: true, trackChanges: true, enableUndo: false, maxHistorySize: 50, deepWatch: true, computedProperties: {}, ...options }; // Model state this._data = {}; this._computed = new Map(); this._validators = new Map(); this._relationships = new Map(); // Change tracking this._changes = new Map(); this._history = []; this._historyIndex = -1; // Validation state this._errors = new Map(); this._isValid = true; // Events this._changeListeners = new Map(); this._validationListeners = new Set(); this.init(data); } init(data) { this.setupComputedProperties(); this.setData(data, false); this.setupValidation(); } // Data management setData(data, notify = true) { const oldData = { ...this._data }; Object.keys(data).forEach(key => { this.set(key, data[key], false); }); if (notify) { this.emit('dataChanged', { oldData, newData: this._data }); } } get(key) { if (this._computed.has(key)) { return this._computed.get(key).call(this); } if (key.includes('.')) { return this.getNestedValue(key); } return this._data[key]; } set(key, value, notify = true) { const oldValue = this.get(key); // Validation if (this.options.validateOnSet && !this.validateField(key, value)) { return false; } // Set value if (key.includes('.')) { this.setNestedValue(key, value); } else { this._data[key] = value; } // Track changes if (this.options.trackChanges) { this._changes.set(key, { oldValue, newValue: value, timestamp: Date.now() }); } // History if (this.options.enableUndo) { this.addToHistory(key, oldValue, value); } // Notify if (notify) { this.notifyChange(key, value, oldValue); this.recomputeDependents(key); } return true; } getNestedValue(path) { return path.split('.').reduce((obj, key) => obj?.[key], this._data); } setNestedValue(path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((obj, key) => { if (!obj[key] || typeof obj[key] !== 'object') { obj[key] = {}; } return obj[key]; }, this._data); target[lastKey] = value; } // Computed properties setupComputedProperties() { Object.entries(this.options.computedProperties).forEach(([key, fn]) => { this.addComputed(key, fn); }); } addComputed(key, fn, dependencies = []) { this._computed.set(key, fn); // Track dependencies for recomputation dependencies.forEach(dep => { if (!this._changeListeners.has(dep)) { this._changeListeners.set(dep, new Set()); } this._changeListeners.get(dep).add(() => { this.recompute(key); }); }); } recompute(key) { const computeFn = this._computed.get(key); if (computeFn) { const newValue = computeFn.call(this); this.emit('computed', { key, value: newValue }); } } recomputeDependents(changedKey) { const listeners = this._changeListeners.get(changedKey); if (listeners) { listeners.forEach(listener => listener()); } } // Validation setupValidation() { this.addValidator('required', (value) => value !== null && value !== undefined && value !== ''); this.addValidator('email', (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)); this.addValidator('minLength', (value, min) => String(value).length >= min); this.addValidator('maxLength', (value, max) => String(value).length <= max); this.addValidator('numeric', (value) => !isNaN(Number(value))); } addValidator(name, validatorFn) { this._validators.set(name, validatorFn); } validateField(key, value) { const rules = this.getValidationRules(key); if (!rules || rules.length === 0) return true; const errors = []; for (const rule of rules) { const { validator, params, message } = this.parseValidationRule(rule); const validatorFn = this._validators.get(validator); if (validatorFn && !validatorFn(value, ...params)) { errors.push(message || `Validation failed for ${key}`); } } if (errors.length > 0) { this._errors.set(key, errors); this._isValid = false; return false; } else { this._errors.delete(key); this._isValid = this._errors.size === 0; return true; } } validateAll() { let isValid = true; Object.keys(this._data).forEach(key => { if (!this.validateField(key, this.get(key))) { isValid = false; } }); return isValid; } getValidationRules(key) { return this.options.validationRules?.[key] || []; } parseValidationRule(rule) { if (typeof rule === 'string') { return { validator: rule, params: [], message: null }; } return { validator: rule.validator || rule.type, params: rule.params || [], message: rule.message }; } // Relationships hasOne(key, modelClass, foreignKey = 'id') { this._relationships.set(key, { type: 'hasOne', modelClass, foreignKey }); } hasMany(key, modelClass, foreignKey) { this._relationships.set(key, { type: 'hasMany', modelClass, foreignKey }); } belongsTo(key, modelClass, foreignKey) { this._relationships.set(key, { type: 'belongsTo', modelClass, foreignKey }); } // History and undo/redo addToHistory(key, oldValue, newValue) { if (this._history.length >= this.options.maxHistorySize) { this._history.shift(); } this._history.push({ key, oldValue, newValue, timestamp: Date.now() }); this._historyIndex = this._history.length - 1; } undo() { if (this._historyIndex > 0) { const change = this._history[this._historyIndex]; this.set(change.key, change.oldValue, false); this._historyIndex--; this.emit('undo', change); return true; } return false; } redo() { if (this._historyIndex < this._history.length - 1) { this._historyIndex++; const change = this._history[this._historyIndex]; this.set(change.key, change.newValue, false); this.emit('redo', change); return true; } return false; } // Event handling notifyChange(key, newValue, oldValue) { this.emit('change', { key, newValue, oldValue }); this.emit(`change:${key}`, { newValue, oldValue }); } // Serialization toJSON() { return { ...this._data }; } fromJSON(json) { this.setData(json); } // Public API getData() { return { ...this._data }; } getErrors() { return Object.fromEntries(this._errors); } isValid() { return this._isValid; } getChanges() { return Object.fromEntries(this._changes); } reset() { this._data = {}; this._changes.clear(); this._errors.clear(); this._history = []; this._historyIndex = -1; this._isValid = true; } } // ============ VIEW LAYER ============ class MusPEView extends MusPE { constructor(options = {}) { super(); this.options = { template: '', container: null, autoRender: true, enableVirtualDOM: true, enableAnimations: true, bindEvents: true, ...options }; // View state this.element = null; this.template = this.options.template; this.bindings = new Map(); this.eventBindings = new Map(); this.childViews = new Set(); // Virtual DOM this.vdom = null; this.previousVDOM = null; // Rendering state this.isRendered = false; this.isDestroyed = false; this.init(); } init() { this.setupTemplate(); this.createElement(); if (this.options.autoRender) { this.render(); } } // Template management setupTemplate() { if (typeof this.template === 'function') { // Template is a function return; } if (this.template.startsWith('#')) { // Template selector const templateElement = document.querySelector(this.template); this.template = templateElement?.innerHTML || ''; } } createElement() { if (this.options.container) { this.element = typeof this.options.container === 'string' ? document.querySelector(this.options.container) : this.options.container; } else { this.element = document.createElement('div'); this.element.className = 'muspe-view'; } } // Rendering render(data = {}) { if (this.isDestroyed) return; const renderData = { ...this.getData(), ...data }; if (this.options.enableVirtualDOM) { this.renderWithVDOM(renderData); } else { this.renderDirect(renderData); } if (this.options.bindEvents) { this.bindEvents(); } this.isRendered = true; this.emit('rendered', { data: renderData }); } renderWithVDOM(data) { const newVDOM = this.createVDOM(data); if (this.previousVDOM) { this.patchDOM(this.element, this.previousVDOM, newVDOM); } else { this.element.innerHTML = this.renderVDOM(newVDOM); } this.previousVDOM = newVDOM; this.vdom = newVDOM; } renderDirect(data) { const html = this.compileTemplate(data); if (this.options.enableAnimations) { this.animateRender(() => { this.element.innerHTML = html; }); } else { this.element.innerHTML = html; } } createVDOM(data) { const template = typeof this.template === 'function' ? this.template(data) : this.compileTemplate(data); return this.parseHTML(template); } renderVDOM(vdom) { if (typeof vdom === 'string') return vdom; if (!vdom || !vdom.tag) return ''; const attrs = Object.entries(vdom.attrs || {}) .map(([key, value]) => `${key}="${value}"`) .join(' '); const children = (vdom.children || []) .map(child => this.renderVDOM(child)) .join(''); return `<${vdom.tag}${attrs ? ' ' + attrs : ''}>${children}</${vdom.tag}>`; } compileTemplate(data) { let html = this.template; // Simple template interpolation html = html.replace(/\{\{([^}]+)\}\}/g, (match, expression) => { try { const value = this.evaluateExpression(expression.trim(), data); return value !== undefined ? String(value) : ''; } catch (error) { console.warn('Template expression error:', error); return match; } }); // Conditional rendering html = html.replace(/\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, condition, content) => { try { const result = this.evaluateExpression(condition.trim(), data); return result ? content : ''; } catch (error) { return ''; } }); // Loop rendering html = html.replace(/\{\{#each\s+([^}]+)\s+as\s+([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, arrayExpr, itemName, content) => { try { const array = this.evaluateExpression(arrayExpr.trim(), data); if (!Array.isArray(array)) return ''; return array.map((item, index) => { const itemData = { ...data, [itemName]: item, index }; return this.compileTemplate.call({ template: content }, itemData); }).join(''); } catch (error) { return ''; } }); return html; } evaluateExpression(expression, data) { // Simple expression evaluator const func = new Function('data', `with(data) { return ${expression}; }`); return func(data); } parseHTML(html) { // Simple HTML parser for VDOM const parser = new DOMParser(); const doc = parser.parseFromString(`<div>${html}</div>`, 'text/html'); return this.domToVDOM(doc.body.firstChild.firstChild); } domToVDOM(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType === Node.ELEMENT_NODE) { const attrs = {}; Array.from(node.attributes).forEach(attr => { attrs[attr.name] = attr.value; }); const children = Array.from(node.childNodes).map(child => this.domToVDOM(child)); return { tag: node.tagName.toLowerCase(), attrs, children }; } return null; } patchDOM(parent, oldVDOM, newVDOM) { // Simple VDOM diffing and patching if (!oldVDOM) { parent.appendChild(this.createElementFromVDOM(newVDOM)); } else if (!newVDOM) { parent.removeChild(parent.firstChild); } else if (this.isVDOMDifferent(oldVDOM, newVDOM)) { parent.replaceChild( this.createElementFromVDOM(newVDOM), parent.firstChild ); } else if (newVDOM.tag) { const element = parent.firstChild; // Update attributes this.updateAttributes(element, oldVDOM.attrs || {}, newVDOM.attrs || {}); // Update children const oldChildren = oldVDOM.children || []; const newChildren = newVDOM.children || []; for (let i = 0; i < Math.max(oldChildren.length, newChildren.length); i++) { this.patchDOM(element, oldChildren[i], newChildren[i]); } } } createElementFromVDOM(vdom) { if (typeof vdom === 'string') { return document.createTextNode(vdom); } const element = document.createElement(vdom.tag); Object.entries(vdom.attrs || {}).forEach(([key, value]) => { element.setAttribute(key, value); }); (vdom.children || []).forEach(child => { element.appendChild(this.createElementFromVDOM(child)); }); return element; } isVDOMDifferent(oldVDOM, newVDOM) { if (typeof oldVDOM !== typeof newVDOM) return true; if (typeof oldVDOM === 'string') return oldVDOM !== newVDOM; if (oldVDOM.tag !== newVDOM.tag) return true; return false; } updateAttributes(element, oldAttrs, newAttrs) { // Remove old attributes Object.keys(oldAttrs).forEach(key => { if (!(key in newAttrs)) { element.removeAttribute(key); } }); // Set new attributes Object.entries(newAttrs).forEach(([key, value]) => { if (oldAttrs[key] !== value) { element.setAttribute(key, value); } }); } // Event binding bindEvents() { if (!this.element) return; // Clear previous bindings this.unbindEvents(); // Bind new events this.element.querySelectorAll('[data-event]').forEach(element => { const eventConfig = element.dataset.event; const [eventName, methodName] = eventConfig.split(':'); if (typeof this[methodName] === 'function') { const handler = this[methodName].bind(this); element.addEventListener(eventName, handler); if (!this.eventBindings.has(element)) { this.eventBindings.set(element, new Map()); } this.eventBindings.get(element).set(eventName, handler); } }); } unbindEvents() { this.eventBindings.forEach((events, element) => { events.forEach((handler, eventName) => { element.removeEventListener(eventName, handler); }); }); this.eventBindings.clear(); } // Animation helpers animateRender(renderFn) { this.element.style.opacity = '0'; this.element.style.transform = 'translateY(10px)'; renderFn(); requestAnimationFrame(() => { this.element.style.transition = 'all 0.3s ease'; this.element.style.opacity = '1'; this.element.style.transform = 'translateY(0)'; }); } // Child view management addChildView(view) { this.childViews.add(view); view.parentView = this; } removeChildView(view) { this.childViews.delete(view); view.parentView = null; } // Lifecycle show() { if (this.element) { this.element.style.display = ''; this.emit('shown'); } } hide() { if (this.element) { this.element.style.display = 'none'; this.emit('hidden'); } } destroy() { this.unbindEvents(); this.childViews.forEach(child => child.destroy()); this.childViews.clear(); if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } this.isDestroyed = true; this.emit('destroyed'); super.destroy(); } // Data binding (to be overridden by subclasses) getData() { return {}; } } // ============ CONTROLLER LAYER ============ class MusPEController extends MusPE { constructor(options = {}) { super(); this.options = { autoInitialize: true, enableRouting: false, enableCaching: false, ...options }; // MVC components this.model = null; this.view = null; this.services = new Map(); // Controller state this.isInitialized = false; this.isDestroyed = false; // Action handlers this.actions = new Map(); this.middlewares = []; // Caching this.cache = this.options.enableCaching ? new Map() : null; if (this.options.autoInitialize) { this.initialize(); } } initialize() { if (this.isInitialized) return; this.setupModel(); this.setupView(); this.setupServices(); this.bindModelToView(); this.registerActions(); this.isInitialized = true; this.emit('initialized'); } // MVC setup setupModel() { // Override in subclasses } setupView() { // Override in subclasses } setupServices() { // Override in subclasses } bindModelToView() { if (!this.model || !this.view) return; // Bind model changes to view updates this.model.on('change', (data) => { this.view.render(this.model.getData()); this.emit('modelChanged', data); }); // Bind view events to controller actions this.view.on('*', (eventName, data) => { this.handleViewEvent(eventName, data); }); } handleViewEvent(eventName, data) { const actionName = `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`; if (typeof this[actionName] === 'function') { this.executeAction(actionName, data); } } // Action system registerAction(name, handler, middleware = []) { this.actions.set(name, { handler, middleware: [...this.middlewares, ...middleware] }); } async executeAction(name, ...args) { const action = this.actions.get(name); if (!action) { console.warn(`Action '${name}' not found`); return; } try { // Execute middlewares for (const middleware of action.middleware) { const result = await middleware(name, args, this); if (result === false) { console.log(`Action '${name}' blocked by middleware`); return; } } // Execute action const result = await action.handler.apply(this, args); this.emit('actionExecuted', { name, args, result }); return result; } catch (error) { console.error(`Action '${name}' failed:`, error); this.emit('actionError', { name, args, error }); throw error; } } addMiddleware(middleware) { this.middlewares.push(middleware); } // Service management addService(name, service) { this.services.set(name, service); // Auto-inject into model and view if (this.model && typeof this.model.setService === 'function') { this.model.setService(name, service); } if (this.view && typeof this.view.setService === 'function') { this.view.setService(name, service); } } getService(name) { return this.services.get(name); } // Data operations async loadData(params = {}) { const cacheKey = JSON.stringify(params); if (this.cache && this.cache.has(cacheKey)) { const data = this.cache.get(cacheKey); this.updateModel(data); return data; } try { const data = await this.fetchData(params); if (this.cache) { this.cache.set(cacheKey, data); } this.updateModel(data); this.emit('dataLoaded', { params, data }); return data; } catch (error) { console.error('Failed to load data:', error); this.emit('dataError', { params, error }); throw error; } } async saveData(data = null) { const saveData = data || (this.model ? this.model.getData() : {}); if (this.model && !this.model.isValid()) { const errors = this.model.getErrors(); this.emit('validationError', { errors }); throw new Error('Validation failed'); } try { const result = await this.persistData(saveData); this.emit('dataSaved', { data: saveData, result }); return result; } catch (error) { console.error('Failed to save data:', error); this.emit('saveError', { data: saveData, error }); throw error; } } updateModel(data) { if (this.model) { if (typeof this.model.setData === 'function') { this.model.setData(data); } else { Object.assign(this.model, data); } } } // Data layer methods (to be overridden) async fetchData(params) { throw new Error('fetchData method must be implemented'); } async persistData(data) { throw new Error('persistData method must be implemented'); } // Navigation and routing navigate(route, params = {}) { if (this.options.enableRouting && window.MusPERouter) { window.MusPERouter.navigate(route, params); } else { console.warn('Routing not enabled or router not available'); } } // Lifecycle refresh() { if (this.view) { this.view.render(this.model ? this.model.getData() : {}); } } reset() { if (this.model && typeof this.model.reset === 'function') { this.model.reset(); } if (this.cache) { this.cache.clear(); } this.refresh(); this.emit('reset'); } destroy() { if (this.model && typeof this.model.destroy === 'function') { this.model.destroy(); } if (this.view && typeof this.view.destroy === 'function') { this.view.destroy(); } this.services.forEach(service => { if (typeof service.destroy === 'function') { service.destroy(); } }); this.isDestroyed = true; this.emit('destroyed'); super.destroy(); } } // ============ MVC FACTORY ============ class MusPEMVCFactory { static createMVC(options = {}) { const { modelClass = MusPEModel, viewClass = MusPEView, controllerClass = MusPEController, modelOptions = {}, viewOptions = {}, controllerOptions = {} } = options; const model = new modelClass(modelOptions); const view = new viewClass(viewOptions); const controller = new controllerClass({ ...controllerOptions, autoInitialize: false }); // Inject dependencies controller.model = model; controller.view = view; // Initialize controller.initialize(); return { model, view, controller }; } static createModel(data, options) { return new MusPEModel(data, options); } static createView(options) { return new MusPEView(options); } static createController(options) { return new MusPEController(options); } } // Export classes export { MusPEModel, MusPEView, MusPEController, MusPEMVCFactory }; // Global registration if (typeof window !== 'undefined') { window.MusPEModel = MusPEModel; window.MusPEView = MusPEView; window.MusPEController = MusPEController; window.MusPEMVCFactory = MusPEMVCFactory; }