Files
testdrive-jsui/js/testdrive-jsui.js
tegwick 796c04709a feat: implement JavaScript-first TestDriveJSUI library (v1.0.0)
Completed Phase 1 refactoring to JavaScript-first architecture:

Core Library Implementation:
- Created js/testdrive-jsui.js main library class
- Integrated all existing components (SectionManager, DOMRenderer, DocumentControls)
- Added marked.js integration for markdown rendering
- Implemented event-driven API (on/off/emit)
- Support for edit/view modes and themes
- LocalStorage save/load functionality
- Download as markdown file
- Keyboard shortcuts (Ctrl+S, Escape)
- Auto-save capability (optional)

Examples:
- examples/full-editor.html - Complete demo with all features
- Updated examples/README.md with full documentation

Documentation:
- Updated README.md with JavaScript-first architecture section
- Added complete API reference (constructor, methods, events)
- Updated CLAUDE.md with library quick start and API
- Emphasized JavaScript-first design principles

Architecture:
- JavaScript provides ALL functionality
- Language adapters are optional integration helpers
- Works standalone in browser (no backend required)
- Clean separation: JS (functionality) vs Adapters (integration)

This completes the architectural shift documented in ARCHITECTURE.md
and JS_FIRST_REFACTORING.md Phase 1.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:14:58 +01:00

548 lines
15 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,
...options
};
// Internal state
this.container = null;
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = 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;
}
if (typeof DocumentControls === 'undefined') {
console.error('TestDriveJSUI: DocumentControls not loaded');
return;
}
// Create section manager
this.sectionManager = new SectionManager();
// Create DOM renderer
this.domRenderer = new DOMRenderer(this.sectionManager, this.container);
// Create document controls
this.documentControls = new DocumentControls();
this.documentControls.create();
// Set up event handlers for controls
this.setupControlHandlers();
// 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();
}
});
}
/**
* 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 document controls
if (this.documentControls) {
this.documentControls.destroy();
}
// Clear container
if (this.container) {
this.container.innerHTML = '';
}
// Clear references
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = 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;
}