generated from coulomb/repo-seed
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>
646 lines
19 KiB
JavaScript
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;
|
|
}
|