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
590 lines (474 loc) • 13.9 kB
JavaScript
// MusPE Advanced Component System - React/Vue-like components with unique mobile-first approach
class MusPEComponent {
constructor(props = {}) {
this.props = reactive(props);
this.state = reactive({});
this.refs = {};
this.effects = [];
this.watchers = [];
this.isMounted = false;
this.isDestroyed = false;
this.element = null;
this.vnode = null;
this.prevVNode = null;
// Unique MusPE features
this.touchHandlers = new Map();
this.gestureRecognizers = new Map();
this.animations = new Set();
// Auto-bind methods
this.autoBind();
// Call setup if available (Vue 3-like composition API)
if (this.setup) {
const setupResult = this.setup(this.props);
if (setupResult && typeof setupResult === 'object') {
Object.assign(this.state, setupResult);
}
}
}
// Auto-bind all methods to this instance
autoBind() {
const proto = Object.getPrototypeOf(this);
Object.getOwnPropertyNames(proto).forEach(key => {
if (key !== 'constructor' && typeof this[key] === 'function') {
this[key] = this[key].bind(this);
}
});
}
// Lifecycle: Before mount (like Vue beforeMount)
beforeMount() {
// Override in subclasses
}
// Lifecycle: After mount (like React componentDidMount)
mounted() {
// Override in subclasses
}
// Lifecycle: Before update
beforeUpdate() {
// Override in subclasses
}
// Lifecycle: After update
updated() {
// Override in subclasses
}
// Lifecycle: Before unmount
beforeUnmount() {
// Override in subclasses
}
// Lifecycle: After unmount
unmounted() {
// Override in subclasses
}
// Error handling
errorCaptured(error, instance, info) {
console.error('Component error:', error, info);
return false;
}
// Render method - must be implemented by subclasses
render() {
throw new Error('Component must implement render() method');
}
// Mount component to container
mount(container) {
if (this.isMounted) return this;
if (typeof container === 'string') {
container = document.querySelector(container);
}
if (!container) {
throw new Error('Invalid container for mounting component');
}
try {
this.beforeMount();
// Render virtual DOM
this.vnode = this.render();
// Create real DOM
this.element = vdom.createElement(this.vnode);
// Mount to container
container.appendChild(this.element);
this.isMounted = true;
// Set up reactivity
this.setupReactivity();
// Mobile-specific setup
this.setupMobileFeatures();
this.mounted();
} catch (error) {
this.errorCaptured(error, this, 'mount');
}
return this;
}
// Unmount component
unmount() {
if (!this.isMounted || this.isDestroyed) return this;
try {
this.beforeUnmount();
// Cleanup effects and watchers
this.cleanup();
// Cleanup mobile features
this.cleanupMobileFeatures();
// Remove from DOM
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.isMounted = false;
this.isDestroyed = true;
this.unmounted();
} catch (error) {
this.errorCaptured(error, this, 'unmount');
}
return this;
}
// Update component (re-render)
update() {
if (!this.isMounted || this.isDestroyed) return this;
try {
this.beforeUpdate();
// Store previous vnode
this.prevVNode = this.vnode;
// Render new vnode
this.vnode = this.render();
// Patch DOM
vdom.patch(this.prevVNode, this.vnode, this.element.parentNode);
this.updated();
} catch (error) {
this.errorCaptured(error, this, 'update');
}
return this;
}
// Setup reactivity for automatic updates
setupReactivity() {
// Watch for state changes
this.effects.push(
effect(() => {
if (this.isMounted && !this.isDestroyed) {
this.update();
}
}, { scheduler: this.scheduleUpdate.bind(this) })
);
}
// Schedule update (for batching)
scheduleUpdate() {
if (this.updateScheduled) return;
this.updateScheduled = true;
// Use requestAnimationFrame for smooth updates
requestAnimationFrame(() => {
this.updateScheduled = false;
this.update();
});
}
// Mobile-specific features setup
setupMobileFeatures() {
if (!MusPE.env.isMobile) return;
// Setup touch events
this.setupTouchEvents();
// Setup gesture recognition
this.setupGestureRecognition();
// Setup haptic feedback
this.setupHapticFeedback();
}
// Touch events setup
setupTouchEvents() {
const element = this.element;
if (!element) return;
// Prevent default touch behaviors
element.addEventListener('touchstart', this.handleTouchStart, { passive: false });
element.addEventListener('touchmove', this.handleTouchMove, { passive: false });
element.addEventListener('touchend', this.handleTouchEnd, { passive: false });
}
handleTouchStart(e) {
this.touchStartTime = Date.now();
this.touchStartPos = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
};
// Call onTouchStart if available
if (this.onTouchStart) {
this.onTouchStart(e);
}
}
handleTouchMove(e) {
if (this.onTouchMove) {
this.onTouchMove(e);
}
}
handleTouchEnd(e) {
const touchDuration = Date.now() - this.touchStartTime;
const touchEndPos = {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY
};
// Detect tap
if (touchDuration < 300 && this.getDistance(this.touchStartPos, touchEndPos) < 10) {
if (this.onTap) {
this.onTap(e);
}
}
// Call onTouchEnd if available
if (this.onTouchEnd) {
this.onTouchEnd(e);
}
}
// Gesture recognition setup
setupGestureRecognition() {
// Swipe detection
this.recognizeSwipe();
// Pinch detection
this.recognizePinch();
// Long press detection
this.recognizeLongPress();
}
recognizeSwipe() {
let startX, startY, startTime;
this.element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
});
this.element.addEventListener('touchend', (e) => {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const diffX = endX - startX;
const diffY = endY - startY;
const duration = endTime - startTime;
if (duration < 500 && Math.abs(diffX) > 50) {
const direction = diffX > 0 ? 'right' : 'left';
if (this.onSwipe) {
this.onSwipe(direction, { diffX, diffY, duration });
}
}
});
}
recognizePinch() {
let initialDistance = 0;
this.element.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
initialDistance = this.getDistance(
{ x: e.touches[0].clientX, y: e.touches[0].clientY },
{ x: e.touches[1].clientX, y: e.touches[1].clientY }
);
}
});
this.element.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
const currentDistance = this.getDistance(
{ x: e.touches[0].clientX, y: e.touches[0].clientY },
{ x: e.touches[1].clientX, y: e.touches[1].clientY }
);
const scale = currentDistance / initialDistance;
if (this.onPinch) {
this.onPinch(scale, e);
}
}
});
}
recognizeLongPress() {
let pressTimer;
this.element.addEventListener('touchstart', (e) => {
pressTimer = setTimeout(() => {
if (this.onLongPress) {
this.onLongPress(e);
}
}, 750);
});
this.element.addEventListener('touchend', () => {
clearTimeout(pressTimer);
});
this.element.addEventListener('touchmove', () => {
clearTimeout(pressTimer);
});
}
// Haptic feedback setup
setupHapticFeedback() {
if ('vibrate' in navigator) {
this.haptic = {
light: () => navigator.vibrate(10),
medium: () => navigator.vibrate(20),
heavy: () => navigator.vibrate(50),
success: () => navigator.vibrate([10, 50, 10]),
error: () => navigator.vibrate([50, 50, 50])
};
}
}
// Cleanup mobile features
cleanupMobileFeatures() {
// Stop all animations
this.animations.forEach(animation => {
if (animation.cancel) animation.cancel();
});
this.animations.clear();
// Clear touch handlers
this.touchHandlers.clear();
// Clear gesture recognizers
this.gestureRecognizers.clear();
}
// Utility methods
getDistance(pos1, pos2) {
return Math.sqrt(
Math.pow(pos2.x - pos1.x, 2) + Math.pow(pos2.y - pos1.y, 2)
);
}
// Animation helpers
animate(element, keyframes, options = {}) {
const animation = element.animate(keyframes, {
duration: 300,
easing: 'ease-out',
fill: 'forwards',
...options
});
this.animations.add(animation);
animation.addEventListener('finish', () => {
this.animations.delete(animation);
});
return animation;
}
// Fade animations
fadeIn(duration = 300) {
return this.animate(this.element, [
{ opacity: 0 },
{ opacity: 1 }
], { duration });
}
fadeOut(duration = 300) {
return this.animate(this.element, [
{ opacity: 1 },
{ opacity: 0 }
], { duration });
}
// Slide animations
slideIn(direction = 'left', duration = 300) {
const transforms = {
left: ['translateX(-100%)', 'translateX(0)'],
right: ['translateX(100%)', 'translateX(0)'],
up: ['translateY(-100%)', 'translateY(0)'],
down: ['translateY(100%)', 'translateY(0)']
};
return this.animate(this.element, [
{ transform: transforms[direction][0] },
{ transform: transforms[direction][1] }
], { duration });
}
// State helpers
setState(newState) {
Object.assign(this.state, newState);
}
// Ref helpers
ref(name) {
return (element) => {
this.refs[name] = element;
};
}
// Event helpers
emit(event, data) {
MusPE.emit(event, { component: this, data });
}
// Cleanup effects and watchers
cleanup() {
this.effects.forEach(effect => {
if (effect.stop) effect.stop();
});
this.effects = [];
this.watchers.forEach(watcher => {
if (watcher.stop) watcher.stop();
});
this.watchers = [];
}
// Hook-like methods
useState(initialValue) {
const state = ref(initialValue);
return [
() => state.value,
(newValue) => { state.value = newValue; }
];
}
useEffect(fn, deps = []) {
const effectFn = effect(fn, {
scheduler: (effect) => {
// Check dependencies
if (deps.length === 0 || this.dependenciesChanged(deps)) {
effect.run();
}
}
});
this.effects.push(effectFn);
return effectFn;
}
useWatch(source, callback, options = {}) {
const watcher = watch(source, callback, options);
this.watchers.push(watcher);
return watcher;
}
// Dependency tracking for useEffect
dependenciesChanged(deps) {
if (!this.prevDeps) {
this.prevDeps = deps.slice();
return true;
}
const changed = deps.some((dep, index) => dep !== this.prevDeps[index]);
this.prevDeps = deps.slice();
return changed;
}
}
// Component factory function
function defineComponent(options) {
return class extends MusPEComponent {
constructor(props) {
super(props);
// Apply options
if (options.data && typeof options.data === 'function') {
Object.assign(this.state, options.data.call(this));
}
if (options.methods) {
Object.assign(this, options.methods);
}
if (options.computed) {
for (const [key, computeFn] of Object.entries(options.computed)) {
this[key] = computed(computeFn.bind(this));
}
}
if (options.watch) {
for (const [key, watchFn] of Object.entries(options.watch)) {
this.useWatch(() => this.state[key], watchFn.bind(this));
}
}
}
render() {
if (options.render) {
return options.render.call(this);
}
if (options.template) {
return jsx`${options.template}`;
}
throw new Error('Component must have render method or template');
}
// Apply lifecycle hooks
beforeMount() {
if (options.beforeMount) options.beforeMount.call(this);
}
mounted() {
if (options.mounted) options.mounted.call(this);
}
beforeUpdate() {
if (options.beforeUpdate) options.beforeUpdate.call(this);
}
updated() {
if (options.updated) options.updated.call(this);
}
beforeUnmount() {
if (options.beforeUnmount) options.beforeUnmount.call(this);
}
unmounted() {
if (options.unmounted) options.unmounted.call(this);
}
};
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
MusPEComponent,
defineComponent
};
}
// Make available globally
if (typeof window !== 'undefined') {
window.MusPEComponent = MusPEComponent;
window.defineComponent = defineComponent;
}