Files
markitect-main/capabilities/testdrive-jsui/js/widgets/base/UIWidget.js
tegwick ab3f0db86f feat: consolidate testdrive-jsui to capabilities and implement plugin self-declaration
## Major Changes
- Moved all testdrive-jsui assets from root to capabilities/testdrive-jsui/
- Consolidated directory structure: js/, static/css/, static/images/, static/templates/
- Implemented plugin self-declaration (get_plugin_source_dir, get_asset_paths)
- Removed hardcoded plugin discovery from rendering.py
- Updated all asset paths to be relative to capability root

## Architecture Improvements
- Single source of truth for all testdrive-jsui assets
- Plugin declares its own location (no hardcoded paths)
- Generic plugin discovery using hasattr check
- Clean separation: all JS in .js files, no code mixing
- Standalone capability ready for independent use

## Files Changed
- markitect/plugins/testdrive_jsui.py: Added self-declaration methods
- markitect/plugins/rendering.py: Removed hardcoded discovery
- capabilities/testdrive-jsui/README.md: Added standalone usage documentation
- Moved 17 asset files to consolidated structure
- Deleted obsolete /testdrive-jsui/ root directory

## Testing
- All 17 assets verified and working
- Tested via CLI: markitect md-render --engine testdrive-jsui
- Full document rendering successful

Prepares testdrive-jsui to become a git submodule with proper dependency management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 23:42:54 +01:00

215 lines
5.1 KiB
JavaScript

/**
* UI Widget Base Class
*
* Extends Widget with DOM manipulation and visual functionality.
* Base for all widgets that render UI elements.
*/
import { Widget } from './Widget.js';
export class UIWidget extends Widget {
constructor(options = {}) {
super(options);
// UI properties
this.element = null;
this.isVisible = false;
this.isRendered = false;
this.theme = options.theme || 'default';
this.cssClasses = new Set(['markitect-widget']);
// Animation support
this.animationDuration = options.animationDuration || 300;
this.enableAnimations = options.enableAnimations !== false;
}
/**
* Render the widget to DOM (abstract method)
*/
async render() {
throw new Error('render() method must be implemented by subclass');
}
/**
* Show the widget
*/
async show(options = {}) {
if (!this.isRendered) {
await this.render();
}
if (this.isVisible) {
return this;
}
this.isVisible = true;
if (this.element) {
if (this.enableAnimations && !options.immediate) {
await this.animateShow();
} else {
this.element.style.display = '';
}
}
this.emit('shown');
return this;
}
/**
* Hide the widget
*/
async hide(options = {}) {
if (!this.isVisible) {
return this;
}
this.isVisible = false;
if (this.element) {
if (this.enableAnimations && !options.immediate) {
await this.animateHide();
} else {
this.element.style.display = 'none';
}
}
this.emit('hidden');
return this;
}
/**
* Toggle visibility
*/
async toggle(options = {}) {
return this.isVisible ? this.hide(options) : this.show(options);
}
/**
* Show animation (override for custom animations)
*/
async animateShow() {
if (!this.element) return;
return new Promise(resolve => {
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
this.element.style.opacity = '0';
this.element.style.display = '';
// Force reflow
this.element.offsetHeight;
this.element.style.opacity = '1';
setTimeout(() => {
this.element.style.transition = '';
resolve();
}, this.animationDuration);
});
}
/**
* Hide animation (override for custom animations)
*/
async animateHide() {
if (!this.element) return;
return new Promise(resolve => {
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
this.element.style.opacity = '0';
setTimeout(() => {
this.element.style.display = 'none';
this.element.style.transition = '';
this.element.style.opacity = '';
resolve();
}, this.animationDuration);
});
}
/**
* CSS class management
*/
addClass(className) {
this.cssClasses.add(className);
if (this.element) {
this.element.classList.add(className);
}
return this;
}
removeClass(className) {
this.cssClasses.delete(className);
if (this.element) {
this.element.classList.remove(className);
}
return this;
}
hasClass(className) {
return this.cssClasses.has(className);
}
/**
* Apply theme styling
*/
applyTheme(themeName) {
const oldTheme = this.theme;
this.theme = themeName;
this.removeClass(`theme-${oldTheme}`);
this.addClass(`theme-${themeName}`);
this.emit('theme-changed', { oldTheme, newTheme: themeName });
return this;
}
/**
* Find child element by selector
*/
findElement(selector) {
return this.element ? this.element.querySelector(selector) : null;
}
/**
* Find all child elements by selector
*/
findElements(selector) {
return this.element ? this.element.querySelectorAll(selector) : [];
}
/**
* Override destroy to clean up DOM
*/
async destroy() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
this.isRendered = false;
this.isVisible = false;
await super.destroy();
}
/**
* Apply all CSS classes to element
*/
applyCSSClasses(element = this.element) {
if (element) {
element.className = Array.from(this.cssClasses).join(' ');
}
}
/**
* Default configuration for UI widgets
*/
getDefaultConfig() {
return {
...super.getDefaultConfig(),
theme: 'default',
animationDuration: 300,
enableAnimations: true
};
}
}