Files
testdrive-jsui/js/testdrive-jsui.js
tegwick 177d5cdf69 feat: integrate advanced control panels with feature toggles
Added feature toggle system to TestDriveJSUI library:

Integration:
- Added control configuration (editControl, statusControl, contentsControl, debugControl)
- Added compass positioning configuration (nw, ne, e, se, s, sw, w)
- Implemented initializeAdvancedControls() method
- Updated destroy() to clean up all control panels
- Maintained backward compatibility with simple controls

Configuration:
- controls.editControl (default: true)
- controls.statusControl (default: true)
- controls.contentsControl (default: true)
- controls.debugControl (default: false)
- controls.simpleControls (default: false) - legacy mode

Usage:
new TestDriveJSUI({
    container: '#editor',
    controls: {
        editControl: true,
        statusControl: false,
        contentsControl: true
    }
});

This integrates the existing advanced control system (ControlBase, EditControl,
StatusControl, ContentsControl, DebugControl) into the new library API.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 13:50:31 +01:00

646 lines
19 KiB
JavaScript

/**
* TestDrive-JSUI - JavaScript-First Markdown Editor
*
* Main entry point for the TestDrive-JSUI library.
* This is a pure JavaScript library for interactive markdown editing.
* Language adapters (Python, Ruby, Java) are optional integration helpers.
*
* @version 1.0.0
* @license MIT
*/
/**
* TestDriveJSUI - Main library class
*
* Usage:
* ```javascript
* const editor = new TestDriveJSUI({
* container: '#editor',
* markdown: '# Hello World\n\nEdit me!',
* mode: 'edit',
* theme: 'github',
* autosave: false
* });
* ```
*/
class TestDriveJSUI {
constructor(options = {}) {
// Validate required options
if (!options.container) {
throw new Error('TestDriveJSUI: container option is required');
}
// Configuration
this.config = {
container: options.container,
markdown: options.markdown || '# Welcome\n\nStart editing...',
mode: options.mode || 'edit', // 'edit' or 'view'
theme: options.theme || 'github',
autosave: options.autosave || false,
shortcuts: options.shortcuts !== false, // default true
sections: options.sections !== false, // default true
debug: options.debug || false,
// Advanced control panels (compass-positioned)
controls: {
editControl: options.controls?.editControl !== false, // default true
statusControl: options.controls?.statusControl !== false, // default true
contentsControl: options.controls?.contentsControl !== false, // default true
debugControl: options.controls?.debugControl || false, // default false
// Legacy simple control panel (top-right box)
simpleControls: options.controls?.simpleControls || false // default false
},
// Control positioning (compass: nw, ne, e, se, s, sw, w)
controlPositions: {
editControl: options.controlPositions?.editControl || 'ne',
statusControl: options.controlPositions?.statusControl || 'e',
contentsControl: options.controlPositions?.contentsControl || 'nw',
debugControl: options.controlPositions?.debugControl || 'se'
},
...options
};
// Internal state
this.container = null;
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = null;
// Advanced control panel instances
this.editControl = null;
this.statusControl = null;
this.contentsControl = null;
this.debugControl = null;
this.isInitialized = false;
this.listeners = new Map();
// Initialize
this.init();
}
/**
* Initialize the editor
*/
init() {
if (this.isInitialized) {
console.warn('TestDriveJSUI: Already initialized');
return;
}
// Resolve container
this.container = this.resolveContainer(this.config.container);
if (!this.container) {
throw new Error('TestDriveJSUI: Could not resolve container');
}
// Check for marked.js dependency
if (typeof marked === 'undefined') {
console.warn('TestDriveJSUI: marked.js not loaded. Markdown rendering will be limited.');
}
// Apply theme
this.applyTheme(this.config.theme);
// Initialize components based on mode
if (this.config.mode === 'edit') {
this.initEditMode();
} else {
this.initViewMode();
}
this.isInitialized = true;
// Emit initialized event
this.emit('initialized', {
mode: this.config.mode,
theme: this.config.theme,
sections: this.sectionManager ? this.sectionManager.sections.size : 0
});
}
/**
* Initialize edit mode with full interactivity
*/
initEditMode() {
// Load required components
if (typeof SectionManager === 'undefined') {
console.error('TestDriveJSUI: SectionManager not loaded');
return;
}
if (typeof DOMRenderer === 'undefined') {
console.error('TestDriveJSUI: DOMRenderer not loaded');
return;
}
// Create section manager
this.sectionManager = new SectionManager();
// Create DOM renderer
this.domRenderer = new DOMRenderer(this.sectionManager, this.container);
// Initialize control panels based on configuration
if (this.config.controls.simpleControls) {
// Legacy simple controls (top-right box)
if (typeof DocumentControls === 'undefined') {
console.warn('TestDriveJSUI: DocumentControls not loaded');
} else {
this.documentControls = new DocumentControls();
this.documentControls.create();
this.setupControlHandlers();
}
} else {
// Advanced compass-positioned control panels
this.initializeAdvancedControls();
}
// Parse markdown into sections and render
this.sectionManager.createSectionsFromMarkdown(this.config.markdown);
// Setup keyboard shortcuts if enabled
if (this.config.shortcuts) {
this.setupKeyboardShortcuts();
}
// Setup autosave if enabled
if (this.config.autosave) {
this.setupAutosave();
}
}
/**
* Initialize view mode (read-only rendering)
*/
initViewMode() {
// Clear container
this.container.innerHTML = '';
// Render markdown directly using marked.js or simple renderer
const html = this.renderMarkdown(this.config.markdown);
// Create content wrapper
const contentWrapper = document.createElement('div');
contentWrapper.className = 'testdrive-view-content';
contentWrapper.innerHTML = html;
this.container.appendChild(contentWrapper);
// Apply view mode styling
contentWrapper.style.cssText = `
padding: 20px;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
`;
}
/**
* Setup control panel event handlers
*/
setupControlHandlers() {
this.documentControls.setEventHandlers({
'save-document': () => {
const markdown = this.getMarkdown();
this.emit('save', { markdown });
// If autosave is enabled and we have a save handler, call it
if (this.listeners.has('save')) {
// User has registered a save handler
} else {
// Default: save to localStorage
this.saveToLocalStorage(markdown);
alert('Document saved to browser localStorage');
}
},
'reset-all': () => {
if (confirm('Reset all sections to original content?')) {
this.resetAll();
}
},
'show-status': () => {
const status = this.getStatus();
this.showStatus(status);
},
'toggle-debug': () => {
this.toggleDebug();
}
});
}
/**
* Initialize advanced compass-positioned control panels
*/
initializeAdvancedControls() {
console.log('🎛️ Initializing advanced control panels...');
// ContentsControl (Table of Contents)
if (this.config.controls.contentsControl && typeof ContentsControl !== 'undefined') {
this.contentsControl = new ContentsControl();
this.contentsControl.config.position = this.config.controlPositions.contentsControl;
this.contentsControl.show();
console.log(`✅ ContentsControl initialized (${this.config.controlPositions.contentsControl})`);
}
// StatusControl (Document Statistics)
if (this.config.controls.statusControl && typeof StatusControl !== 'undefined') {
this.statusControl = new StatusControl();
this.statusControl.config.position = this.config.controlPositions.statusControl;
this.statusControl.show();
console.log(`✅ StatusControl initialized (${this.config.controlPositions.statusControl})`);
}
// DebugControl (Debug Logs)
if (this.config.controls.debugControl && typeof DebugControl !== 'undefined') {
this.debugControl = new DebugControl();
this.debugControl.config.position = this.config.controlPositions.debugControl;
this.debugControl.show();
console.log(`✅ DebugControl initialized (${this.config.controlPositions.debugControl})`);
}
// EditControl (Document Actions)
if (this.config.controls.editControl && typeof EditControl !== 'undefined') {
this.editControl = new EditControl();
this.editControl.config.position = this.config.controlPositions.editControl;
this.editControl.show();
console.log(`✅ EditControl initialized (${this.config.controlPositions.editControl})`);
}
// Setup keyboard shortcuts if enabled
if (this.config.shortcuts) {
this.setupKeyboardShortcuts();
}
// Setup autosave if enabled
if (this.config.autosave) {
this.setupAutosave();
}
}
/**
* Setup keyboard shortcuts
*/
setupKeyboardShortcuts() {
document.addEventListener('keydown', (event) => {
// Ctrl+S or Cmd+S - Save
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
this.save();
}
// Escape - Close editor
if (event.key === 'Escape') {
if (this.domRenderer && this.domRenderer.currentFloatingMenu) {
this.domRenderer.hideCurrentEditor();
}
}
});
}
/**
* Setup autosave functionality
*/
setupAutosave() {
// Save every 30 seconds
this.autosaveInterval = setInterval(() => {
const markdown = this.getMarkdown();
this.saveToLocalStorage(markdown);
this.emit('autosave', { markdown });
}, 30000);
}
/**
* Resolve container from selector or element
*/
resolveContainer(container) {
if (typeof container === 'string') {
return document.querySelector(container);
} else if (container instanceof HTMLElement) {
return container;
}
return null;
}
/**
* Render markdown to HTML
*/
renderMarkdown(markdown) {
if (typeof marked !== 'undefined') {
// Use marked.js for full markdown support
marked.setOptions({
gfm: true,
breaks: true,
headerIds: true,
mangle: false,
sanitize: false
});
return marked.parse(markdown);
} else {
// Fallback to simple rendering
return this.simpleMarkdownRender(markdown);
}
}
/**
* Simple markdown renderer (fallback)
*/
simpleMarkdownRender(markdown) {
return markdown
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;" />')
.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" target="_blank">$1</a>')
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
.replace(/`([^`]+)`/gim, '<code>$1</code>')
.replace(/\n/gim, '<br>');
}
/**
* Apply theme to the editor
*/
applyTheme(themeName) {
// Remove existing theme classes
this.container.className = this.container.className
.split(' ')
.filter(c => !c.startsWith('theme-'))
.join(' ');
// Add new theme class
this.container.classList.add(`theme-${themeName}`);
// Apply base styles
this.container.style.cssText = `
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #24292e;
background: #ffffff;
min-height: 300px;
`;
}
/**
* Get current markdown content
*/
getMarkdown() {
if (this.sectionManager) {
return this.sectionManager.getDocumentMarkdown();
}
return this.config.markdown;
}
/**
* Set markdown content
*/
setMarkdown(markdown) {
this.config.markdown = markdown;
if (this.config.mode === 'edit' && this.sectionManager) {
// Re-parse and render sections
this.sectionManager.sections.clear();
this.sectionManager.createSectionsFromMarkdown(markdown);
} else {
// Re-render view mode
this.container.innerHTML = '';
this.initViewMode();
}
this.emit('content-changed', { markdown });
}
/**
* Get editor status
*/
getStatus() {
if (this.sectionManager) {
const sections = this.sectionManager.getAllSections();
const editingSections = sections.filter(s => s.isEditing());
const modifiedSections = sections.filter(s => s.hasChanges());
return {
mode: this.config.mode,
totalSections: sections.length,
editingSections: editingSections.length,
modifiedSections: modifiedSections.length,
wordCount: this.getWordCount(),
characterCount: this.getMarkdown().length
};
}
return {
mode: this.config.mode,
wordCount: this.getWordCount(),
characterCount: this.config.markdown.length
};
}
/**
* Get word count
*/
getWordCount() {
const text = this.getMarkdown();
return text.split(/\s+/).filter(word => word.length > 0).length;
}
/**
* Show status dialog
*/
showStatus(status) {
const message = [
`Mode: ${status.mode}`,
`Total Sections: ${status.totalSections || 0}`,
`Editing: ${status.editingSections || 0}`,
`Modified: ${status.modifiedSections || 0}`,
`Words: ${status.wordCount}`,
`Characters: ${status.characterCount}`
].join('\n');
alert(message);
}
/**
* Save current content
*/
save() {
const markdown = this.getMarkdown();
this.emit('save', { markdown });
// Default: save to localStorage
this.saveToLocalStorage(markdown);
}
/**
* Save to browser localStorage
*/
saveToLocalStorage(markdown) {
try {
localStorage.setItem('testdrive-jsui-content', markdown);
localStorage.setItem('testdrive-jsui-timestamp', new Date().toISOString());
} catch (e) {
console.error('Failed to save to localStorage:', e);
}
}
/**
* Load from browser localStorage
*/
loadFromLocalStorage() {
try {
const markdown = localStorage.getItem('testdrive-jsui-content');
if (markdown) {
this.setMarkdown(markdown);
return true;
}
} catch (e) {
console.error('Failed to load from localStorage:', e);
}
return false;
}
/**
* Download markdown as file
*/
download(filename = 'document.md') {
const markdown = this.getMarkdown();
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.emit('download', { filename, markdown });
}
/**
* Reset all sections to original content
*/
resetAll() {
if (this.sectionManager) {
const sections = this.sectionManager.getAllSections();
sections.forEach(section => {
this.sectionManager.resetSection(section.id);
});
}
this.emit('reset');
}
/**
* Toggle debug mode
*/
toggleDebug() {
this.config.debug = !this.config.debug;
const debugContainer = document.getElementById('debug-messages-container');
if (debugContainer) {
debugContainer.style.display = this.config.debug ? 'block' : 'none';
}
this.emit('debug-toggled', { enabled: this.config.debug });
}
/**
* Event listener system
*/
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this;
}
/**
* Remove event listener
*/
off(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
return this;
}
/**
* Emit event
*/
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => {
try {
callback(data);
} catch (e) {
console.error(`Error in event listener for '${event}':`, e);
}
});
}
}
/**
* Destroy the editor and clean up
*/
destroy() {
// Clear autosave interval
if (this.autosaveInterval) {
clearInterval(this.autosaveInterval);
}
// Destroy simple document controls
if (this.documentControls) {
this.documentControls.destroy();
}
// Destroy advanced control panels
if (this.editControl) {
this.editControl.destroy();
}
if (this.statusControl) {
this.statusControl.destroy();
}
if (this.contentsControl) {
this.contentsControl.destroy();
}
if (this.debugControl) {
this.debugControl.destroy();
}
// Clear container
if (this.container) {
this.container.innerHTML = '';
}
// Clear references
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = null;
this.editControl = null;
this.statusControl = null;
this.contentsControl = null;
this.debugControl = null;
this.listeners.clear();
this.isInitialized = false;
this.emit('destroyed');
}
}
// Export for CommonJS (Node.js, Jest)
if (typeof module !== 'undefined' && module.exports) {
module.exports = { TestDriveJSUI };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.TestDriveJSUI = TestDriveJSUI;
}