generated from coulomb/repo-seed
Compare commits
8 Commits
6a6543228a
...
66cbd5c3d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 66cbd5c3d8 | |||
| f2d9d853d5 | |||
| 177d5cdf69 | |||
| 7759f9e427 | |||
| 796c04709a | |||
| b84770e2d3 | |||
| ab4679126e | |||
| 1fe4b6b9fa |
411
ARCHITECTURE.md
Normal file
411
ARCHITECTURE.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# TestDrive-JSUI Architecture
|
||||
|
||||
**Last Updated**: 2025-12-16
|
||||
**Status**: Refactoring to JavaScript-first architecture
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Core Principle
|
||||
|
||||
**TestDrive-JSUI is a JavaScript library, not a Python package.**
|
||||
|
||||
The JavaScript code provides all functionality. Python, Ruby, Java, or other language bindings are **integration adapters** that help serve, configure, and test the JavaScript library - they do not provide core functionality.
|
||||
|
||||
---
|
||||
|
||||
## 📐 Architectural Layers
|
||||
|
||||
### Layer 1: Core JavaScript Library (Functionality)
|
||||
|
||||
**Location**: `/js/`
|
||||
**Purpose**: Complete standalone JavaScript UI framework
|
||||
**Dependencies**: Only `marked.js` (markdown parsing)
|
||||
**Language**: Pure JavaScript (ES6+)
|
||||
|
||||
```
|
||||
js/
|
||||
├── testdrive-jsui.js # Main library bundle (future)
|
||||
├── core/ # Core systems
|
||||
│ ├── debug-system.js # Debug infrastructure
|
||||
│ └── section-manager.js # Document section management
|
||||
├── components/ # UI components
|
||||
│ ├── dom-renderer.js # DOM rendering engine
|
||||
│ ├── debug-panel.js # Debug UI
|
||||
│ └── document-controls.js # Document control UI
|
||||
├── controls/ # Interactive control panels
|
||||
│ ├── control-base.js # Base control class
|
||||
│ ├── edit-control.js # Edit mode controls
|
||||
│ ├── debug-control.js # Debug controls
|
||||
│ ├── status-control.js # Status indicator
|
||||
│ └── contents-control.js # Table of contents
|
||||
├── widgets/ # Reusable UI widgets
|
||||
├── plugins/ # Extension system
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
**Key Characteristics**:
|
||||
- ✅ Works standalone in any browser
|
||||
- ✅ No backend required (can load from file://)
|
||||
- ✅ Self-contained markdown rendering
|
||||
- ✅ All UI logic in JavaScript
|
||||
- ✅ Configuration via JSON
|
||||
|
||||
**Standalone Usage**:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="testdrive-jsui.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- TestDrive-JSUI -->
|
||||
<script src="testdrive-jsui.js"></script>
|
||||
|
||||
<script>
|
||||
// Initialize with configuration
|
||||
const ui = new TestDriveJSUI({
|
||||
container: '#content',
|
||||
mode: 'edit',
|
||||
markdown: '# Hello World\n\nEdit me!',
|
||||
theme: 'github'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Layer 2: Language Adapters (Integration)
|
||||
|
||||
**Purpose**: Integration helpers for different backend languages
|
||||
**Function**: Serve assets, provide testing, backend integration
|
||||
**NOT**: Core functionality implementation
|
||||
|
||||
#### Python Adapter
|
||||
|
||||
**Location**: `/src/testdrive_jsui/` (optional, for Python projects)
|
||||
**Purpose**: Python project integration
|
||||
|
||||
```python
|
||||
# Python is just a helper to serve/test the JS library
|
||||
from testdrive_jsui import TestDriveJSUIAdapter
|
||||
|
||||
adapter = TestDriveJSUIAdapter()
|
||||
|
||||
# Adapter helps with:
|
||||
# 1. Asset serving
|
||||
assets = adapter.get_asset_urls()
|
||||
|
||||
# 2. HTML template generation
|
||||
html = adapter.generate_html(
|
||||
markdown="# Content",
|
||||
mode="edit",
|
||||
config={...}
|
||||
)
|
||||
|
||||
# 3. Testing JavaScript
|
||||
test_result = adapter.run_js_tests()
|
||||
```
|
||||
|
||||
**Python Adapter Responsibilities**:
|
||||
- ✅ Asset path resolution
|
||||
- ✅ HTML template generation
|
||||
- ✅ Configuration serialization (Python dict → JSON)
|
||||
- ✅ JavaScript test runner integration (pytest ↔ Jest)
|
||||
- ✅ Development server (optional)
|
||||
- ❌ **NOT** markdown rendering (JS does this)
|
||||
- ❌ **NOT** UI logic (JS does this)
|
||||
- ❌ **NOT** content transformation (JS does this)
|
||||
|
||||
#### Ruby Adapter (Future)
|
||||
|
||||
```ruby
|
||||
# Similar concept for Ruby/Rails projects
|
||||
adapter = TestDriveJSUI::Adapter.new
|
||||
|
||||
# Generate view helper
|
||||
<%= testdrive_jsui_editor(
|
||||
markdown: @document.content,
|
||||
mode: :edit
|
||||
) %>
|
||||
```
|
||||
|
||||
#### Java Adapter (Future)
|
||||
|
||||
```java
|
||||
// Similar concept for Java/Spring projects
|
||||
TestDriveJSUIAdapter adapter = new TestDriveJSUIAdapter();
|
||||
String html = adapter.generateHtml(markdown, "edit", config);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
### Standalone Mode (No Backend)
|
||||
|
||||
```
|
||||
1. Browser loads HTML
|
||||
2. HTML includes testdrive-jsui.js
|
||||
3. JS initializes with config
|
||||
4. JS fetches/parses markdown → marked.js
|
||||
5. JS renders UI → DOM
|
||||
6. User interacts → JS handles everything
|
||||
7. Save via JS (localStorage, download, etc.)
|
||||
```
|
||||
|
||||
### With Backend Adapter (Python/Ruby/Java)
|
||||
|
||||
```
|
||||
1. Backend serves HTML template
|
||||
2. Backend injects config JSON
|
||||
3. Backend includes JS/CSS assets
|
||||
4. Browser runs JavaScript (same as standalone)
|
||||
5. JS may call backend APIs for save/load
|
||||
```
|
||||
|
||||
**Key Point**: Backend is optional and only for convenience!
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Current vs Target Architecture
|
||||
|
||||
### Current (Hybrid - Needs Refactoring)
|
||||
|
||||
```
|
||||
❌ Python generates HTML structure
|
||||
❌ Python does markdown fallback rendering
|
||||
❌ JavaScript enhances the Python-generated HTML
|
||||
❌ Tight coupling between Python and JavaScript
|
||||
```
|
||||
|
||||
### Target (Clean Separation)
|
||||
|
||||
```
|
||||
✅ JavaScript is the complete UI library
|
||||
✅ JavaScript handles all rendering (markdown → HTML → DOM)
|
||||
✅ Python/Ruby/Java are thin adapters
|
||||
✅ Zero coupling - JS works without any backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Distribution Models
|
||||
|
||||
### Model 1: CDN (Simplest)
|
||||
|
||||
```html
|
||||
<!-- Use directly from CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/testdrive-jsui@1.0.0/dist/testdrive-jsui.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/testdrive-jsui@1.0.0/dist/testdrive-jsui.min.css">
|
||||
```
|
||||
|
||||
### Model 2: npm Package
|
||||
|
||||
```bash
|
||||
npm install testdrive-jsui
|
||||
```
|
||||
|
||||
```javascript
|
||||
import TestDriveJSUI from 'testdrive-jsui';
|
||||
```
|
||||
|
||||
### Model 3: With Language Adapter
|
||||
|
||||
```bash
|
||||
# Python projects
|
||||
pip install testdrive-jsui-python
|
||||
|
||||
# Ruby projects
|
||||
gem install testdrive-jsui
|
||||
|
||||
# Java projects
|
||||
maven: com.testdrive:testdrive-jsui-java
|
||||
```
|
||||
|
||||
**Important**: Language adapters are separate packages that bundle/reference the core JS library.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Architecture
|
||||
|
||||
### JavaScript Tests (Core)
|
||||
|
||||
**Framework**: Jest with jsdom
|
||||
**Location**: `/js/tests/`
|
||||
**What**: Tests the JavaScript library itself
|
||||
|
||||
```javascript
|
||||
describe('TestDriveJSUI', () => {
|
||||
test('renders markdown to HTML', () => {
|
||||
const ui = new TestDriveJSUI({...});
|
||||
expect(ui.render('# Hello')).toContain('<h1>');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Python Integration Tests (Adapter)
|
||||
|
||||
**Framework**: pytest
|
||||
**Location**: `/tests/` (in Python adapter)
|
||||
**What**: Tests Python ↔ JavaScript integration
|
||||
|
||||
```python
|
||||
def test_python_adapter_runs_js_tests():
|
||||
adapter = TestDriveJSUIAdapter()
|
||||
result = adapter.run_js_tests()
|
||||
assert result.success
|
||||
```
|
||||
|
||||
**Key**: Python tests verify the adapter works, not the JS functionality!
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration System
|
||||
|
||||
### JavaScript Configuration (Core)
|
||||
|
||||
```javascript
|
||||
{
|
||||
// Core JS library config
|
||||
"mode": "edit", // 'edit' | 'view'
|
||||
"theme": "github", // CSS theme
|
||||
"markdown": "# Content", // Initial content
|
||||
"autosave": false, // Auto-save behavior
|
||||
"shortcuts": true, // Keyboard shortcuts
|
||||
"sections": true, // Section management
|
||||
"debug": false // Debug mode
|
||||
}
|
||||
```
|
||||
|
||||
### Adapter Configuration (Language-specific)
|
||||
|
||||
```python
|
||||
# Python adapter config (separate from JS)
|
||||
{
|
||||
"asset_base_url": "/static", # Where to serve JS/CSS
|
||||
"development_mode": True, # Dev vs production
|
||||
"template_path": "...", # HTML template location
|
||||
}
|
||||
```
|
||||
|
||||
**Separation**: JS config controls the library, adapter config controls integration.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Migration Path
|
||||
|
||||
### Phase 1: Improve Current Structure ✅ (Starting)
|
||||
|
||||
1. ✅ Document architecture (this file)
|
||||
2. ⏸️ Rename Python code to clearly show it's an adapter
|
||||
3. ⏸️ Ensure JS can work standalone
|
||||
4. ⏸️ Separate JS config from adapter config
|
||||
|
||||
### Phase 2: Pure JavaScript Rendering
|
||||
|
||||
1. ⏸️ Move all markdown rendering to JavaScript (use marked.js)
|
||||
2. ⏸️ Remove Python HTML generation
|
||||
3. ⏸️ Make Python adapter purely serve assets + config
|
||||
4. ⏸️ Create standalone HTML example
|
||||
|
||||
### Phase 3: Proper Distribution
|
||||
|
||||
1. ⏸️ Bundle JavaScript library (`testdrive-jsui.js`)
|
||||
2. ⏸️ Publish to npm
|
||||
3. ⏸️ Publish to CDN
|
||||
4. ⏸️ Separate Python adapter package
|
||||
|
||||
### Phase 4: Additional Adapters
|
||||
|
||||
1. ⏸️ Ruby adapter
|
||||
2. ⏸️ Java adapter
|
||||
3. ⏸️ PHP adapter (if needed)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── javascript/ # Core JS library docs
|
||||
│ ├── getting-started.md # Standalone usage
|
||||
│ ├── api-reference.md # JavaScript API
|
||||
│ ├── configuration.md # JS config options
|
||||
│ └── examples/ # Pure JS examples
|
||||
├── adapters/ # Language adapter docs
|
||||
│ ├── python/
|
||||
│ │ ├── installation.md
|
||||
│ │ ├── django-integration.md
|
||||
│ │ └── flask-integration.md
|
||||
│ ├── ruby/
|
||||
│ │ └── rails-integration.md
|
||||
│ └── java/
|
||||
│ └── spring-integration.md
|
||||
└── architecture/
|
||||
└── ARCHITECTURE.md # This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Principles
|
||||
|
||||
1. **JavaScript First**: All functionality in JavaScript
|
||||
2. **Backend Optional**: Works without any backend
|
||||
3. **Adapters Not Implementations**: Language bindings help integrate, don't implement
|
||||
4. **Clear Boundaries**: JS does UI, backend does serving/storage
|
||||
5. **Language Agnostic**: Core library works with any backend
|
||||
6. **Zero Lock-in**: Use testdrive-jsui without any specific backend framework
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Anti-Patterns to Avoid
|
||||
|
||||
❌ **Don't**: Implement UI logic in Python/Ruby/Java
|
||||
✅ **Do**: Implement UI logic in JavaScript
|
||||
|
||||
❌ **Don't**: Render markdown in backend
|
||||
✅ **Do**: Render markdown in JavaScript (marked.js)
|
||||
|
||||
❌ **Don't**: Generate HTML structure in backend
|
||||
✅ **Do**: Generate HTML structure in JavaScript
|
||||
|
||||
❌ **Don't**: Make JavaScript depend on backend APIs
|
||||
✅ **Do**: Make JavaScript work standalone, optionally call APIs
|
||||
|
||||
❌ **Don't**: Create language-specific forks
|
||||
✅ **Do**: Create thin adapters around single JS library
|
||||
|
||||
---
|
||||
|
||||
## 📈 Success Metrics
|
||||
|
||||
- [ ] Can use testdrive-jsui with pure HTML file (no backend)
|
||||
- [ ] Can use testdrive-jsui with Python/Flask
|
||||
- [ ] Can use testdrive-jsui with Ruby/Rails
|
||||
- [ ] Can use testdrive-jsui with Java/Spring
|
||||
- [ ] JavaScript library < 100KB minified
|
||||
- [ ] Zero runtime dependencies except marked.js
|
||||
- [ ] Works in all modern browsers
|
||||
- [ ] Published to npm and CDN
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
When contributing:
|
||||
- Core functionality → JavaScript
|
||||
- Integration helpers → Language adapters
|
||||
- Keep adapters thin and similar across languages
|
||||
- Test JavaScript with Jest
|
||||
- Test adapters with language-native tools (pytest, rspec, junit)
|
||||
|
||||
---
|
||||
|
||||
**Remember**: TestDrive-JSUI is a JavaScript library with optional backend adapters, not a backend framework with JavaScript assets!
|
||||
115
CLAUDE.md
115
CLAUDE.md
@@ -4,10 +4,70 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Overview
|
||||
|
||||
TestDrive-JSUI is a standalone JavaScript UI rendering engine and testing framework. It's designed as an independent, reusable capability that provides:
|
||||
- Interactive JavaScript UI for markdown editing/viewing
|
||||
- Python-JavaScript test integration bridge
|
||||
- Self-contained plugin architecture with no hardcoded paths
|
||||
**TestDrive-JSUI is a JavaScript-first markdown editor library.**
|
||||
|
||||
This is a pure JavaScript library for interactive markdown editing that works standalone in any browser. Language adapters (Python, Ruby, Java) are **optional integration helpers** - they do NOT provide core functionality.
|
||||
|
||||
**Key Facts**:
|
||||
- **Core:** Complete JavaScript library (`js/testdrive-jsui.js`)
|
||||
- **Dependencies:** Only marked.js for markdown parsing
|
||||
- **Standalone:** Works without any backend (can run from file://)
|
||||
- **Optional Adapters:** Python/Ruby/Java adapters help with asset serving and backend integration
|
||||
- **Architecture:** JavaScript-first design (see `ARCHITECTURE.md`)
|
||||
|
||||
## JavaScript Library
|
||||
|
||||
### Quick Start
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="js/testdrive-jsui.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Hello World\n\nEdit me!',
|
||||
mode: 'edit',
|
||||
theme: 'github'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
**Constructor Options:**
|
||||
- `container` (required): CSS selector or DOM element
|
||||
- `markdown`: Initial content
|
||||
- `mode`: 'edit' or 'view' (default: 'edit')
|
||||
- `theme`: Theme name (default: 'github')
|
||||
- `autosave`: Auto-save enabled (default: false)
|
||||
- `shortcuts`: Keyboard shortcuts (default: true)
|
||||
- `sections`: Section-based editing (default: true)
|
||||
- `debug`: Debug mode (default: false)
|
||||
|
||||
**Methods:**
|
||||
- `getMarkdown()`: Get current content
|
||||
- `setMarkdown(markdown)`: Set content
|
||||
- `getStatus()`: Get editor status
|
||||
- `save()`: Trigger save event
|
||||
- `download(filename)`: Download as .md file
|
||||
- `on(event, callback)`: Listen to events
|
||||
- `destroy()`: Clean up
|
||||
|
||||
**Events:** `initialized`, `save`, `content-changed`, `autosave`, `download`, `reset`, `destroyed`
|
||||
|
||||
### Examples
|
||||
|
||||
See `/examples`:
|
||||
- `standalone.html` - Minimal proof of concept
|
||||
- `full-editor.html` - Complete demo with all features
|
||||
|
||||
## Migration Status
|
||||
|
||||
@@ -88,38 +148,55 @@ make testdrive-jsui-clean
|
||||
### Directory Structure
|
||||
```
|
||||
testdrive-jsui/
|
||||
├── src/testdrive_jsui/ # Python package
|
||||
│ ├── core/ # Core framework components
|
||||
│ ├── components/ # UI component helpers
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── testing/ # Python-JS bridge
|
||||
│ ├── js_test_runner.py # JavaScript test execution
|
||||
│ └── integration.py # Pytest integration
|
||||
├── js/ # JavaScript source (consolidated)
|
||||
├── js/ # JavaScript library (Core functionality)
|
||||
│ ├── testdrive-jsui.js # 🎯 Main library entry point
|
||||
│ ├── core/ # Core JS: debug-system, section-manager
|
||||
│ ├── components/ # UI components: dom-renderer, debug-panel
|
||||
│ ├── components/ # UI components: dom-renderer, debug-panel, document-controls
|
||||
│ ├── controls/ # Control panels: edit, debug, status, contents
|
||||
│ ├── plugins/ # JS plugins
|
||||
│ ├── widgets/ # UI widgets
|
||||
│ ├── utils/ # JS utilities
|
||||
│ ├── tests/ # JavaScript tests
|
||||
│ ├── tests/ # JavaScript tests (Jest)
|
||||
│ ├── config-loader.js # Configuration loader
|
||||
│ ├── main.js # Original main entry point
|
||||
│ ├── main.js # Legacy main entry point
|
||||
│ └── main-updated.js # Refactored main entry
|
||||
├── examples/ # 📚 Working examples
|
||||
│ ├── standalone.html # Minimal proof of concept
|
||||
│ └── full-editor.html # Complete demo
|
||||
├── src/testdrive_jsui/ # Python adapter (optional)
|
||||
│ ├── core/ # Adapter core
|
||||
│ ├── components/ # Adapter helpers
|
||||
│ ├── utils/ # Utilities
|
||||
│ └── testing/ # Python-JS bridge
|
||||
│ ├── js_test_runner.py # JavaScript test execution
|
||||
│ └── integration.py # Pytest integration
|
||||
├── static/ # Static assets
|
||||
│ ├── css/ # Stylesheets (editor, controls, themes)
|
||||
│ ├── images/ # Image assets and icons
|
||||
│ └── templates/ # HTML templates (index.html)
|
||||
└── tests/ # Python tests
|
||||
│ └── templates/ # HTML templates
|
||||
├── tests/ # Python tests
|
||||
├── docs/ # Documentation
|
||||
│ ├── prototypes/ # Archived HTML prototypes
|
||||
│ └── migration/ # Migration records
|
||||
├── ARCHITECTURE.md # JavaScript-first architecture details
|
||||
└── README.md # Main documentation
|
||||
```
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
**JavaScript-First Architecture** (NEW in v1.0.0)
|
||||
- **Core functionality in JavaScript**: All UI logic, rendering, and editing in JS
|
||||
- **Standalone capable**: Works without any backend (can run from file://)
|
||||
- **marked.js only dependency**: For markdown parsing
|
||||
- **Language adapters optional**: Python/Ruby/Java are integration helpers, not functionality providers
|
||||
- **Event-driven API**: Clean event system for integration
|
||||
- **See `ARCHITECTURE.md`** for complete architectural documentation
|
||||
|
||||
**Plugin Independence**
|
||||
- **Self-declaring**: Plugin declares its own location - no hardcoded paths needed
|
||||
- **Single source of truth**: All assets in one capability directory
|
||||
- **Clean boundaries**: JSON config interface between Python and JavaScript
|
||||
- **No code mixing**: JavaScript stays in `.js` files, never embedded in Python strings
|
||||
- **Clean boundaries**: JSON config interface between adapters and JavaScript
|
||||
- **No code mixing**: JavaScript stays in `.js` files, never embedded in strings
|
||||
- Works regardless of installation location
|
||||
|
||||
**Testing Architecture**
|
||||
|
||||
@@ -99,7 +99,6 @@ class MyControl extends ControlBase {
|
||||
#### With TestDrive-JSUI System
|
||||
- **Component Discovery**: Listed by `scripts/list_components.py`
|
||||
- **TDD Testing**: Validated by `tests/test_component_listing.py`
|
||||
- **Legacy Support**: `DocumentControlsLegacy` maintains backward compatibility
|
||||
|
||||
#### With MarkiTect md-render
|
||||
- **Plugin Integration**: Ready for deployment via Makefile targets
|
||||
@@ -116,7 +115,6 @@ class MyControl extends ControlBase {
|
||||
#### Automated Testing
|
||||
- Component listing tests ensure discovery
|
||||
- Integration tests validate interaction patterns
|
||||
- Legacy tests maintain backward compatibility
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
|
||||
402
IMPLEMENTATION_SUMMARY.md
Normal file
402
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Implementation Summary
|
||||
|
||||
**Date**: 2025-12-16
|
||||
**Task**: Documentation and HTML Generation System
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation completes two major tasks:
|
||||
1. **Comprehensive feature documentation** for all TestDrive-JSUI controls
|
||||
2. **HTML generation system** with standalone and external resources modes
|
||||
|
||||
---
|
||||
|
||||
## 1. Feature Documentation
|
||||
|
||||
Created comprehensive documentation for all TestDrive-JSUI features in `docs/features/`:
|
||||
|
||||
### Documentation Files Created
|
||||
|
||||
#### A. `docs/features/README.md`
|
||||
Central hub for all feature documentation with:
|
||||
- Overview of all 4 control panels
|
||||
- Configuration examples for feature toggles
|
||||
- Compass positioning guide (nw, ne, e, se, s, sw, w)
|
||||
- Use case scenarios (writer mode, reader mode, developer mode)
|
||||
- Examples for minimal, full-featured, and custom configurations
|
||||
|
||||
#### B. `docs/features/section-editing.md` (459 lines)
|
||||
Complete guide to section editing:
|
||||
- How section detection works (double newlines, headings, images)
|
||||
- Section states (original, editing, modified, saved)
|
||||
- Visual feedback system
|
||||
- Configuration options
|
||||
- Advanced API usage (section metadata, events, types)
|
||||
- Examples, troubleshooting, and best practices
|
||||
|
||||
#### C. `docs/features/edit-control.md` (459 lines)
|
||||
EditControl panel documentation:
|
||||
- **Document Actions**: save, print, export, reset
|
||||
- **Navigation Tools**: scroll to top/bottom, go to line
|
||||
- **Text Tools**: find & replace, font size, copy link
|
||||
- **Markdown Shortcuts**: bold, italic, headers, links, code
|
||||
- API reference with method signatures
|
||||
- Keyboard shortcuts table
|
||||
- Examples and troubleshooting
|
||||
|
||||
#### D. `docs/features/status-control.md` (456 lines)
|
||||
StatusControl panel documentation:
|
||||
- **Content Statistics**: word count, character count, reading time
|
||||
- **Document Structure**: paragraphs, headings, lists, images, links
|
||||
- **Change Tracking**: visual indicators for increases/decreases
|
||||
- **Actions**: refresh, export (JSON)
|
||||
- Auto-refresh (every 10 seconds)
|
||||
- Performance considerations
|
||||
|
||||
#### E. `docs/features/contents-control.md` (518 lines)
|
||||
ContentsControl panel documentation:
|
||||
- **Automatic heading extraction**: h1-h6 detection
|
||||
- **Hierarchical display**: indentation based on heading level
|
||||
- **Search functionality**: filter headings by text
|
||||
- **Click to navigate**: smooth scrolling + visual highlighting
|
||||
- ID generation for headings
|
||||
- Auto-refresh (checks every 5 seconds)
|
||||
|
||||
#### F. `docs/features/debug-control.md` (519 lines)
|
||||
DebugControl panel documentation:
|
||||
- **Console capture**: logs, errors, warnings, info, debug
|
||||
- **Error tracking**: global errors + unhandled promise rejections
|
||||
- **System information**: browser, viewport, memory, language
|
||||
- **Performance metrics**: page load, DOM ready, session time
|
||||
- **Message filtering**: by error/warn/info/debug levels
|
||||
- **Actions**: clear, export (JSON), pause/resume, test
|
||||
- **Disabled by default** (enable for development)
|
||||
|
||||
#### G. `docs/features/keyboard-shortcuts.md` (584 lines)
|
||||
Complete keyboard shortcuts reference:
|
||||
- **Global shortcuts**: Ctrl+S (save), Escape (close)
|
||||
- **Section editing**: Ctrl+Enter (accept), Escape (cancel), Tab (indent)
|
||||
- **Planned shortcuts**: Ctrl+P, Ctrl+F, Ctrl+B, Ctrl+I (v1.1)
|
||||
- Platform differences (Windows/Linux vs macOS)
|
||||
- Browser conflicts and solutions
|
||||
- Custom shortcuts examples
|
||||
- Accessibility considerations
|
||||
|
||||
#### H. `docs/features/themes.md` (538 lines)
|
||||
Theming system documentation:
|
||||
- **Built-in theme**: GitHub (default)
|
||||
- **CSS variables**: Color palette, spacing, typography
|
||||
- **Theme components**: editor, sections, controls, buttons, debug, TOC
|
||||
- **Creating custom themes**: step-by-step guide
|
||||
- **Example themes**: dark, minimal, high contrast
|
||||
- **Advanced theming**: CSS variables, theme switcher, system detection
|
||||
- **Accessibility**: high contrast, reduced motion, color blind friendly
|
||||
|
||||
---
|
||||
|
||||
## 2. HTML Generation System
|
||||
|
||||
### A. HTML Generator Utility
|
||||
|
||||
**File**: `js/utils/html-generator.js` (468 lines)
|
||||
|
||||
A comprehensive HTML generator class that creates complete TestDrive-JSUI HTML documents.
|
||||
|
||||
#### Features
|
||||
|
||||
- **Dual Mode Support**:
|
||||
- **Standalone mode** (default): All CSS/JS embedded inline
|
||||
- **External resources mode**: References files in `_jsui/` directory
|
||||
|
||||
- **Configurable Options**:
|
||||
```javascript
|
||||
new HTMLGenerator({
|
||||
title: 'My Document',
|
||||
description: 'Interactive editor',
|
||||
markdown: '# Content',
|
||||
mode: 'edit', // or 'view'
|
||||
theme: 'github',
|
||||
standalone: true, // or false
|
||||
controls: {
|
||||
editControl: true,
|
||||
statusControl: true,
|
||||
contentsControl: true,
|
||||
debugControl: false
|
||||
},
|
||||
shortcuts: true,
|
||||
autosave: false,
|
||||
baseUrl: '_jsui', // for external mode
|
||||
includeMarkedJS: true
|
||||
});
|
||||
```
|
||||
|
||||
- **Methods**:
|
||||
- `generate()` - Generate HTML document
|
||||
- `generateStandalone()` - Standalone mode
|
||||
- `generateExternal()` - External resources mode
|
||||
- `saveToFile(filename)` - Save to file (Node.js)
|
||||
- `download(filename)` - Download in browser
|
||||
|
||||
#### Usage Example
|
||||
|
||||
```javascript
|
||||
// Standalone mode
|
||||
const generator = new HTMLGenerator({
|
||||
title: 'My Document',
|
||||
markdown: '# Hello World',
|
||||
standalone: true
|
||||
});
|
||||
const html = generator.generate();
|
||||
generator.download('my-doc.html');
|
||||
|
||||
// External mode
|
||||
const generator2 = new HTMLGenerator({
|
||||
title: 'My Document',
|
||||
markdown: '# Hello World',
|
||||
standalone: false,
|
||||
baseUrl: '_jsui'
|
||||
});
|
||||
const html2 = generator2.generate();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### B. _jsui/ Directory Structure
|
||||
|
||||
**Purpose**: External resources for non-standalone HTML files
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
_jsui/
|
||||
├── README.md (comprehensive guide)
|
||||
├── css/
|
||||
│ ├── base.css → ../../static/css/base.css
|
||||
│ ├── editor.css → ../../static/css/editor.css
|
||||
│ ├── controls.css → ../../static/css/controls.css
|
||||
│ └── themes/
|
||||
│ └── github.css → ../../../static/css/themes/github.css
|
||||
└── js/
|
||||
├── core/
|
||||
│ ├── event-emitter.js → ../../../js/core/event-emitter.js
|
||||
│ ├── section.js → ../../../js/core/section.js
|
||||
│ └── section-manager.js → ../../../js/core/section-manager.js
|
||||
├── components/
|
||||
│ ├── dom-renderer.js → ../../../js/components/dom-renderer.js
|
||||
│ └── floating-menu.js → ../../../js/components/floating-menu.js
|
||||
├── controls/
|
||||
│ ├── control-base.js → ../../../js/controls/control-base.js
|
||||
│ ├── edit-control.js → ../../../js/controls/edit-control.js
|
||||
│ ├── status-control.js → ../../../js/controls/status-control.js
|
||||
│ ├── contents-control.js → ../../../js/controls/contents-control.js
|
||||
│ └── debug-control.js → ../../../js/controls/debug-control.js
|
||||
├── utils/
|
||||
│ └── html-generator.js → ../../../js/utils/html-generator.js
|
||||
└── testdrive-jsui.js → ../../js/testdrive-jsui.js
|
||||
```
|
||||
|
||||
**Note**: All files are **symlinked** to source files for easy maintenance. Changes to source files automatically reflect in `_jsui/`.
|
||||
|
||||
**README**: Created comprehensive 265-line README explaining:
|
||||
- Directory structure and purpose
|
||||
- Usage instructions for both modes
|
||||
- When to use each mode
|
||||
- Deployment guidelines
|
||||
- Customization options
|
||||
- File sizes and performance
|
||||
- Troubleshooting guide
|
||||
|
||||
---
|
||||
|
||||
### C. HTML Generator Demo
|
||||
|
||||
**File**: `examples/html-generator-demo.html` (371 lines)
|
||||
|
||||
Interactive web-based demo showing HTML generation in action.
|
||||
|
||||
#### Features
|
||||
|
||||
- **Configuration form**:
|
||||
- Document title and description
|
||||
- Markdown content (textarea)
|
||||
- Mode selection (edit/view)
|
||||
- Control panel toggles
|
||||
- Options (shortcuts, autosave)
|
||||
|
||||
- **Mode comparison**:
|
||||
- Side-by-side comparison cards
|
||||
- Pros/cons of each mode
|
||||
- Use case recommendations
|
||||
|
||||
- **Generation buttons**:
|
||||
- "Generate Standalone HTML"
|
||||
- "Generate External HTML"
|
||||
|
||||
- **Output display**:
|
||||
- Live preview of generated HTML
|
||||
- Download button (downloads .html file)
|
||||
- Copy to clipboard button
|
||||
|
||||
- **User experience**:
|
||||
- Clean, professional UI
|
||||
- GitHub-inspired styling
|
||||
- Responsive design
|
||||
- Interactive examples
|
||||
|
||||
---
|
||||
|
||||
## When to Use Each Mode
|
||||
|
||||
### Standalone Mode (Default)
|
||||
|
||||
**Best for**:
|
||||
- ✅ Single file portability (email, share, move)
|
||||
- ✅ No web server required (works via file://)
|
||||
- ✅ Offline usage
|
||||
- ✅ Simplicity (one file, no dependencies)
|
||||
- ✅ Quick prototypes and demos
|
||||
|
||||
**Characteristics**:
|
||||
- All CSS/JS embedded inline (~100+ KB per file)
|
||||
- No external dependencies (except marked.js CDN)
|
||||
- No browser caching
|
||||
- Larger file size
|
||||
|
||||
### External Resources Mode
|
||||
|
||||
**Best for**:
|
||||
- ✅ Multiple HTML files (share resources)
|
||||
- ✅ Smaller individual HTML files
|
||||
- ✅ Browser caching for performance
|
||||
- ✅ Easier debugging (separate files)
|
||||
- ✅ Production deployments
|
||||
|
||||
**Characteristics**:
|
||||
- References `_jsui/` directory
|
||||
- Requires web server (no file://)
|
||||
- Browser caches resources (~82 KB total)
|
||||
- Smaller HTML files
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
### Documentation (3,533 lines)
|
||||
- `docs/features/README.md` - 306 lines
|
||||
- `docs/features/section-editing.md` - 319 lines
|
||||
- `docs/features/edit-control.md` - 459 lines
|
||||
- `docs/features/status-control.md` - 456 lines
|
||||
- `docs/features/contents-control.md` - 518 lines
|
||||
- `docs/features/debug-control.md` - 519 lines
|
||||
- `docs/features/keyboard-shortcuts.md` - 584 lines
|
||||
- `docs/features/themes.md` - 538 lines
|
||||
|
||||
### HTML Generation System (1,104 lines)
|
||||
- `js/utils/html-generator.js` - 468 lines
|
||||
- `_jsui/README.md` - 265 lines
|
||||
- `examples/html-generator-demo.html` - 371 lines
|
||||
|
||||
### Total: 4,637 lines of documentation and code
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure Created
|
||||
|
||||
```
|
||||
capabilities/testdrive-jsui/
|
||||
├── docs/
|
||||
│ └── features/
|
||||
│ ├── README.md
|
||||
│ ├── section-editing.md
|
||||
│ ├── edit-control.md
|
||||
│ ├── status-control.md
|
||||
│ ├── contents-control.md
|
||||
│ ├── debug-control.md
|
||||
│ ├── keyboard-shortcuts.md
|
||||
│ └── themes.md
|
||||
├── js/
|
||||
│ └── utils/
|
||||
│ └── html-generator.js
|
||||
├── _jsui/
|
||||
│ ├── README.md
|
||||
│ ├── css/ (symlinked)
|
||||
│ └── js/ (symlinked)
|
||||
├── examples/
|
||||
│ └── html-generator-demo.html
|
||||
└── IMPLEMENTATION_SUMMARY.md (this file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Documentation
|
||||
1. Review each documentation file for accuracy
|
||||
2. Test all code examples provided
|
||||
3. Verify all internal links work
|
||||
4. Check formatting and readability
|
||||
|
||||
### HTML Generator
|
||||
1. Test standalone mode generation
|
||||
2. Test external resources mode generation
|
||||
3. Test with various configurations
|
||||
4. Test download functionality
|
||||
5. Test in different browsers
|
||||
6. Verify generated HTML works correctly
|
||||
|
||||
### _jsui/ Directory
|
||||
1. Verify all symlinks work
|
||||
2. Test serving via web server
|
||||
3. Test external mode HTML with _jsui/ resources
|
||||
4. Check file sizes are reasonable
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Optional)
|
||||
|
||||
### Documentation Improvements
|
||||
- Add API reference documentation
|
||||
- Create more examples for each feature
|
||||
- Add screenshots/GIFs to documentation
|
||||
- Create video tutorials
|
||||
|
||||
### HTML Generator Improvements
|
||||
- **Embed actual JavaScript** in standalone mode (currently uses script tags)
|
||||
- Add minification option
|
||||
- Add bundling option
|
||||
- Support for custom CSS injection
|
||||
- Template system for different layouts
|
||||
|
||||
### _jsui/ Enhancements
|
||||
- Add minified versions (.min.js, .min.css)
|
||||
- Create bundled versions (single file)
|
||||
- Add more themes (dark, solarized, monokai)
|
||||
- CDN deployment option
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation provides:
|
||||
|
||||
1. **Comprehensive documentation** covering all TestDrive-JSUI features
|
||||
2. **Flexible HTML generation** with standalone and external modes
|
||||
3. **Developer-friendly structure** with clear examples and guides
|
||||
4. **Production-ready** system for deploying TestDrive-JSUI
|
||||
|
||||
All documentation follows best practices with:
|
||||
- Clear structure and organization
|
||||
- Comprehensive examples
|
||||
- Troubleshooting guides
|
||||
- API references
|
||||
- Best practices sections
|
||||
|
||||
The HTML generation system enables:
|
||||
- Easy deployment of TestDrive-JSUI
|
||||
- Flexibility in resource management
|
||||
- Optimization for different use cases
|
||||
- Developer productivity
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Completed**: 2025-12-16
|
||||
709
JS_FIRST_REFACTORING.md
Normal file
709
JS_FIRST_REFACTORING.md
Normal file
@@ -0,0 +1,709 @@
|
||||
# JavaScript-First Refactoring Plan
|
||||
|
||||
**Goal**: Transform testdrive-jsui from a Python package with JS assets into a **pure JavaScript library** with optional backend adapters.
|
||||
|
||||
**Date**: 2025-12-16
|
||||
**Status**: Planning
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Vision
|
||||
|
||||
### Before (Current - Hybrid)
|
||||
```
|
||||
Python (TestDriveJSUIEngine)
|
||||
↓ generates HTML template
|
||||
↓ does fallback markdown rendering
|
||||
↓ injects configuration
|
||||
Browser receives: HTML with embedded JS config
|
||||
JavaScript: Enhances the pre-rendered HTML
|
||||
```
|
||||
|
||||
### After (Target - JS-First)
|
||||
```
|
||||
JavaScript Library (testdrive-jsui.js)
|
||||
↓ Standalone, works in any browser
|
||||
↓ Renders markdown using marked.js
|
||||
↓ Creates full UI
|
||||
↓ Handles all interactions
|
||||
|
||||
Optional: Python/Ruby/Java Adapter
|
||||
↓ Just serves JS/CSS files
|
||||
↓ Passes config as JSON
|
||||
↓ Provides testing helpers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 1: Create Standalone JavaScript Bundle
|
||||
|
||||
### Goal
|
||||
Create a single `testdrive-jsui.js` file that works completely standalone in a browser.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1.1: Create Main Library Entry Point
|
||||
|
||||
**Create**: `js/testdrive-jsui.js`
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* TestDrive-JSUI - Standalone JavaScript Markdown Editor
|
||||
*
|
||||
* A complete markdown editing and viewing UI that runs entirely in the browser.
|
||||
* No backend required!
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @requires marked.js (for markdown parsing)
|
||||
*/
|
||||
|
||||
class TestDriveJSUI {
|
||||
/**
|
||||
* Initialize TestDrive-JSUI
|
||||
*
|
||||
* @param {Object} config - Configuration options
|
||||
* @param {string|HTMLElement} config.container - Container element or selector
|
||||
* @param {string} config.mode - 'edit' or 'view'
|
||||
* @param {string} config.markdown - Initial markdown content
|
||||
* @param {string} config.theme - Theme name (default: 'github')
|
||||
* @param {boolean} config.autosave - Enable autosave (default: false)
|
||||
* @param {Object} config.callbacks - Event callbacks
|
||||
*/
|
||||
constructor(config) {
|
||||
this.config = this._validateConfig(config);
|
||||
this.container = this._resolveContainer(config.container);
|
||||
|
||||
// Initialize core systems
|
||||
this.debugSystem = new MarkitectDebugSystem();
|
||||
this.sectionManager = new SectionManager();
|
||||
this.domRenderer = new DOMRenderer(this.sectionManager, this.container);
|
||||
|
||||
// Initialize controls based on mode
|
||||
this._initializeControls();
|
||||
|
||||
// Load initial content
|
||||
if (config.markdown) {
|
||||
this.setContent(config.markdown);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set markdown content
|
||||
* @param {string} markdown - Markdown text
|
||||
*/
|
||||
setContent(markdown) {
|
||||
// Parse markdown to sections
|
||||
const sections = this.sectionManager.createSectionsFromMarkdown(markdown);
|
||||
|
||||
// Render to DOM
|
||||
this.domRenderer.renderAllSections(sections);
|
||||
|
||||
// Emit event
|
||||
this._emit('contentLoaded', { markdown, sections });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current markdown content
|
||||
* @returns {string} Current markdown
|
||||
*/
|
||||
getContent() {
|
||||
return this.sectionManager.getAllSectionsAsMarkdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between edit and view modes
|
||||
* @param {string} mode - 'edit' or 'view'
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (!['edit', 'view'].includes(mode)) {
|
||||
throw new Error(`Invalid mode: ${mode}`);
|
||||
}
|
||||
|
||||
this.config.mode = mode;
|
||||
this._reinitializeControls();
|
||||
this._emit('modeChanged', { mode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current content (triggers callback)
|
||||
*/
|
||||
save() {
|
||||
const markdown = this.getContent();
|
||||
|
||||
if (this.config.callbacks?.onSave) {
|
||||
this.config.callbacks.onSave(markdown);
|
||||
} else if (this.config.autosave) {
|
||||
// Default: save to localStorage
|
||||
localStorage.setItem('testdrive-jsui-content', markdown);
|
||||
}
|
||||
|
||||
this._emit('saved', { markdown });
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the editor and clean up
|
||||
*/
|
||||
destroy() {
|
||||
// Clean up controls
|
||||
this.controls?.forEach(control => control?.destroy?.());
|
||||
|
||||
// Clear container
|
||||
this.container.innerHTML = '';
|
||||
|
||||
this._emit('destroyed');
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
_validateConfig(config) {
|
||||
const defaults = {
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
autosave: false,
|
||||
shortcuts: true,
|
||||
sections: true,
|
||||
debug: false,
|
||||
callbacks: {}
|
||||
};
|
||||
|
||||
return { ...defaults, ...config };
|
||||
}
|
||||
|
||||
_resolveContainer(selector) {
|
||||
if (typeof selector === 'string') {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Container not found: ${selector}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
_initializeControls() {
|
||||
this.controls = [];
|
||||
|
||||
if (this.config.mode === 'edit') {
|
||||
this.controls.push(
|
||||
new EditControl(this),
|
||||
new StatusControl(this),
|
||||
new ContentsControl(this)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
this.controls.push(new DebugControl(this));
|
||||
}
|
||||
|
||||
// Initialize all controls
|
||||
this.controls.forEach(control => control.initialize());
|
||||
}
|
||||
|
||||
_reinitializeControls() {
|
||||
// Destroy old controls
|
||||
this.controls?.forEach(control => control?.destroy?.());
|
||||
|
||||
// Create new controls for current mode
|
||||
this._initializeControls();
|
||||
}
|
||||
|
||||
_emit(event, data) {
|
||||
const callback = this.config.callbacks?.[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`];
|
||||
if (callback) {
|
||||
callback(data);
|
||||
}
|
||||
|
||||
// Also emit as custom event
|
||||
this.container.dispatchEvent(new CustomEvent(`testdrive:${event}`, { detail: data }));
|
||||
}
|
||||
}
|
||||
|
||||
// Expose globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.TestDriveJSUI = TestDriveJSUI;
|
||||
}
|
||||
|
||||
// ES module export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TestDriveJSUI;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2: Create Standalone HTML Example
|
||||
|
||||
**Create**: `examples/standalone.html`
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestDrive-JSUI Standalone Demo</title>
|
||||
|
||||
<!-- TestDrive-JSUI Styles -->
|
||||
<link rel="stylesheet" href="../static/css/editor.css">
|
||||
<link rel="stylesheet" href="../static/css/controls.css">
|
||||
<link rel="stylesheet" href="../static/css/themes/github.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor-container"></div>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- TestDrive-JSUI Core Components -->
|
||||
<script src="../js/core/debug-system.js"></script>
|
||||
<script src="../js/core/section-manager.js"></script>
|
||||
<script src="../js/components/dom-renderer.js"></script>
|
||||
<script src="../js/components/debug-panel.js"></script>
|
||||
<script src="../js/controls/control-base.js"></script>
|
||||
<script src="../js/controls/edit-control.js"></script>
|
||||
<script src="../js/controls/debug-control.js"></script>
|
||||
<script src="../js/controls/status-control.js"></script>
|
||||
<script src="../js/controls/contents-control.js"></script>
|
||||
|
||||
<!-- Main library -->
|
||||
<script src="../js/testdrive-jsui.js"></script>
|
||||
|
||||
<!-- Initialize -->
|
||||
<script>
|
||||
// Example markdown content
|
||||
const sampleMarkdown = `# TestDrive-JSUI Standalone Demo
|
||||
|
||||
## Welcome!
|
||||
|
||||
This is a **completely standalone** markdown editor running entirely in your browser. No backend required!
|
||||
|
||||
### Features
|
||||
|
||||
- ✅ Real-time markdown rendering
|
||||
- ✅ Section-based editing
|
||||
- ✅ Interactive controls
|
||||
- ✅ Keyboard shortcuts
|
||||
- ✅ Debug mode
|
||||
|
||||
### Try It Out
|
||||
|
||||
1. Click the edit button on any section
|
||||
2. Modify the markdown
|
||||
3. Save your changes
|
||||
|
||||
## Code Example
|
||||
|
||||
\`\`\`javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
mode: 'edit',
|
||||
markdown: '# Hello World'
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
That's it! No backend needed.`;
|
||||
|
||||
// Initialize TestDrive-JSUI
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
mode: 'edit',
|
||||
markdown: sampleMarkdown,
|
||||
theme: 'github',
|
||||
autosave: true,
|
||||
debug: true,
|
||||
callbacks: {
|
||||
onSave: (markdown) => {
|
||||
console.log('Content saved:', markdown);
|
||||
alert('Content saved to localStorage!');
|
||||
},
|
||||
onContentLoaded: (data) => {
|
||||
console.log('Content loaded:', data);
|
||||
},
|
||||
onModeChanged: (data) => {
|
||||
console.log('Mode changed to:', data.mode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Expose for debugging
|
||||
window.editor = editor;
|
||||
|
||||
console.log('✅ TestDrive-JSUI initialized!');
|
||||
console.log('Try: editor.getContent(), editor.setMode("view"), editor.save()');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### 1.3: Update Components to Work Standalone
|
||||
|
||||
**Tasks**:
|
||||
- Ensure all JS components work without Python-generated HTML
|
||||
- Remove any assumptions about server-side rendering
|
||||
- Make all components initialize from JavaScript
|
||||
|
||||
#### 1.4: Add Markdown Rendering Helper
|
||||
|
||||
**Update**: `js/utils/markdown-helper.js` (create if needed)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Markdown rendering utilities
|
||||
* Uses marked.js for parsing
|
||||
*/
|
||||
const MarkdownHelper = {
|
||||
/**
|
||||
* Render markdown to HTML
|
||||
* @param {string} markdown - Markdown text
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
render(markdown) {
|
||||
if (!window.marked) {
|
||||
throw new Error('marked.js is required but not loaded');
|
||||
}
|
||||
|
||||
// Configure marked
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
headerIds: true,
|
||||
mangle: false
|
||||
});
|
||||
|
||||
return marked.parse(markdown);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract sections from markdown
|
||||
* @param {string} markdown - Markdown text
|
||||
* @returns {Array} Array of section objects
|
||||
*/
|
||||
extractSections(markdown) {
|
||||
// Split on headers
|
||||
const sections = [];
|
||||
const lines = markdown.split('\n');
|
||||
let currentSection = { level: 0, title: '', content: '' };
|
||||
|
||||
for (const line of lines) {
|
||||
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
|
||||
if (headerMatch) {
|
||||
// Save previous section
|
||||
if (currentSection.content) {
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSection = {
|
||||
level: headerMatch[1].length,
|
||||
title: headerMatch[2],
|
||||
content: line + '\n'
|
||||
};
|
||||
} else {
|
||||
currentSection.content += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add final section
|
||||
if (currentSection.content) {
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MarkdownHelper = MarkdownHelper;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 2: Refactor Python as Thin Adapter
|
||||
|
||||
### Goal
|
||||
Transform Python code from "rendering engine" to "integration adapter".
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 2.1: Rename Python Package Structure
|
||||
|
||||
```
|
||||
src/testdrive_jsui/ # OLD NAME
|
||||
↓
|
||||
src/testdrive_jsui_python/ # NEW NAME (makes it clear it's an adapter)
|
||||
```
|
||||
|
||||
Or keep name but rename classes to show they're adapters:
|
||||
|
||||
```python
|
||||
# Old (implies it does rendering)
|
||||
class TestDriveJSUIEngine
|
||||
|
||||
# New (clear it's just an adapter)
|
||||
class TestDriveJSUIAdapter
|
||||
class PythonIntegrationAdapter
|
||||
```
|
||||
|
||||
#### 2.2: Simplify Python Adapter
|
||||
|
||||
**New**: `src/testdrive_jsui/adapter.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
Python Integration Adapter for TestDrive-JSUI
|
||||
|
||||
This is NOT the rendering engine - it's a helper for Python projects.
|
||||
The JavaScript library does all the actual work.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
import json
|
||||
|
||||
class TestDriveJSUIAdapter:
|
||||
"""
|
||||
Python integration adapter for TestDrive-JSUI JavaScript library.
|
||||
|
||||
Purpose:
|
||||
- Serve JS/CSS assets
|
||||
- Generate HTML templates with config
|
||||
- Provide testing helpers
|
||||
|
||||
Does NOT:
|
||||
- Render markdown (JS does this)
|
||||
- Implement UI (JS does this)
|
||||
- Transform content (JS does this)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.js_library_path = self._find_js_library()
|
||||
|
||||
def _find_js_library(self) -> Path:
|
||||
"""Locate the JavaScript library files."""
|
||||
# Look in capability directory
|
||||
current = Path(__file__).parent
|
||||
while current.parent != current:
|
||||
js_dir = current / "js"
|
||||
if js_dir.exists() and (js_dir / "testdrive-jsui.js").exists():
|
||||
return js_dir
|
||||
current = current.parent
|
||||
raise FileNotFoundError("testdrive-jsui.js not found")
|
||||
|
||||
def get_asset_urls(self, base_url: str = "/static") -> Dict[str, list]:
|
||||
"""
|
||||
Get URLs for JS/CSS assets.
|
||||
|
||||
Helper for web frameworks to include assets in templates.
|
||||
"""
|
||||
return {
|
||||
"css": [
|
||||
f"{base_url}/testdrive-jsui/css/editor.css",
|
||||
f"{base_url}/testdrive-jsui/css/controls.css",
|
||||
f"{base_url}/testdrive-jsui/css/themes/github.css",
|
||||
],
|
||||
"js": [
|
||||
"https://cdn.jsdelivr.net/npm/marked/marked.min.js",
|
||||
f"{base_url}/testdrive-jsui/js/testdrive-jsui.js",
|
||||
]
|
||||
}
|
||||
|
||||
def generate_html(self,
|
||||
markdown: str,
|
||||
mode: str = "edit",
|
||||
config: Optional[Dict] = None) -> str:
|
||||
"""
|
||||
Generate HTML that loads and initializes the JS library.
|
||||
|
||||
This is just a convenience helper - you can write your own template!
|
||||
"""
|
||||
config = config or {}
|
||||
config.update({
|
||||
"markdown": markdown,
|
||||
"mode": mode
|
||||
})
|
||||
|
||||
assets = self.get_asset_urls()
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>TestDrive-JSUI</title>
|
||||
{''.join(f'<link rel="stylesheet" href="{url}">' for url in assets['css'])}
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
|
||||
{''.join(f'<script src="{url}"></script>' for url in assets['js'])}
|
||||
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({json.dumps(config, indent=2)});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def run_js_tests(self):
|
||||
"""Run JavaScript tests (existing functionality - keep this!)."""
|
||||
from .testing import JavaScriptTestRunner
|
||||
runner = JavaScriptTestRunner(self.js_library_path.parent)
|
||||
return runner.run_js_tests()
|
||||
```
|
||||
|
||||
#### 2.3: Update Documentation
|
||||
|
||||
**Update**: README.md to show Python as optional
|
||||
|
||||
```markdown
|
||||
## Installation
|
||||
|
||||
### Option 1: Use JavaScript Directly (No Backend)
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/testdrive-jsui@1.0.0"></script>
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({ ... });
|
||||
</script>
|
||||
```
|
||||
|
||||
### Option 2: With Python Adapter (for Django/Flask)
|
||||
|
||||
```bash
|
||||
pip install testdrive-jsui-python
|
||||
```
|
||||
|
||||
```python
|
||||
from testdrive_jsui import TestDriveJSUIAdapter
|
||||
adapter = TestDriveJSUIAdapter()
|
||||
html = adapter.generate_html(markdown="# Hello")
|
||||
```
|
||||
|
||||
### Option 3: With Ruby Adapter (for Rails) - Coming Soon
|
||||
|
||||
### Option 4: With Java Adapter (for Spring) - Coming Soon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 3: Build and Distribution
|
||||
|
||||
### Goal
|
||||
Package JavaScript library for easy distribution.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 3.1: Create Build Script
|
||||
|
||||
**Create**: `js/build.js`
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Build script for testdrive-jsui
|
||||
* Combines all modules into single distributable file
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Files to concatenate in order
|
||||
const files = [
|
||||
'utils/markdown-helper.js',
|
||||
'core/debug-system.js',
|
||||
'core/section-manager.js',
|
||||
'components/dom-renderer.js',
|
||||
'components/debug-panel.js',
|
||||
'controls/control-base.js',
|
||||
'controls/edit-control.js',
|
||||
'controls/debug-control.js',
|
||||
'controls/status-control.js',
|
||||
'controls/contents-control.js',
|
||||
'testdrive-jsui.js' // Main class last
|
||||
];
|
||||
|
||||
// Read and concatenate
|
||||
let bundle = '// TestDrive-JSUI v1.0.0 - MIT License\n';
|
||||
bundle += '(function() {\n';
|
||||
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(path.join(__dirname, file), 'utf8');
|
||||
bundle += '\n// === ' + file + ' ===\n';
|
||||
bundle += content + '\n';
|
||||
}
|
||||
|
||||
bundle += '})();\n';
|
||||
|
||||
// Write bundle
|
||||
fs.writeFileSync(path.join(__dirname, 'dist', 'testdrive-jsui.js'), bundle);
|
||||
|
||||
console.log('✅ Built: dist/testdrive-jsui.js');
|
||||
```
|
||||
|
||||
#### 3.2: Create package.json for npm
|
||||
|
||||
**Update**: `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "testdrive-jsui",
|
||||
"version": "1.0.0",
|
||||
"description": "Standalone JavaScript markdown editor with interactive UI",
|
||||
"main": "js/dist/testdrive-jsui.js",
|
||||
"types": "js/testdrive-jsui.d.ts",
|
||||
"files": [
|
||||
"js/dist/",
|
||||
"static/css/",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node js/build.js",
|
||||
"test": "jest",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown", "editor", "javascript", "standalone", "ui"
|
||||
],
|
||||
"dependencies": {
|
||||
"marked": "^4.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
### Phase 1 Complete When:
|
||||
- [ ] Can open `standalone.html` in browser (no server needed)
|
||||
- [ ] Can edit markdown completely in JavaScript
|
||||
- [ ] All UI controls work without Python
|
||||
- [ ] marked.js renders markdown in browser
|
||||
|
||||
### Phase 2 Complete When:
|
||||
- [ ] Python adapter is < 200 lines of code
|
||||
- [ ] Python doesn't render markdown
|
||||
- [ ] Python doesn't generate HTML structure
|
||||
- [ ] Can use testdrive-jsui with Flask/Django via adapter
|
||||
|
||||
### Phase 3 Complete When:
|
||||
- [ ] Single `testdrive-jsui.js` file works everywhere
|
||||
- [ ] Published to npm
|
||||
- [ ] Available via CDN
|
||||
- [ ] < 100KB minified + gzipped
|
||||
|
||||
---
|
||||
|
||||
## 📅 Timeline Estimate
|
||||
|
||||
- **Phase 1**: 1-2 days (core JavaScript refactoring)
|
||||
- **Phase 2**: 1 day (simplify Python adapter)
|
||||
- **Phase 3**: 0.5 day (build and distribution)
|
||||
|
||||
**Total**: 2-4 days of focused work
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Priority Order
|
||||
|
||||
1. **Phase 1.2**: Create standalone.html example (proves concept)
|
||||
2. **Phase 1.4**: Markdown rendering in JS
|
||||
3. **Phase 1.1**: Main library class
|
||||
4. **Phase 2.2**: Simplify Python adapter
|
||||
5. **Phase 3**: Build and distribution
|
||||
|
||||
Start with #1 to validate the approach!
|
||||
138
README.md
138
README.md
@@ -1,18 +1,131 @@
|
||||
# TestDrive-JSUI
|
||||
|
||||
A standalone JavaScript UI rendering engine and testing framework. Originally developed for MarkiTect, TestDrive-JSUI is designed as an independent, reusable capability that can be integrated into any Python project.
|
||||
**A JavaScript-first markdown editor and UI library.**
|
||||
|
||||
TestDrive-JSUI is a pure JavaScript library for interactive markdown editing with section-based management. Language adapters (Python, Ruby, Java) are optional integration helpers for serving assets and backend integration - they do not provide core functionality.
|
||||
|
||||
## 🎯 **Purpose**
|
||||
|
||||
TestDrive-JSUI is designed to:
|
||||
|
||||
- **📝 Render markdown with interactive JavaScript UI** for editing and viewing
|
||||
- **🔌 Work as a standalone plugin** or integrate with any Python project
|
||||
- **🔒 Protect existing JavaScript UI functionality** during refactoring and development
|
||||
- **🧪 Integrate JavaScript tests** into Python test suites
|
||||
- **🏗️ Provide a clean architecture** for JavaScript framework development
|
||||
- **📊 Enable comprehensive testing** of JavaScript UI components
|
||||
- **🚀 Support extensibility** for JavaScript framework evolution
|
||||
- **📝 Provide a complete JavaScript library** for interactive markdown editing
|
||||
- **🌐 Work standalone in any browser** without backend requirements
|
||||
- **🔌 Offer optional language adapters** for Python, Ruby, Java, etc.
|
||||
- **✂️ Enable section-based editing** with independent section management
|
||||
- **🎨 Support multiple modes** (edit/view) and themes
|
||||
- **💾 Handle content persistence** (localStorage, download, custom backends)
|
||||
- **🧪 Include comprehensive testing** for JavaScript and integrations
|
||||
- **🚀 Support extensibility** through events and configuration
|
||||
|
||||
## 🏛️ **Architecture: JavaScript-First Design**
|
||||
|
||||
TestDrive-JSUI follows a **JavaScript-first architecture**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Core: JavaScript Library │
|
||||
│ - All functionality in JS │
|
||||
│ - Works standalone in browser │
|
||||
│ - No backend required │
|
||||
│ - Uses marked.js for markdown │
|
||||
└─────────────────────────────────────┘
|
||||
↑ ↑ ↑
|
||||
│ │ │
|
||||
┌──────────┴──┐ ┌───┴────┐ ┌──┴──────┐
|
||||
│ Python │ │ Ruby │ │ Java │
|
||||
│ Adapter │ │ Adapter│ │ Adapter │
|
||||
│ (optional) │ │(future)│ │(future) │
|
||||
└─────────────┘ └────────┘ └─────────┘
|
||||
```
|
||||
|
||||
**Key Principle:** JavaScript provides ALL functionality. Language adapters only help with:
|
||||
- Asset serving
|
||||
- Configuration injection
|
||||
- Backend integration (storage, auth, etc.)
|
||||
- Testing infrastructure
|
||||
|
||||
### Quick Start: JavaScript Library
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="testdrive-jsui.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Hello World\n\nEdit me!',
|
||||
mode: 'edit',
|
||||
theme: 'github'
|
||||
});
|
||||
|
||||
// Listen to events
|
||||
editor.on('save', (data) => {
|
||||
console.log('Saved:', data.markdown);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### API Reference
|
||||
|
||||
#### Constructor Options
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor', // CSS selector or DOM element (required)
|
||||
markdown: '# Content', // Initial markdown content
|
||||
mode: 'edit', // 'edit' or 'view' (default: 'edit')
|
||||
theme: 'github', // Theme name (default: 'github')
|
||||
autosave: false, // Auto-save to localStorage (default: false)
|
||||
shortcuts: true, // Keyboard shortcuts (default: true)
|
||||
sections: true, // Section-based editing (default: true)
|
||||
debug: false // Debug mode (default: false)
|
||||
})
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `getMarkdown()` | Get current document content |
|
||||
| `setMarkdown(markdown)` | Set document content |
|
||||
| `getStatus()` | Get editor status (sections, words, etc.) |
|
||||
| `save()` | Trigger save event |
|
||||
| `download(filename)` | Download as markdown file |
|
||||
| `loadFromLocalStorage()` | Load from browser storage |
|
||||
| `saveToLocalStorage()` | Save to browser storage |
|
||||
| `resetAll()` | Reset all sections to original content |
|
||||
| `destroy()` | Clean up and destroy editor |
|
||||
| `on(event, callback)` | Listen to events |
|
||||
| `off(event, callback)` | Remove event listener |
|
||||
|
||||
#### Events
|
||||
|
||||
| Event | Data | Description |
|
||||
|-------|------|-------------|
|
||||
| `initialized` | `{mode, theme, sections}` | Editor finished initializing |
|
||||
| `save` | `{markdown}` | Save triggered |
|
||||
| `content-changed` | `{markdown}` | Content modified |
|
||||
| `autosave` | `{markdown}` | Auto-save occurred |
|
||||
| `download` | `{filename, markdown}` | Document downloaded |
|
||||
| `reset` | `{}` | Document reset |
|
||||
| `destroyed` | `{}` | Editor destroyed |
|
||||
|
||||
### Examples
|
||||
|
||||
See the `/examples` directory for:
|
||||
- **`standalone.html`** - Minimal proof of concept (works from file://)
|
||||
- **`full-editor.html`** - Complete demo with all features
|
||||
|
||||
## 🐍 **Python Adapter (Optional)**
|
||||
|
||||
For Python projects, TestDrive-JSUI provides an **optional adapter** for integration:
|
||||
|
||||
## 🏗️ **Architecture**
|
||||
|
||||
@@ -459,6 +572,9 @@ MIT License - See main MarkiTect project for details.
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2025-11-09*
|
||||
*Status: Phase 1 Implementation*
|
||||
*Next: Copy JavaScript files and create integration tests*
|
||||
**Version:** 1.0.0
|
||||
**Status:** Full JavaScript Library Implementation Complete ✅
|
||||
**Architecture:** JavaScript-First with Optional Language Adapters
|
||||
**Last Updated:** 2025-12-16
|
||||
|
||||
See `/examples` for working demos and `ARCHITECTURE.md` for detailed design documentation.
|
||||
291
_jsui/README.md
Normal file
291
_jsui/README.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# TestDrive-JSUI External Resources Directory
|
||||
|
||||
This directory contains external CSS and JavaScript files used when generating HTML in **external resources mode**.
|
||||
|
||||
## Purpose
|
||||
|
||||
TestDrive-JSUI HTML generator supports two modes:
|
||||
|
||||
1. **Standalone mode** (default): All CSS/JS embedded inline in HTML file
|
||||
2. **External resources mode**: HTML references external files from this `_jsui/` directory
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
_jsui/
|
||||
├── README.md (this file)
|
||||
├── css/
|
||||
│ ├── base.css # Base styles
|
||||
│ ├── editor.css # Editor styles
|
||||
│ ├── controls.css # Control panel styles
|
||||
│ └── themes/
|
||||
│ └── github.css # GitHub theme
|
||||
└── js/
|
||||
├── core/
|
||||
│ ├── event-emitter.js # Event system
|
||||
│ ├── section.js # Section class
|
||||
│ └── section-manager.js # Section management
|
||||
├── components/
|
||||
│ ├── dom-renderer.js # DOM rendering
|
||||
│ └── floating-menu.js # Floating editor menu
|
||||
├── controls/
|
||||
│ ├── control-base.js # Base control class
|
||||
│ ├── edit-control.js # Edit control panel
|
||||
│ ├── status-control.js # Status control panel
|
||||
│ ├── contents-control.js # Contents control panel
|
||||
│ └── debug-control.js # Debug control panel
|
||||
├── utils/
|
||||
│ └── html-generator.js # HTML generation utility
|
||||
└── testdrive-jsui.js # Main library
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### External Resources Mode
|
||||
|
||||
Generate HTML that references files in this directory:
|
||||
|
||||
```javascript
|
||||
const generator = new HTMLGenerator({
|
||||
title: 'My Document',
|
||||
markdown: '# Hello World',
|
||||
standalone: false, // Use external resources
|
||||
baseUrl: '_jsui' // Path to this directory
|
||||
});
|
||||
|
||||
const html = generator.generate();
|
||||
```
|
||||
|
||||
**Generated HTML includes**:
|
||||
```html
|
||||
<link rel="stylesheet" href="_jsui/css/base.css">
|
||||
<link rel="stylesheet" href="_jsui/css/editor.css">
|
||||
<link rel="stylesheet" href="_jsui/css/controls.css">
|
||||
<link rel="stylesheet" href="_jsui/css/themes/github.css">
|
||||
|
||||
<script src="_jsui/js/core/event-emitter.js"></script>
|
||||
<script src="_jsui/js/core/section.js"></script>
|
||||
<!-- ... more scripts ... -->
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Generate HTML with all resources embedded:
|
||||
|
||||
```javascript
|
||||
const generator = new HTMLGenerator({
|
||||
title: 'My Document',
|
||||
markdown: '# Hello World',
|
||||
standalone: true // Embed all resources (default)
|
||||
});
|
||||
|
||||
const html = generator.generate();
|
||||
```
|
||||
|
||||
**Generated HTML includes**:
|
||||
```html
|
||||
<style>
|
||||
/* All CSS embedded here */
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// All JavaScript embedded here
|
||||
</script>
|
||||
```
|
||||
|
||||
## When to Use Each Mode
|
||||
|
||||
### Use External Resources Mode When:
|
||||
- ✅ Generating multiple HTML files (share resources)
|
||||
- ✅ Want smaller individual HTML files
|
||||
- ✅ Need browser caching for performance
|
||||
- ✅ Easier to debug (separate files)
|
||||
- ✅ Want to update resources without regenerating HTML
|
||||
|
||||
### Use Standalone Mode When:
|
||||
- ✅ Single file portability required
|
||||
- ✅ Email HTML file to someone
|
||||
- ✅ No web server available
|
||||
- ✅ Simplicity (one file, no dependencies)
|
||||
- ✅ Offline usage (no external dependencies)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Copy to Project
|
||||
|
||||
Copy this directory to your project:
|
||||
|
||||
```bash
|
||||
cp -r capabilities/testdrive-jsui/_jsui /path/to/your/project/
|
||||
```
|
||||
|
||||
### Serve with Web Server
|
||||
|
||||
Ensure your web server can serve files from `_jsui/`:
|
||||
|
||||
```bash
|
||||
# Python
|
||||
python3 -m http.server 8080
|
||||
|
||||
# Node.js
|
||||
npx http-server -p 8080
|
||||
|
||||
# PHP
|
||||
php -S localhost:8080
|
||||
```
|
||||
|
||||
Access your HTML: `http://localhost:8080/your-document.html`
|
||||
|
||||
## Customization
|
||||
|
||||
### Add Custom Theme
|
||||
|
||||
1. Create theme CSS file:
|
||||
```bash
|
||||
touch _jsui/css/themes/custom-theme.css
|
||||
```
|
||||
|
||||
2. Define theme styles:
|
||||
```css
|
||||
:root {
|
||||
--custom-primary: #8b5cf6;
|
||||
--custom-border: #e5e7eb;
|
||||
/* ... more variables ... */
|
||||
}
|
||||
|
||||
.theme-custom-theme .markitect-section {
|
||||
/* ... theme styles ... */
|
||||
}
|
||||
```
|
||||
|
||||
3. Generate HTML with custom theme:
|
||||
```javascript
|
||||
const generator = new HTMLGenerator({
|
||||
theme: 'custom-theme',
|
||||
standalone: false
|
||||
});
|
||||
```
|
||||
|
||||
### Modify Styles
|
||||
|
||||
Edit CSS files in `_jsui/css/` to customize appearance.
|
||||
|
||||
**Note**: These are symlinks to source files in `static/css/`. Changes affect source.
|
||||
|
||||
### Extend Functionality
|
||||
|
||||
Add custom JavaScript:
|
||||
|
||||
1. Create file: `_jsui/js/custom/my-extension.js`
|
||||
2. Reference in generated HTML:
|
||||
```html
|
||||
<script src="_jsui/js/custom/my-extension.js"></script>
|
||||
```
|
||||
|
||||
## File Sizes
|
||||
|
||||
Approximate sizes (unminified):
|
||||
|
||||
| File | Size | Description |
|
||||
|------|------|-------------|
|
||||
| `css/base.css` | ~2 KB | Base styles |
|
||||
| `css/editor.css` | ~3 KB | Editor styles |
|
||||
| `css/controls.css` | ~5 KB | Control panels |
|
||||
| `css/themes/github.css` | ~2 KB | GitHub theme |
|
||||
| `js/testdrive-jsui.js` | ~20 KB | Main library |
|
||||
| `js/core/*.js` | ~15 KB | Core functionality |
|
||||
| `js/components/*.js` | ~10 KB | UI components |
|
||||
| `js/controls/*.js` | ~25 KB | Control panels |
|
||||
|
||||
**Total**: ~82 KB unminified, ~30 KB minified + gzipped
|
||||
|
||||
## Performance
|
||||
|
||||
### External Resources Mode
|
||||
- **First load**: Download all files (~82 KB)
|
||||
- **Subsequent loads**: Browser cache (0 KB)
|
||||
- **Multiple pages**: Share cached resources
|
||||
|
||||
### Standalone Mode
|
||||
- **Every load**: Full HTML file (~100+ KB per file)
|
||||
- **No caching**: Resources embedded, not cached separately
|
||||
- **Single page**: More efficient for one-time use
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Resources Not Loading
|
||||
|
||||
**Problem**: CSS/JS files return 404 errors
|
||||
|
||||
**Solutions**:
|
||||
1. Verify `_jsui/` directory exists in same directory as HTML
|
||||
2. Check file paths in HTML match directory structure
|
||||
3. Ensure web server serves static files
|
||||
4. Use browser DevTools Network tab to debug
|
||||
|
||||
### Relative Path Issues
|
||||
|
||||
**Problem**: Resources work locally but not on server
|
||||
|
||||
**Solution**: Use correct base URL:
|
||||
|
||||
```javascript
|
||||
// For local file://
|
||||
baseUrl: '_jsui'
|
||||
|
||||
// For web server
|
||||
baseUrl: '/path/to/_jsui'
|
||||
|
||||
// For CDN
|
||||
baseUrl: 'https://cdn.example.com/_jsui'
|
||||
```
|
||||
|
||||
### Symlink Issues (Development)
|
||||
|
||||
**Problem**: Symlinks don't work on Windows
|
||||
|
||||
**Solution**: Copy files instead of symlinking:
|
||||
|
||||
```bash
|
||||
# Copy instead of symlink
|
||||
cp -r static/css/* _jsui/css/
|
||||
cp -r js/* _jsui/js/
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Updating Resources
|
||||
|
||||
Files in `_jsui/` are symlinked to source files:
|
||||
- Edit source files in `js/` or `static/css/`
|
||||
- Changes automatically reflect in `_jsui/` (via symlinks)
|
||||
- No need to manually copy files
|
||||
|
||||
### Building for Production
|
||||
|
||||
1. **Minify CSS**:
|
||||
```bash
|
||||
npx cssnano _jsui/css/*.css
|
||||
```
|
||||
|
||||
2. **Minify JavaScript**:
|
||||
```bash
|
||||
npx terser _jsui/js/**/*.js -c -m
|
||||
```
|
||||
|
||||
3. **Bundle** (optional):
|
||||
```bash
|
||||
# Combine all JS into single file
|
||||
cat _jsui/js/**/*.js > _jsui/js/testdrive-jsui.bundle.js
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [HTML Generator API](../docs/api/html-generator.md) (coming soon)
|
||||
- [Themes](../docs/features/themes.md)
|
||||
- [Customization](../docs/customization.md) (coming soon)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
1
_jsui/css/base.css
Symbolic link
1
_jsui/css/base.css
Symbolic link
@@ -0,0 +1 @@
|
||||
../../static/css/base.css
|
||||
1
_jsui/css/controls.css
Symbolic link
1
_jsui/css/controls.css
Symbolic link
@@ -0,0 +1 @@
|
||||
../../static/css/controls.css
|
||||
1
_jsui/css/editor.css
Symbolic link
1
_jsui/css/editor.css
Symbolic link
@@ -0,0 +1 @@
|
||||
../../static/css/editor.css
|
||||
1
_jsui/css/themes/github.css
Symbolic link
1
_jsui/css/themes/github.css
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../static/css/themes/github.css
|
||||
1
_jsui/js/components/dom-renderer.js
Symbolic link
1
_jsui/js/components/dom-renderer.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/components/dom-renderer.js
|
||||
1
_jsui/js/components/floating-menu.js
Symbolic link
1
_jsui/js/components/floating-menu.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/components/floating-menu.js
|
||||
1
_jsui/js/controls/contents-control.js
Symbolic link
1
_jsui/js/controls/contents-control.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/controls/contents-control.js
|
||||
1
_jsui/js/controls/control-base.js
Symbolic link
1
_jsui/js/controls/control-base.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/controls/control-base.js
|
||||
1
_jsui/js/controls/debug-control.js
Symbolic link
1
_jsui/js/controls/debug-control.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/controls/debug-control.js
|
||||
1
_jsui/js/controls/edit-control.js
Symbolic link
1
_jsui/js/controls/edit-control.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/controls/edit-control.js
|
||||
1
_jsui/js/controls/status-control.js
Symbolic link
1
_jsui/js/controls/status-control.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/controls/status-control.js
|
||||
1
_jsui/js/core/event-emitter.js
Symbolic link
1
_jsui/js/core/event-emitter.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/core/event-emitter.js
|
||||
1
_jsui/js/core/section-manager.js
Symbolic link
1
_jsui/js/core/section-manager.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/core/section-manager.js
|
||||
1
_jsui/js/core/section.js
Symbolic link
1
_jsui/js/core/section.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/core/section.js
|
||||
1
_jsui/js/testdrive-jsui.js
Symbolic link
1
_jsui/js/testdrive-jsui.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../js/testdrive-jsui.js
|
||||
1
_jsui/js/utils/html-generator.js
Symbolic link
1
_jsui/js/utils/html-generator.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../js/utils/html-generator.js
|
||||
305
docs/features/README.md
Normal file
305
docs/features/README.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# TestDrive-JSUI Features
|
||||
|
||||
**Complete feature documentation for TestDrive-JSUI**
|
||||
|
||||
TestDrive-JSUI provides a modular system of interactive controls that enhance markdown editing and viewing. Each control can be enabled/disabled independently and positioned using compass directions.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Feature Documentation
|
||||
|
||||
### Core Features
|
||||
- **[Section Editing](section-editing.md)** - Click any section to edit it independently
|
||||
- **[Keyboard Shortcuts](keyboard-shortcuts.md)** - Quick actions via keyboard
|
||||
- **[Themes](themes.md)** - Visual theming system
|
||||
|
||||
### Control Panels
|
||||
|
||||
#### [Edit Control](edit-control.md) 📝
|
||||
**Position**: Northeast (ne)
|
||||
**Purpose**: Document actions and editing tools
|
||||
|
||||
Features:
|
||||
- Print, save, export document
|
||||
- Navigation helpers (scroll to top/bottom, go to line)
|
||||
- Text formatting tools
|
||||
- Find & replace
|
||||
- Font size adjustment
|
||||
- Markdown shortcuts
|
||||
|
||||
**[→ Full Documentation](edit-control.md)**
|
||||
|
||||
---
|
||||
|
||||
#### [Status Control](status-control.md) 📊
|
||||
**Position**: East (e)
|
||||
**Purpose**: Real-time document statistics
|
||||
|
||||
Features:
|
||||
- Word and character count
|
||||
- Reading time estimation
|
||||
- Document structure analysis (headings, paragraphs, lists)
|
||||
- Change tracking
|
||||
- Content complexity metrics
|
||||
|
||||
**[→ Full Documentation](status-control.md)**
|
||||
|
||||
---
|
||||
|
||||
#### [Contents Control](contents-control.md) 📋
|
||||
**Position**: Northwest (nw)
|
||||
**Purpose**: Table of contents navigation
|
||||
|
||||
Features:
|
||||
- Automatic heading extraction
|
||||
- Hierarchical display with indentation
|
||||
- Click to navigate with smooth scrolling
|
||||
- Search within headings
|
||||
- Real-time updates
|
||||
|
||||
**[→ Full Documentation](contents-control.md)**
|
||||
|
||||
---
|
||||
|
||||
####[Debug Control](debug-control.md) 🐛
|
||||
**Position**: Southeast (se)
|
||||
**Purpose**: Development and debugging tools
|
||||
|
||||
Features:
|
||||
- Real-time debug logs
|
||||
- Error tracking
|
||||
- Performance monitoring
|
||||
- Component state inspection
|
||||
- Event tracking
|
||||
|
||||
**[→ Full Documentation](debug-control.md)**
|
||||
|
||||
---
|
||||
|
||||
## 🎛️ Configuration
|
||||
|
||||
### Enabling/Disabling Controls
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# My Document',
|
||||
mode: 'edit',
|
||||
|
||||
// Control configuration
|
||||
controls: {
|
||||
editControl: true, // Document actions (default: true)
|
||||
statusControl: true, // Statistics (default: true)
|
||||
contentsControl: true, // Table of contents (default: true)
|
||||
debugControl: false // Debug logs (default: false)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Positioning
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
|
||||
// Custom compass positions
|
||||
controlPositions: {
|
||||
editControl: 'ne', // Northeast (default)
|
||||
statusControl: 'e', // East (default)
|
||||
contentsControl: 'nw', // Northwest (default)
|
||||
debugControl: 'se' // Southeast (default)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Compass Positions**:
|
||||
- `nw` - Northwest (top-left)
|
||||
- `ne` - Northeast (top-right)
|
||||
- `e` - East (middle-right)
|
||||
- `se` - Southeast (bottom-right)
|
||||
- `s` - South (bottom-center)
|
||||
- `sw` - Southwest (bottom-left)
|
||||
- `w` - West (middle-left)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Control Behavior
|
||||
|
||||
### States
|
||||
|
||||
Each control has two states:
|
||||
|
||||
1. **Collapsed** (40x40px icon button)
|
||||
- Minimal screen space
|
||||
- Shows icon only
|
||||
- Click to expand
|
||||
|
||||
2. **Expanded** (full panel)
|
||||
- Shows full functionality
|
||||
- Draggable by header
|
||||
- Resizable from bottom-left corner
|
||||
- Click close (✕) to collapse
|
||||
|
||||
### Interactions
|
||||
|
||||
- **Click icon**: Expand panel
|
||||
- **Click header**: Toggle content visibility (header-only mode)
|
||||
- **Drag header**: Reposition panel
|
||||
- **Drag resize handle**: Resize panel
|
||||
- **Click close (✕)**: Collapse to icon
|
||||
|
||||
---
|
||||
|
||||
## 📖 Example Configurations
|
||||
|
||||
### Minimal (Section Editing Only)
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: false,
|
||||
statusControl: false,
|
||||
contentsControl: false,
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Writer Mode (Edit + Status)
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true,
|
||||
statusControl: true,
|
||||
contentsControl: false,
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Reader Mode (Contents Only)
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
mode: 'view',
|
||||
controls: {
|
||||
editControl: false,
|
||||
statusControl: false,
|
||||
contentsControl: true,
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Developer Mode (All Controls)
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true,
|
||||
statusControl: true,
|
||||
contentsControl: true,
|
||||
debugControl: true // Enable debug panel
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Auto-save
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
autosave: true // Saves to localStorage every 30 seconds
|
||||
});
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
shortcuts: true // Enable keyboard shortcuts (default: true)
|
||||
});
|
||||
```
|
||||
|
||||
Common shortcuts:
|
||||
- `Ctrl+S` - Save document
|
||||
- `Escape` - Close editor/dialog
|
||||
- `Ctrl+F` - Find & replace
|
||||
- `Ctrl+B` - Bold
|
||||
- `Ctrl+I` - Italic
|
||||
|
||||
### Themes
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'github' // Available: 'github' (more coming soon)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### Documentation Editor
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true, // For saving/exporting
|
||||
statusControl: true, // Track word count
|
||||
contentsControl: true, // Navigate long docs
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Blog Post Editor
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true, // Save drafts
|
||||
statusControl: true, // Monitor length
|
||||
contentsControl: false, // Usually shorter posts
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Code Documentation Viewer
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#viewer',
|
||||
mode: 'view',
|
||||
controls: {
|
||||
editControl: false,
|
||||
statusControl: false,
|
||||
contentsControl: true, // Navigate API docs
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
- Read individual control documentation for detailed features
|
||||
- Check out [examples](../../examples/) for working demos
|
||||
- See [API Reference](../api/) for complete API documentation
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
731
docs/features/contents-control.md
Normal file
731
docs/features/contents-control.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# Contents Control
|
||||
|
||||
**Interactive table of contents navigation** 📋
|
||||
|
||||
The Contents Control provides an automatically generated, hierarchical table of contents for quick document navigation. Click any heading to jump to that section with smooth scrolling and visual highlighting. Perfect for long documents with multiple sections. Positioned in the northwest by default.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Contents Control extracts all headings from your document and displays them in a structured, clickable table of contents. Navigate long documents quickly and understand document structure at a glance.
|
||||
|
||||
### Position
|
||||
- **Default**: Northwest (nw) - top-left corner
|
||||
- **Appearance**: 📋 icon when collapsed
|
||||
- **Title**: "Contents"
|
||||
|
||||
### Key Features
|
||||
|
||||
- 📋 **Automatic extraction** - Finds all headings (h1-h6) in document
|
||||
- 🔍 **Search functionality** - Filter headings by text
|
||||
- 🎯 **Click to navigate** - Smooth scroll to any heading
|
||||
- ✨ **Visual feedback** - Temporary highlight on target heading
|
||||
- 🔄 **Auto-refresh** - Updates when document structure changes
|
||||
- 📐 **Hierarchical display** - Proper indentation for heading levels
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
contentsControl: true // Enable (default: true)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
contentsControl: 'nw' // Northwest (default), or: ne, e, se, s, sw, w
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Automatic Heading Extraction
|
||||
|
||||
The Contents Control automatically scans your document for headings (h1-h6) and builds a table of contents.
|
||||
|
||||
#### How It Works
|
||||
|
||||
**Scanning**:
|
||||
- Queries document for `h1, h2, h3, h4, h5, h6` elements
|
||||
- Extracts heading text and level
|
||||
- Generates unique IDs for navigation (if not present)
|
||||
|
||||
**ID Generation**:
|
||||
- Converts heading text to URL-safe format
|
||||
- Removes special characters
|
||||
- Replaces spaces with hyphens
|
||||
- Ensures uniqueness with counter suffix if needed
|
||||
|
||||
**Example**:
|
||||
```markdown
|
||||
# Getting Started
|
||||
## Installation Steps
|
||||
### Prerequisites
|
||||
```
|
||||
|
||||
Becomes:
|
||||
```javascript
|
||||
[
|
||||
{ id: 'getting-started', text: 'Getting Started', level: 1 },
|
||||
{ id: 'installation-steps', text: 'Installation Steps', level: 2 },
|
||||
{ id: 'prerequisites', text: 'Prerequisites', level: 3 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Hierarchical Display
|
||||
|
||||
Headings are displayed with indentation based on their level.
|
||||
|
||||
#### Indentation Rules
|
||||
|
||||
- **H1**: No indentation (0px)
|
||||
- **H2**: 15px indent
|
||||
- **H3**: 30px indent
|
||||
- **H4**: 45px indent
|
||||
- **H5**: 60px indent
|
||||
- **H6**: 75px indent
|
||||
|
||||
**Formula**: `indent = (level - 1) × 15px`
|
||||
|
||||
#### Visual Indicators
|
||||
|
||||
Each heading link shows:
|
||||
- **Level badge**: `H1`, `H2`, etc. (gray color)
|
||||
- **Heading text**: Blue, underlined on hover
|
||||
- **Hover effect**: Light gray background
|
||||
|
||||
**Example Display**:
|
||||
```
|
||||
H1 Getting Started
|
||||
H2 Installation Steps
|
||||
H3 Prerequisites
|
||||
H3 Download
|
||||
H2 Configuration
|
||||
H3 Basic Setup
|
||||
H3 Advanced Options
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Search Functionality
|
||||
|
||||
Filter headings by text to quickly find sections in large documents.
|
||||
|
||||
#### Usage
|
||||
|
||||
1. Type in the search box at the top of the Contents panel
|
||||
2. Results filter in real-time as you type
|
||||
3. Matching headings remain visible
|
||||
4. Clear search to show all headings
|
||||
|
||||
**Search behavior**:
|
||||
- Case-insensitive
|
||||
- Partial text matching
|
||||
- Searches heading text only (not IDs)
|
||||
- Updates status count: "Found X headings"
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Search: "install"
|
||||
|
||||
Results:
|
||||
H1 Getting Started
|
||||
H2 Installation Steps
|
||||
H2 Uninstall Guide
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Click to Navigate
|
||||
|
||||
Click any heading in the Contents panel to jump to that section.
|
||||
|
||||
#### Navigation Behavior
|
||||
|
||||
**Smooth scrolling**:
|
||||
- Animated scroll to target heading
|
||||
- Position: `block: 'start'` (heading at top)
|
||||
- Duration: Browser default (~500ms)
|
||||
|
||||
**Visual highlight**:
|
||||
- Target heading gets yellow background (`#fff3cd`)
|
||||
- Highlight fades after 1.5 seconds
|
||||
- Smooth transition (300ms ease)
|
||||
|
||||
**Example user flow**:
|
||||
1. User clicks "Installation Steps" in Contents
|
||||
2. Document smoothly scrolls to that heading
|
||||
3. Heading briefly highlights in yellow
|
||||
4. Highlight fades back to normal
|
||||
|
||||
---
|
||||
|
||||
### 5. Refresh Functionality
|
||||
|
||||
Update the table of contents when document structure changes.
|
||||
|
||||
#### Manual Refresh
|
||||
|
||||
**Button**: "🔄 Refresh Contents" at bottom of panel
|
||||
|
||||
**Usage**:
|
||||
- Click to re-scan document for headings
|
||||
- Shows "✅ Updated" confirmation for 1 second
|
||||
- Rebuilds entire table of contents
|
||||
|
||||
**When to use**:
|
||||
- After adding new headings via section editing
|
||||
- After programmatically modifying document
|
||||
- To ensure TOC is up-to-date
|
||||
|
||||
#### Auto-Refresh
|
||||
|
||||
**Automatic detection** every 5 seconds:
|
||||
- Counts current headings in document
|
||||
- Compares to stored count
|
||||
- Refreshes automatically if different
|
||||
|
||||
**Silent updates**:
|
||||
- No visual feedback (unlike manual refresh)
|
||||
- Preserves scroll position
|
||||
- Maintains search query
|
||||
|
||||
**Disable auto-refresh**:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
clearInterval(editor.contentsControl.updateInterval);
|
||||
editor.contentsControl.updateInterval = null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Heading Status
|
||||
|
||||
Shows count of headings found.
|
||||
|
||||
**Display**: "Found X heading(s)" (gray text, centered)
|
||||
|
||||
**Updates**:
|
||||
- After manual refresh
|
||||
- During search filtering
|
||||
- On auto-refresh
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Found 12 headings (all headings)
|
||||
Found 3 headings (filtered by search)
|
||||
No headings found (empty document)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Chapter 1\\n\\n## Section 1.1\\n\\n## Section 1.2',
|
||||
controls: {
|
||||
contentsControl: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position (Bottom-Left)
|
||||
|
||||
```javascript
|
||||
// Move to southwest (bottom-left)
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
contentsControl: 'sw'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Documentation Viewer Setup
|
||||
|
||||
```javascript
|
||||
// Perfect for long documentation
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#viewer',
|
||||
mode: 'view', // Read-only
|
||||
controls: {
|
||||
editControl: false,
|
||||
statusControl: false,
|
||||
contentsControl: true, // Only show TOC
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Navigate to specific heading by ID
|
||||
editor.contentsControl.navigateToHeading('installation-steps');
|
||||
|
||||
// Or find and navigate by text
|
||||
const heading = editor.contentsControl.headings.find(h =>
|
||||
h.text.includes('Installation')
|
||||
);
|
||||
if (heading) {
|
||||
editor.contentsControl.navigateToHeading(heading.id);
|
||||
}
|
||||
```
|
||||
|
||||
### Track Navigation Events
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Monitor which sections users visit
|
||||
const originalNavigate = editor.contentsControl.navigateToHeading.bind(editor.contentsControl);
|
||||
editor.contentsControl.navigateToHeading = function(headingId) {
|
||||
const heading = this.headings.find(h => h.id === headingId);
|
||||
console.log(`User navigated to: ${heading?.text || headingId}`);
|
||||
|
||||
// Analytics
|
||||
fetch('/api/analytics', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event: 'toc_navigation',
|
||||
heading: heading?.text,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
|
||||
return originalNavigate(headingId);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Properties
|
||||
|
||||
Access via `editor.contentsControl`:
|
||||
|
||||
```javascript
|
||||
const contentsCtrl = editor.contentsControl;
|
||||
|
||||
// Array of extracted headings
|
||||
contentsCtrl.headings = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
text: 'Getting Started',
|
||||
level: 1,
|
||||
element: HTMLElement, // Reference to heading element
|
||||
index: 0
|
||||
},
|
||||
// ... more headings
|
||||
];
|
||||
|
||||
// Current search query
|
||||
contentsCtrl.searchQuery = 'install';
|
||||
|
||||
// Last scan timestamp
|
||||
contentsCtrl.lastScanTime = 1702742452123;
|
||||
|
||||
// Auto-refresh interval ID
|
||||
contentsCtrl.updateInterval = 12345;
|
||||
|
||||
// Configuration
|
||||
contentsCtrl.config.position = 'nw'; // Compass position
|
||||
contentsCtrl.config.icon = '📋'; // Panel icon
|
||||
contentsCtrl.config.title = 'Contents'; // Panel title
|
||||
contentsCtrl.isExpanded; // true if expanded
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```javascript
|
||||
const contentsCtrl = editor.contentsControl;
|
||||
|
||||
// Extract headings from document
|
||||
const headings = contentsCtrl.extractHeadings();
|
||||
// Returns: Array of heading objects
|
||||
|
||||
// Filter headings by search query
|
||||
const filtered = contentsCtrl.filterHeadings(headings, 'install');
|
||||
// Returns: Array of matching headings
|
||||
|
||||
// Navigate to heading with smooth scroll
|
||||
const success = contentsCtrl.navigateToHeading('installation-steps');
|
||||
// Returns: true if heading found, false otherwise
|
||||
|
||||
// Handle search input
|
||||
contentsCtrl.handleSearch('configuration');
|
||||
// Updates searchQuery and rebuilds content
|
||||
|
||||
// Refresh contents manually
|
||||
contentsCtrl.refreshContents();
|
||||
// Re-scans document and updates display
|
||||
|
||||
// Generate content HTML
|
||||
const html = contentsCtrl.generateContent();
|
||||
// Returns: HTML string for panel content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Heading ID Format
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Override ID generation
|
||||
const originalExtract = editor.contentsControl.extractHeadings.bind(editor.contentsControl);
|
||||
editor.contentsControl.extractHeadings = function() {
|
||||
const result = originalExtract();
|
||||
|
||||
// Custom ID format: section-1-2-3
|
||||
result.forEach((heading, index) => {
|
||||
heading.id = `section-${index + 1}`;
|
||||
heading.element.id = heading.id;
|
||||
});
|
||||
|
||||
this.headings = result;
|
||||
return result;
|
||||
};
|
||||
```
|
||||
|
||||
### Collapse All Sections Except One
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
function focusOnSection(headingId) {
|
||||
// Navigate to heading
|
||||
editor.contentsControl.navigateToHeading(headingId);
|
||||
|
||||
// Highlight in TOC
|
||||
const tocItems = document.querySelectorAll('.contents-item a');
|
||||
tocItems.forEach(item => {
|
||||
if (item.getAttribute('href') === `#${headingId}`) {
|
||||
item.style.fontWeight = 'bold';
|
||||
item.style.backgroundColor = '#e7f3ff';
|
||||
} else {
|
||||
item.style.fontWeight = 'normal';
|
||||
item.style.backgroundColor = 'transparent';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focusOnSection('installation-steps');
|
||||
```
|
||||
|
||||
### Export Table of Contents
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
function exportTOC() {
|
||||
const toc = editor.contentsControl.headings.map(h => ({
|
||||
level: h.level,
|
||||
text: h.text,
|
||||
id: h.id
|
||||
}));
|
||||
|
||||
// As JSON
|
||||
const json = JSON.stringify(toc, null, 2);
|
||||
|
||||
// As Markdown
|
||||
const markdown = toc.map(h => {
|
||||
const indent = ' '.repeat(h.level - 1);
|
||||
return `${indent}- [${h.text}](#${h.id})`;
|
||||
}).join('\\n');
|
||||
|
||||
console.log('JSON:', json);
|
||||
console.log('Markdown:', markdown);
|
||||
|
||||
return { json, markdown };
|
||||
}
|
||||
|
||||
exportTOC();
|
||||
```
|
||||
|
||||
### Custom Search Logic
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Override filter to search by level too
|
||||
const originalFilter = editor.contentsControl.filterHeadings.bind(editor.contentsControl);
|
||||
editor.contentsControl.filterHeadings = function(headings, query) {
|
||||
if (!query || query.trim() === '') {
|
||||
return headings;
|
||||
}
|
||||
|
||||
// Support "h1", "h2" prefixes
|
||||
const levelMatch = query.match(/^h([1-6])\s*/i);
|
||||
if (levelMatch) {
|
||||
const level = parseInt(levelMatch[1]);
|
||||
const textQuery = query.substring(levelMatch[0].length);
|
||||
|
||||
let results = headings.filter(h => h.level === level);
|
||||
if (textQuery) {
|
||||
results = results.filter(h =>
|
||||
h.text.toLowerCase().includes(textQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Default text search
|
||||
return originalFilter(headings, query);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Headings Found
|
||||
|
||||
**Problem**: Contents panel shows "No headings found in document"
|
||||
|
||||
**Causes & Solutions**:
|
||||
|
||||
1. **Document has no headings**
|
||||
- Add headings to your markdown: `# Heading`, `## Subheading`
|
||||
- Click "🔄 Refresh" after adding headings
|
||||
|
||||
2. **Headings not rendered yet**
|
||||
- Wait for markdown rendering to complete
|
||||
- Use manual refresh button
|
||||
|
||||
3. **Wrong selector**
|
||||
- Verify headings are actual `<h1>` - `<h6>` elements (not styled divs)
|
||||
- Check browser console for errors
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Check if headings exist in DOM
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
console.log('Headings found:', headings.length);
|
||||
```
|
||||
|
||||
### Navigation Not Working
|
||||
|
||||
**Problem**: Clicking headings in TOC doesn't navigate
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check for errors**:
|
||||
```javascript
|
||||
// Test navigation programmatically
|
||||
editor.contentsControl.navigateToHeading('your-heading-id');
|
||||
```
|
||||
|
||||
2. **Verify heading IDs**:
|
||||
```javascript
|
||||
// List all heading IDs
|
||||
console.log(editor.contentsControl.headings.map(h => h.id));
|
||||
```
|
||||
|
||||
3. **Scroll container issues**:
|
||||
- Ensure content is in scrollable container
|
||||
- Check CSS `overflow` properties
|
||||
- Try without smooth scroll:
|
||||
```javascript
|
||||
const elem = document.getElementById('heading-id');
|
||||
elem.scrollIntoView({ behavior: 'auto' });
|
||||
```
|
||||
|
||||
### Search Not Working
|
||||
|
||||
**Problem**: Typing in search box doesn't filter headings
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check console for errors**
|
||||
2. **Verify control reference**:
|
||||
```javascript
|
||||
console.log(editor.contentsControl); // Should not be undefined
|
||||
```
|
||||
|
||||
3. **Test search manually**:
|
||||
```javascript
|
||||
editor.contentsControl.handleSearch('your query');
|
||||
```
|
||||
|
||||
4. **Clear search**:
|
||||
```javascript
|
||||
editor.contentsControl.searchQuery = '';
|
||||
editor.contentsControl.buildContent();
|
||||
```
|
||||
|
||||
### Duplicate Headings
|
||||
|
||||
**Problem**: Multiple headings with same text cause navigation issues
|
||||
|
||||
**Solution**: The control auto-generates unique IDs by appending counters:
|
||||
```
|
||||
heading-text → heading-text
|
||||
heading-text → heading-text-1
|
||||
heading-text → heading-text-2
|
||||
```
|
||||
|
||||
To use custom IDs:
|
||||
```html
|
||||
<h2 id="custom-id-1">Same Text</h2>
|
||||
<h2 id="custom-id-2">Same Text</h2>
|
||||
```
|
||||
|
||||
### Auto-Refresh Not Working
|
||||
|
||||
**Problem**: TOC doesn't update when headings change
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check if interval is running**:
|
||||
```javascript
|
||||
console.log(editor.contentsControl.updateInterval); // Should be a number
|
||||
```
|
||||
|
||||
2. **Restart auto-refresh**:
|
||||
```javascript
|
||||
clearInterval(editor.contentsControl.updateInterval);
|
||||
editor.contentsControl.updateInterval = setInterval(() => {
|
||||
const currentCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
|
||||
if (currentCount !== editor.contentsControl.headings.length) {
|
||||
editor.contentsControl.refreshContents();
|
||||
}
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
3. **Use manual refresh**: Click the "🔄 Refresh Contents" button
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Writers
|
||||
|
||||
1. **Use logical heading hierarchy** - Don't skip levels (h1 → h3)
|
||||
2. **Keep headings concise** - Long headings clutter the TOC
|
||||
3. **Use descriptive text** - Helps readers and search functionality
|
||||
4. **Refresh after major edits** - Ensure TOC stays current
|
||||
|
||||
### For Readers
|
||||
|
||||
1. **Use search** - Quickly find sections in long documents
|
||||
2. **Hover for highlights** - See which section each link goes to
|
||||
3. **Leave panel expanded** - Keep TOC visible for quick navigation
|
||||
4. **Bookmark important sections** - Use browser bookmarks with heading IDs
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Provide heading IDs** - Don't rely on auto-generation for critical links
|
||||
2. **Test navigation** - Verify smooth scroll works across browsers
|
||||
3. **Monitor performance** - Disable auto-refresh for very large documents (500+ headings)
|
||||
4. **Integrate analytics** - Track which sections users visit most
|
||||
|
||||
Example analytics integration:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Track TOC usage
|
||||
const originalNav = editor.contentsControl.navigateToHeading.bind(editor.contentsControl);
|
||||
let navCount = 0;
|
||||
|
||||
editor.contentsControl.navigateToHeading = function(headingId) {
|
||||
navCount++;
|
||||
console.log(`TOC navigation #${navCount} to: ${headingId}`);
|
||||
|
||||
// Send to analytics service
|
||||
if (window.analytics) {
|
||||
analytics.track('TOC Navigation', {
|
||||
headingId,
|
||||
headingText: this.headings.find(h => h.id === headingId)?.text,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return originalNav(headingId);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### For Large Documents (100+ headings)
|
||||
|
||||
**Impact**: Auto-refresh and search may slow down
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Increase refresh interval**:
|
||||
```javascript
|
||||
clearInterval(editor.contentsControl.updateInterval);
|
||||
editor.contentsControl.updateInterval = setInterval(() => {
|
||||
// Check logic
|
||||
}, 15000); // 15 seconds instead of 5
|
||||
```
|
||||
|
||||
2. **Disable auto-refresh**:
|
||||
```javascript
|
||||
clearInterval(editor.contentsControl.updateInterval);
|
||||
editor.contentsControl.updateInterval = null;
|
||||
```
|
||||
|
||||
3. **Lazy rendering** - Only render visible TOC items (not currently implemented)
|
||||
|
||||
### Memory Usage
|
||||
|
||||
Each heading stores:
|
||||
- ID string (~20 bytes)
|
||||
- Text string (~50 bytes)
|
||||
- Element reference (8 bytes)
|
||||
- Level + index (8 bytes)
|
||||
|
||||
**Estimate**: ~86 bytes per heading
|
||||
- 100 headings: ~8.6 KB
|
||||
- 1000 headings: ~86 KB
|
||||
|
||||
Negligible for most documents. No memory leaks if control is properly destroyed.
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Section Editing](section-editing.md)** - Edit headings, triggers TOC refresh
|
||||
- **[Status Control](status-control.md)** - Shows heading count statistics
|
||||
- **[Edit Control](edit-control.md)** - Go to line feature (complementary navigation)
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [API Reference](../api/contents-control.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
785
docs/features/debug-control.md
Normal file
785
docs/features/debug-control.md
Normal file
@@ -0,0 +1,785 @@
|
||||
# Debug Control
|
||||
|
||||
**Development and debugging tools** 🐛
|
||||
|
||||
The Debug Control provides comprehensive debugging capabilities including real-time console message capture, error tracking, performance monitoring, and system information display. Essential for development, troubleshooting, and quality assurance workflows. Positioned in the west by default. **Disabled by default** - enable explicitly for development.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Debug Control serves as a central hub for all debugging activities, capturing console messages, tracking errors, monitoring performance, and displaying system information. Perfect for developers troubleshooting issues or optimizing performance.
|
||||
|
||||
### Position
|
||||
- **Default**: West (w) - middle-left side
|
||||
- **Appearance**: 🐛 icon when collapsed
|
||||
- **Title**: "Debug"
|
||||
- **Status**: **Disabled by default** (enable for development)
|
||||
|
||||
### Key Features
|
||||
|
||||
- 📝 **Console capture** - Records all console.log, error, warn, info, debug
|
||||
- 🚨 **Error tracking** - Captures global errors and unhandled promises
|
||||
- 📊 **Performance metrics** - Page load times, session duration
|
||||
- 💻 **System information** - Browser, viewport, memory, language
|
||||
- 🔍 **Message filtering** - Filter by error/warn/info/debug levels
|
||||
- 💾 **Export logs** - Download debug session as JSON
|
||||
- ⏸️ **Pause/Resume** - Control message recording
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable Debug Control
|
||||
|
||||
**Important**: Debug control is **disabled by default**. You must explicitly enable it:
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
debugControl: true // Enable debug panel (default: false)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
debugControl: true
|
||||
},
|
||||
controlPositions: {
|
||||
debugControl: 'w' // West (default), or: nw, ne, e, se, s, sw
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Console Message Capture
|
||||
|
||||
The Debug Control automatically captures all console output.
|
||||
|
||||
#### Captured Methods
|
||||
|
||||
- `console.log()` → INFO messages
|
||||
- `console.error()` → ERROR messages
|
||||
- `console.warn()` → WARN messages
|
||||
- `console.info()` → INFO messages
|
||||
- `console.debug()` → DEBUG messages
|
||||
|
||||
#### How It Works
|
||||
|
||||
**Initialization**:
|
||||
1. Saves original console methods
|
||||
2. Overrides each method with custom wrapper
|
||||
3. Wrapper calls original method (preserves normal console behavior)
|
||||
4. Wrapper also logs to Debug Control
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
console.log('Application started');
|
||||
// ✅ Shows in browser console (normal)
|
||||
// ✅ Shows in Debug Control (captured)
|
||||
```
|
||||
|
||||
**Restoration**:
|
||||
When Debug Control is destroyed, original console methods are restored.
|
||||
|
||||
#### Message Structure
|
||||
|
||||
Each captured message includes:
|
||||
- **Category**: LOG, ERROR, WARN, INFO, DEBUG
|
||||
- **Timestamp**: Absolute time (HH:MM:SS)
|
||||
- **Relative time**: Milliseconds since session start
|
||||
- **Message**: Combined text from all arguments
|
||||
- **Level**: error, warn, info, debug (for filtering)
|
||||
|
||||
---
|
||||
|
||||
### 2. Error Tracking
|
||||
|
||||
Captures errors automatically without requiring try/catch blocks.
|
||||
|
||||
#### Global Error Capture
|
||||
|
||||
**Window errors**:
|
||||
```javascript
|
||||
window.addEventListener('error', (event) => {
|
||||
// Captured: message, filename, line number
|
||||
});
|
||||
```
|
||||
|
||||
**Example captured error**:
|
||||
```
|
||||
ERROR: Uncaught TypeError: Cannot read property 'foo' of undefined at script.js:42
|
||||
```
|
||||
|
||||
#### Unhandled Promise Rejections
|
||||
|
||||
**Promise rejections**:
|
||||
```javascript
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
// Captured: rejection reason
|
||||
});
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
fetch('/api/data')
|
||||
.then(res => res.json())
|
||||
// No .catch() - rejection captured automatically
|
||||
```
|
||||
|
||||
**Captured message**:
|
||||
```
|
||||
PROMISE_REJECT: Unhandled promise rejection: NetworkError: Failed to fetch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. System Information
|
||||
|
||||
Displays comprehensive browser and environment details.
|
||||
|
||||
#### Displayed Information
|
||||
|
||||
| Property | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| **Markitect** | Version number | 1.0.0 |
|
||||
| **Viewport** | Window dimensions | 1920x1080 |
|
||||
| **Screen** | Physical display size | 2560x1440 |
|
||||
| **Memory** | JavaScript heap usage | Used: 45MB |
|
||||
| **Language** | Browser language | en-US |
|
||||
| **Status** | Online/offline | Online |
|
||||
| **Protocol** | HTTP/HTTPS | https: |
|
||||
|
||||
**Additional info** (not displayed but available):
|
||||
- User agent string
|
||||
- Color depth
|
||||
- Timezone
|
||||
- Cookies enabled
|
||||
|
||||
#### Memory Information
|
||||
|
||||
**Note**: `performance.memory` is only available in Chrome/Edge with `--enable-precise-memory-info` flag.
|
||||
|
||||
**Fallback**: Shows "Not available" if memory API unavailable
|
||||
|
||||
---
|
||||
|
||||
### 4. Performance Metrics
|
||||
|
||||
Real-time performance monitoring.
|
||||
|
||||
#### Metrics Displayed
|
||||
|
||||
| Metric | Description | Calculation |
|
||||
|--------|-------------|-------------|
|
||||
| **Page Load** | Total page load time | loadEventEnd - navigationStart |
|
||||
| **DOM Ready** | DOM parsing complete | domContentLoadedEventEnd - navigationStart |
|
||||
| **First Byte** | Time to first response byte | responseStart - navigationStart |
|
||||
| **Session Time** | Time since Debug Control started | Current time - startTime |
|
||||
| **Debug Messages** | Total messages captured | messages.length |
|
||||
|
||||
**Units**: Milliseconds for load metrics, seconds for session time
|
||||
|
||||
**Example display**:
|
||||
```
|
||||
Page Load: 1247ms
|
||||
DOM Ready: 892ms
|
||||
First Byte: 124ms
|
||||
Session Time: 45s
|
||||
Debug Messages: 37
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Message Filtering
|
||||
|
||||
Filter messages by level to focus on specific types.
|
||||
|
||||
#### Filter Options
|
||||
|
||||
- **ALL** - Show all messages (default)
|
||||
- **ERROR** - Only error messages
|
||||
- **WARN** - Only warnings
|
||||
- **INFO** - Only info messages
|
||||
- **DEBUG** - Only debug messages
|
||||
|
||||
#### Usage
|
||||
|
||||
Click filter button in Debug Control panel.
|
||||
|
||||
**Visual feedback**:
|
||||
- Active filter: Blue button
|
||||
- Inactive filters: Gray buttons
|
||||
|
||||
**Status display**: Shows "Filter: ERROR | Messages: 5/37"
|
||||
- 5 = filtered count
|
||||
- 37 = total count
|
||||
|
||||
---
|
||||
|
||||
### 6. Message Display
|
||||
|
||||
Shows last 20 messages in scrollable container.
|
||||
|
||||
#### Message Appearance
|
||||
|
||||
**Color coding**:
|
||||
- **Error**: Red border, red badge (#dc3545)
|
||||
- **Warn**: Yellow border, yellow badge (#ffc107)
|
||||
- **Info**: Teal border, teal badge (#17a2b8)
|
||||
- **Debug**: Gray border, gray badge (#6c757d)
|
||||
|
||||
**Message format**:
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ [ERROR] 14:32:15 │
|
||||
│ Cannot read property 'x' │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
**Auto-scroll**: Enabled by default, scrolls to newest message
|
||||
|
||||
#### Message Limit
|
||||
|
||||
**Max messages**: 100 (oldest messages are discarded)
|
||||
|
||||
**Displayed**: Last 20 messages visible in panel
|
||||
|
||||
**Reason**: Prevent memory issues with long sessions
|
||||
|
||||
---
|
||||
|
||||
### 7. Control Actions
|
||||
|
||||
Four action buttons for managing debug session.
|
||||
|
||||
#### 🗑️ Clear
|
||||
|
||||
Removes all captured messages.
|
||||
|
||||
**Usage**: Click "🗑️ Clear" button
|
||||
|
||||
**Effect**:
|
||||
- Empties messages array
|
||||
- Clears display
|
||||
- Also clears MarkitectDebugSystem if present
|
||||
|
||||
**Use case**: Start fresh debugging session
|
||||
|
||||
#### 💾 Export
|
||||
|
||||
Download debug log as JSON file.
|
||||
|
||||
**Usage**: Click "💾 Export" button
|
||||
|
||||
**File contents**:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-16T14:30:52.123Z",
|
||||
"session": {
|
||||
"startTime": "2025-12-16T14:25:12.456Z",
|
||||
"duration": 340000,
|
||||
"messageCount": 37
|
||||
},
|
||||
"system": {
|
||||
"userAgent": "Mozilla/5.0...",
|
||||
"viewport": "1920x1080",
|
||||
"url": "http://localhost:8080/editor.html"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": 1702742452123.456,
|
||||
"timestamp": 1702742452123,
|
||||
"category": "ERROR",
|
||||
"message": "Cannot read property...",
|
||||
"level": "error",
|
||||
"displayTime": "14:32:15",
|
||||
"relativeTime": 123456
|
||||
}
|
||||
// ... more messages
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Filename**: `debug-log-YYYY-MM-DD.json`
|
||||
|
||||
**Use cases**:
|
||||
- Share logs with team
|
||||
- Attach to bug reports
|
||||
- Archive for analysis
|
||||
|
||||
#### ⏸️ Pause / ▶️ Record
|
||||
|
||||
Toggle message capture on/off.
|
||||
|
||||
**Usage**: Click "⏸️ Pause" or "▶️ Record" button
|
||||
|
||||
**States**:
|
||||
- **Recording** (▶️): Yellow button, captures messages
|
||||
- **Paused** (⏸️): Gray button, ignores new messages
|
||||
|
||||
**Use case**: Pause during noisy operations, resume for specific testing
|
||||
|
||||
**Status indicator**: Shows "Recording: 🟢 Active" or "🔴 Paused" at bottom
|
||||
|
||||
#### 🧪 Test
|
||||
|
||||
Add random test message to verify Debug Control is working.
|
||||
|
||||
**Usage**: Click "🧪 Test" button
|
||||
|
||||
**Generates**: Random message from:
|
||||
- "This is a test info message" (INFO)
|
||||
- "This is a test warning message" (WARN)
|
||||
- "This is a test error message" (ERROR)
|
||||
- "This is a test debug message" (DEBUG)
|
||||
|
||||
**Use case**: Verify Debug Control is capturing and displaying correctly
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage (Development Mode)
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# My Document',
|
||||
controls: {
|
||||
editControl: true,
|
||||
statusControl: true,
|
||||
contentsControl: true,
|
||||
debugControl: true // Enable for development
|
||||
}
|
||||
});
|
||||
|
||||
// Debug messages will be captured
|
||||
console.log('Editor initialized');
|
||||
console.error('Oops, something went wrong');
|
||||
```
|
||||
|
||||
### Production Mode (Debug Disabled)
|
||||
|
||||
```javascript
|
||||
// Default: debug control is OFF
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
debugControl: false // Explicit disable (default)
|
||||
}
|
||||
});
|
||||
|
||||
// Console still works normally, just not captured
|
||||
console.log('Normal console output');
|
||||
```
|
||||
|
||||
### Environment-Based Configuration
|
||||
|
||||
```javascript
|
||||
const isDevelopment = window.location.hostname === 'localhost';
|
||||
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
debugControl: isDevelopment // Auto-enable in dev
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Error Reporting
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: { debugControl: true }
|
||||
});
|
||||
|
||||
// Add custom error handler
|
||||
window.addEventListener('error', (event) => {
|
||||
// Send to analytics
|
||||
fetch('/api/errors', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Properties
|
||||
|
||||
Access via `editor.debugControl`:
|
||||
|
||||
```javascript
|
||||
const debugCtrl = editor.debugControl;
|
||||
|
||||
// Message storage
|
||||
debugCtrl.messages = [
|
||||
{
|
||||
id: 1702742452123.456,
|
||||
timestamp: 1702742452123,
|
||||
category: 'ERROR',
|
||||
message: 'Error text',
|
||||
level: 'error',
|
||||
displayTime: '14:32:15',
|
||||
relativeTime: 123456
|
||||
},
|
||||
// ... more messages
|
||||
];
|
||||
|
||||
// Configuration
|
||||
debugCtrl.maxMessages = 100; // Message limit
|
||||
debugCtrl.messageFilter = 'all'; // Current filter
|
||||
debugCtrl.autoScroll = true; // Auto-scroll enabled
|
||||
debugCtrl.isRecording = true; // Recording state
|
||||
debugCtrl.startTime = 1702742452000; // Session start
|
||||
|
||||
// Original console methods (for restoration)
|
||||
debugCtrl.originalConsole = {
|
||||
log: Function,
|
||||
error: Function,
|
||||
warn: Function,
|
||||
info: Function,
|
||||
debug: Function
|
||||
};
|
||||
|
||||
// Performance marks
|
||||
debugCtrl.performanceMarks = new Map();
|
||||
|
||||
// Control config
|
||||
debugCtrl.config.position = 'w'; // Compass position
|
||||
debugCtrl.config.icon = '🐛'; // Panel icon
|
||||
debugCtrl.config.title = 'Debug'; // Panel title
|
||||
debugCtrl.isExpanded; // true if expanded
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```javascript
|
||||
const debugCtrl = editor.debugControl;
|
||||
|
||||
// Add debug message programmatically
|
||||
debugCtrl.addDebugMessage('CUSTOM', 'Custom message text', 'info');
|
||||
// Parameters: category, message, level
|
||||
|
||||
// Get filtered messages
|
||||
const filtered = debugCtrl.getFilteredMessages();
|
||||
// Returns: Array of messages matching current filter
|
||||
|
||||
// Clear all messages
|
||||
debugCtrl.clearMessages();
|
||||
|
||||
// Export messages to JSON file
|
||||
debugCtrl.exportMessages();
|
||||
|
||||
// Toggle recording on/off
|
||||
debugCtrl.toggleRecording();
|
||||
|
||||
// Add test message
|
||||
debugCtrl.addTestMessage();
|
||||
|
||||
// Set message filter
|
||||
debugCtrl.setMessageFilter('error'); // 'all', 'error', 'warn', 'info', 'debug'
|
||||
|
||||
// Generate HTML components
|
||||
const systemInfoHTML = debugCtrl.generateSystemInfoHTML();
|
||||
const performanceHTML = debugCtrl.generatePerformanceHTML();
|
||||
const messagesHTML = debugCtrl.generateMessagesHTML();
|
||||
const buttonsHTML = debugCtrl.generateControlButtonsHTML();
|
||||
const filterHTML = debugCtrl.generateFilterControlsHTML();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Message Categories
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: { debugControl: true }
|
||||
});
|
||||
|
||||
// Add custom category messages
|
||||
editor.debugControl.addDebugMessage('DATABASE', 'Query executed in 45ms', 'info');
|
||||
editor.debugControl.addDebugMessage('API', 'Request to /api/users completed', 'info');
|
||||
editor.debugControl.addDebugMessage('AUTH', 'User authentication failed', 'error');
|
||||
```
|
||||
|
||||
### Performance Marking
|
||||
|
||||
```javascript
|
||||
const debugCtrl = editor.debugControl;
|
||||
|
||||
// Mark start of operation
|
||||
debugCtrl.performanceMarks.set('render-start', Date.now());
|
||||
|
||||
// ... perform rendering ...
|
||||
|
||||
// Mark end and calculate duration
|
||||
const startTime = debugCtrl.performanceMarks.get('render-start');
|
||||
const duration = Date.now() - startTime;
|
||||
debugCtrl.addDebugMessage('PERF', `Rendering took ${duration}ms`, 'debug');
|
||||
```
|
||||
|
||||
### Conditional Logging
|
||||
|
||||
```javascript
|
||||
const DEBUG_ENABLED = true;
|
||||
|
||||
function debugLog(message) {
|
||||
if (DEBUG_ENABLED && editor.debugControl) {
|
||||
editor.debugControl.addDebugMessage('APP', message, 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('User clicked save button');
|
||||
debugLog('Validation passed');
|
||||
```
|
||||
|
||||
### Batch Export on Error
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: { debugControl: true }
|
||||
});
|
||||
|
||||
// Auto-export logs when critical error occurs
|
||||
window.addEventListener('error', (event) => {
|
||||
if (event.message.includes('Critical')) {
|
||||
editor.debugControl.exportMessages();
|
||||
alert('Critical error logged. Debug log downloaded.');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Monitor Specific Functions
|
||||
|
||||
```javascript
|
||||
function monitorFunction(fn, name) {
|
||||
return function(...args) {
|
||||
const startTime = performance.now();
|
||||
editor.debugControl.addDebugMessage('CALL', `${name} called with args: ${JSON.stringify(args)}`, 'debug');
|
||||
|
||||
try {
|
||||
const result = fn.apply(this, args);
|
||||
const duration = performance.now() - startTime;
|
||||
editor.debugControl.addDebugMessage('RESULT', `${name} completed in ${duration.toFixed(2)}ms`, 'info');
|
||||
return result;
|
||||
} catch (error) {
|
||||
editor.debugControl.addDebugMessage('ERROR', `${name} failed: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Use monitored function
|
||||
const saveDocument = monitorFunction(originalSaveFunction, 'saveDocument');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debug Control Not Appearing
|
||||
|
||||
**Problem**: Panel doesn't show even when enabled
|
||||
|
||||
**Solutions**:
|
||||
1. Verify configuration:
|
||||
```javascript
|
||||
console.log(editor.config.controls.debugControl); // Should be true
|
||||
```
|
||||
|
||||
2. Check if DebugControl is loaded:
|
||||
```javascript
|
||||
console.log(typeof DebugControl); // Should be 'function'
|
||||
```
|
||||
|
||||
3. Verify instance exists:
|
||||
```javascript
|
||||
console.log(editor.debugControl); // Should be object
|
||||
```
|
||||
|
||||
4. Check browser console for errors
|
||||
|
||||
### Messages Not Captured
|
||||
|
||||
**Problem**: Console messages don't appear in Debug Control
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check recording status**:
|
||||
```javascript
|
||||
console.log(editor.debugControl.isRecording); // Should be true
|
||||
```
|
||||
|
||||
2. **Verify console override**:
|
||||
```javascript
|
||||
console.log(typeof editor.debugControl.originalConsole); // Should be 'object'
|
||||
```
|
||||
|
||||
3. **Test manually**:
|
||||
```javascript
|
||||
editor.debugControl.addDebugMessage('TEST', 'Manual test', 'info');
|
||||
```
|
||||
|
||||
4. **Check if panel is expanded**: Click the 🐛 icon to expand
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Problem**: Debug Control slows down application
|
||||
|
||||
**Causes & Solutions**:
|
||||
|
||||
1. **Too many messages**:
|
||||
- Clear messages regularly
|
||||
- Reduce maxMessages:
|
||||
```javascript
|
||||
editor.debugControl.maxMessages = 50; // Lower limit
|
||||
```
|
||||
|
||||
2. **Noisy logging**:
|
||||
- Pause recording during intensive operations:
|
||||
```javascript
|
||||
editor.debugControl.toggleRecording(); // Pause
|
||||
// ... intensive work ...
|
||||
editor.debugControl.toggleRecording(); // Resume
|
||||
```
|
||||
|
||||
3. **Disable in production**:
|
||||
```javascript
|
||||
controls: {
|
||||
debugControl: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
```
|
||||
|
||||
### Export Not Working
|
||||
|
||||
**Problem**: Export button doesn't download file
|
||||
|
||||
**Solutions**:
|
||||
1. Check browser download permissions
|
||||
2. Verify popup/download blocker settings
|
||||
3. Test programmatically:
|
||||
```javascript
|
||||
editor.debugControl.exportMessages();
|
||||
```
|
||||
4. Check browser console for errors
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Development
|
||||
|
||||
1. **Enable only in development**:
|
||||
```javascript
|
||||
debugControl: window.location.hostname === 'localhost'
|
||||
```
|
||||
|
||||
2. **Use meaningful categories**:
|
||||
```javascript
|
||||
addDebugMessage('USER_ACTION', 'Clicked save', 'info');
|
||||
addDebugMessage('VALIDATION', 'Email format invalid', 'warn');
|
||||
```
|
||||
|
||||
3. **Export before refreshing**: Save logs if you need to refresh page
|
||||
|
||||
4. **Clear between tests**: Start fresh for each test scenario
|
||||
|
||||
### For QA/Testing
|
||||
|
||||
1. **Enable for bug reproduction**:
|
||||
```javascript
|
||||
controls: { debugControl: true }
|
||||
```
|
||||
|
||||
2. **Export logs with bug reports**: Include debug-log.json
|
||||
|
||||
3. **Filter by ERROR**: Focus on failures
|
||||
|
||||
4. **Track timing**: Monitor performance metrics
|
||||
|
||||
### For Production Debugging
|
||||
|
||||
**Generally**: Keep debug control **disabled** in production
|
||||
|
||||
**Exception**: Enable temporarily for specific user sessions:
|
||||
|
||||
```javascript
|
||||
// Enable debug for admin users only
|
||||
const isAdmin = checkUserRole();
|
||||
const editor = new TestDriveJSUI({
|
||||
controls: {
|
||||
debugControl: isAdmin
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Security note**: Debug logs may contain sensitive information (URLs, user actions). Don't expose in production.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Usage
|
||||
|
||||
**Per message**: ~200 bytes (category, message, timestamps)
|
||||
- 100 messages: ~20 KB
|
||||
- 1000 messages: ~200 KB
|
||||
|
||||
**Max limit**: Default 100 messages prevents unbounded growth
|
||||
|
||||
### CPU Impact
|
||||
|
||||
**Console override**: Minimal overhead (~0.1ms per message)
|
||||
|
||||
**Display updates**: Only when panel is expanded
|
||||
|
||||
**Auto-scroll**: Negligible performance impact
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Disable in production**: Saves memory and CPU
|
||||
2. **Lower maxMessages for long sessions**: Prevent accumulation
|
||||
3. **Pause during intensive operations**: Reduce overhead
|
||||
4. **Export and clear periodically**: Free memory
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Edit Control](edit-control.md)** - Document actions
|
||||
- **[Status Control](status-control.md)** - Document statistics
|
||||
- **[Contents Control](contents-control.md)** - Navigation
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [API Reference](../api/debug-control.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
458
docs/features/edit-control.md
Normal file
458
docs/features/edit-control.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# Edit Control
|
||||
|
||||
**Document actions and editing tools** 📝
|
||||
|
||||
The Edit Control provides comprehensive document-level actions including save, export, print, navigation tools, text formatting, and markdown shortcuts. Positioned in the northeast by default.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Edit Control is your command center for document-wide operations. It groups related actions into organized sections for easy access.
|
||||
|
||||
### Position
|
||||
- **Default**: Northeast (ne) - top-right corner
|
||||
- **Appearance**: ✏️ icon when collapsed
|
||||
- **Title**: "Edit"
|
||||
|
||||
### Key Features
|
||||
|
||||
- 📄 Document actions (save, print, export, reset)
|
||||
- 🧭 Navigation tools (scroll to top/bottom, go to line)
|
||||
- 🔍 Text tools (find & replace, font size)
|
||||
- ✍️ Markdown shortcuts (bold, italic, headers, links)
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true // Enable (default: true)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
editControl: 'ne' // Northeast (default), or: nw, e, se, s, sw, w
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Document Actions
|
||||
|
||||
#### 🖨️ Print Document
|
||||
Opens browser print dialog for the current document.
|
||||
|
||||
**Usage**:
|
||||
- Click "Print Document" button
|
||||
- Or use keyboard shortcut: `Ctrl+P`
|
||||
|
||||
**What happens**:
|
||||
- Formats document for printing
|
||||
- Opens browser print dialog
|
||||
- Preserves markdown formatting
|
||||
|
||||
#### 💾 Save Changes
|
||||
Downloads the current document as a markdown file.
|
||||
|
||||
**Usage**:
|
||||
- Click "Save Changes" button
|
||||
- Or use keyboard shortcut: `Ctrl+S`
|
||||
|
||||
**Behavior**:
|
||||
- Generates timestamped filename
|
||||
- Downloads as `.md` file
|
||||
- Includes all accepted edits
|
||||
|
||||
**Example filename**: `document-edited-2025-12-16-143052.md`
|
||||
|
||||
#### 📄 Export Document
|
||||
Export document in various formats.
|
||||
|
||||
**Usage**:
|
||||
- Click "Export Document" button
|
||||
- Choose format in dialog
|
||||
|
||||
**Formats** (coming soon):
|
||||
- Markdown (`.md`)
|
||||
- HTML (`.html`)
|
||||
- PDF (`.pdf`)
|
||||
- Plain text (`.txt`)
|
||||
|
||||
#### 🔄 Reset All
|
||||
Resets all sections to their original content.
|
||||
|
||||
**Usage**:
|
||||
- Click "Reset All" button
|
||||
- Confirm in dialog
|
||||
|
||||
**Warning**: This discards ALL changes! Cannot be undone.
|
||||
|
||||
---
|
||||
|
||||
### 2. Navigation Tools
|
||||
|
||||
#### ⬆️ Scroll to Top
|
||||
Instantly scrolls to the beginning of the document.
|
||||
|
||||
**Usage**:
|
||||
- Click "Top" button
|
||||
- Smooth scroll animation
|
||||
|
||||
#### ⬇️ Scroll to Bottom
|
||||
Instantly scrolls to the end of the document.
|
||||
|
||||
**Usage**:
|
||||
- Click "Bottom" button
|
||||
- Smooth scroll animation
|
||||
|
||||
#### 🎯 Go to Line
|
||||
Jump to a specific line or section in the document.
|
||||
|
||||
**Usage**:
|
||||
- Click "Go to Line" button
|
||||
- Enter line number or section name
|
||||
- Press Enter to navigate
|
||||
|
||||
---
|
||||
|
||||
### 3. Text Tools
|
||||
|
||||
#### 🔍 Find & Replace
|
||||
Search and replace text across the document.
|
||||
|
||||
**Usage**:
|
||||
- Click "Find & Replace" button
|
||||
- Or use keyboard shortcut: `Ctrl+F`
|
||||
|
||||
**Features**:
|
||||
- Case-sensitive option
|
||||
- Replace one or all occurrences
|
||||
- Regex support (coming soon)
|
||||
- Highlight matches
|
||||
|
||||
#### 🔍+ / 🔍- Font Size
|
||||
Adjust document font size for better readability.
|
||||
|
||||
**Usage**:
|
||||
- Click "🔍+ Font" to increase
|
||||
- Click "🔍- Font" to decrease
|
||||
|
||||
**Range**: 12px - 24px
|
||||
|
||||
**Tip**: Useful for presentations or accessibility
|
||||
|
||||
#### 📋 Copy Page Link
|
||||
Copies the current page URL to clipboard.
|
||||
|
||||
**Usage**:
|
||||
- Click "Copy Page Link" button
|
||||
- Link copied to clipboard
|
||||
- Paste anywhere with `Ctrl+V`
|
||||
|
||||
---
|
||||
|
||||
### 4. Markdown Tools
|
||||
|
||||
Quick markdown formatting shortcuts.
|
||||
|
||||
#### **B** Bold
|
||||
Wraps selected text in `**bold**` markers.
|
||||
|
||||
**Usage**:
|
||||
- Select text
|
||||
- Click "B" button
|
||||
- Or use: `Ctrl+B`
|
||||
|
||||
**Result**: `**selected text**`
|
||||
|
||||
#### *I* Italic
|
||||
Wraps selected text in `*italic*` markers.
|
||||
|
||||
**Usage**:
|
||||
- Select text
|
||||
- Click "I" button
|
||||
- Or use: `Ctrl+I`
|
||||
|
||||
**Result**: `*selected text*`
|
||||
|
||||
#### # Header
|
||||
Converts line to heading.
|
||||
|
||||
**Levels**:
|
||||
- H1: `# Heading`
|
||||
- H2: `## Heading`
|
||||
- H3: `### Heading`
|
||||
|
||||
#### 🔗 Link
|
||||
Inserts markdown link format.
|
||||
|
||||
**Usage**:
|
||||
- Click "Link" button
|
||||
- Inserts: `[text](url)`
|
||||
|
||||
#### 📋 Code
|
||||
Wraps text in code markers.
|
||||
|
||||
**Inline**: `` `code` ``
|
||||
**Block**:
|
||||
```
|
||||
```language
|
||||
code block
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# My Document\n\nContent here...',
|
||||
controls: {
|
||||
editControl: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position
|
||||
|
||||
```javascript
|
||||
// Move to northwest (top-left)
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
editControl: 'nw' // Top-left instead of top-right
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### With Event Tracking
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: { editControl: true }
|
||||
});
|
||||
|
||||
// Listen to save events
|
||||
editor.on('save', (data) => {
|
||||
console.log('Document saved:', data.markdown);
|
||||
// Send to backend, update UI, etc.
|
||||
});
|
||||
|
||||
// Access edit control directly
|
||||
if (editor.editControl) {
|
||||
// Programmatically trigger actions
|
||||
editor.editControl.saveDocument();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Methods
|
||||
|
||||
Access via `editor.editControl`:
|
||||
|
||||
```javascript
|
||||
const editCtrl = editor.editControl;
|
||||
|
||||
// Document actions
|
||||
editCtrl.printDocument(); // Open print dialog
|
||||
editCtrl.saveDocument(); // Download as .md
|
||||
editCtrl.exportDocument(); // Export in format
|
||||
editCtrl.resetAll(); // Reset all sections
|
||||
|
||||
// Navigation
|
||||
editCtrl.scrollToTop(); // Scroll to top
|
||||
editCtrl.scrollToBottom(); // Scroll to bottom
|
||||
editCtrl.showGoToLine(); // Show go-to dialog
|
||||
|
||||
// Text tools
|
||||
editCtrl.showFindReplace(); // Open find & replace
|
||||
editCtrl.increaseFontSize(); // Increase font
|
||||
editCtrl.decreaseFontSize(); // Decrease font
|
||||
editCtrl.copyLink(); // Copy page URL
|
||||
|
||||
// Markdown tools
|
||||
editCtrl.insertMarkdown(before, after, placeholder);
|
||||
// Example: insertMarkdown('**', '**', 'bold text')
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
```javascript
|
||||
editCtrl.config.position // Current position (e.g., 'ne')
|
||||
editCtrl.config.icon // Icon emoji ('✏️')
|
||||
editCtrl.config.title // Panel title ('Edit')
|
||||
editCtrl.isExpanded // true if panel expanded
|
||||
editCtrl.fontSize // Current font size (px)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
The Edit Control provides these shortcuts:
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Ctrl+S` | Save document |
|
||||
| `Ctrl+P` | Print document |
|
||||
| `Ctrl+F` | Find & replace |
|
||||
| `Ctrl+B` | Bold selected text |
|
||||
| `Ctrl+I` | Italic selected text |
|
||||
| `Escape` | Exit edit mode |
|
||||
|
||||
**Enable/disable**:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
shortcuts: true // Enable shortcuts (default: true)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Disable Specific Actions
|
||||
|
||||
Currently, all Edit Control actions are enabled by default. To hide the control entirely:
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: false // Hide completely
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Future: Granular Control
|
||||
|
||||
Planned for v1.1:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: {
|
||||
enabled: true,
|
||||
actions: {
|
||||
print: true,
|
||||
save: true,
|
||||
export: false, // Hide export
|
||||
reset: false // Hide reset
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Writers
|
||||
|
||||
1. **Save frequently** - Use `Ctrl+S` or Save button regularly
|
||||
2. **Use shortcuts** - Learn `Ctrl+B`, `Ctrl+I` for quick formatting
|
||||
3. **Navigate efficiently** - Use Top/Bottom for long documents
|
||||
4. **Find & Replace** - Bulk edit repetitive content
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Listen to events** - Track saves for auto-sync
|
||||
2. **Custom save handlers** - Override default behavior
|
||||
3. **Programmatic control** - Trigger actions via API
|
||||
4. **Error handling** - Catch save failures
|
||||
|
||||
Example custom save:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
editor.on('save', async (data) => {
|
||||
try {
|
||||
// Send to backend
|
||||
await fetch('/api/save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ markdown: data.markdown })
|
||||
});
|
||||
alert('Saved to server!');
|
||||
} catch (error) {
|
||||
alert('Save failed: ' + error.message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Save Changes" Not Working
|
||||
|
||||
**Problem**: Clicking save does nothing
|
||||
|
||||
**Solutions**:
|
||||
1. Check browser console for errors
|
||||
2. Verify EditControl is loaded: `typeof EditControl !== 'undefined'`
|
||||
3. Check if control is enabled in config
|
||||
4. Test programmatically: `editor.editControl.saveDocument()`
|
||||
|
||||
### Print Dialog Not Opening
|
||||
|
||||
**Problem**: Print button doesn't open dialog
|
||||
|
||||
**Solutions**:
|
||||
1. Check browser pop-up blocker
|
||||
2. Test with `Ctrl+P` keyboard shortcut
|
||||
3. Verify browser print permissions
|
||||
4. Try programmatically: `window.print()`
|
||||
|
||||
### Font Size Not Changing
|
||||
|
||||
**Problem**: Font buttons don't affect text size
|
||||
|
||||
**Solutions**:
|
||||
1. Check CSS conflicts with `font-size: inherit`
|
||||
2. Inspect element to verify font-size style applied
|
||||
3. Clear browser cache
|
||||
4. Test with browser zoom instead
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Section Editing](section-editing.md)** - Edit individual sections
|
||||
- **[Keyboard Shortcuts](keyboard-shortcuts.md)** - All available shortcuts
|
||||
- **[Status Control](status-control.md)** - Track editing progress
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [API Reference](../api/edit-control.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
673
docs/features/keyboard-shortcuts.md
Normal file
673
docs/features/keyboard-shortcuts.md
Normal file
@@ -0,0 +1,673 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
**Quick actions via keyboard** ⌨️
|
||||
|
||||
TestDrive-JSUI provides comprehensive keyboard shortcuts for efficient document editing and navigation. All shortcuts are enabled by default and work across all browsers. Keyboard shortcuts improve accessibility and productivity for power users.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Keyboard shortcuts provide fast access to common actions without reaching for the mouse. They're organized by context: global shortcuts, section editing shortcuts, and control-specific shortcuts.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Shortcut | Action | Context |
|
||||
|----------|--------|---------|
|
||||
| `Ctrl+S` | Save document | Global |
|
||||
| `Escape` | Close editor/dialog | Global |
|
||||
| `Ctrl+Enter` | Accept changes | Section editing |
|
||||
| `Ctrl+P` | Print document | Document (planned) |
|
||||
| `Ctrl+F` | Find & replace | Document (planned) |
|
||||
| `Ctrl+B` | Bold text | Text editing (planned) |
|
||||
| `Ctrl+I` | Italic text | Text editing (planned) |
|
||||
| `Tab` | Indent | Section editing |
|
||||
|
||||
**Note**: On macOS, `Ctrl` can be replaced with `Cmd` (⌘) for most shortcuts.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable Shortcuts
|
||||
|
||||
Keyboard shortcuts are **enabled by default**. To disable:
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
shortcuts: false // Disable all keyboard shortcuts
|
||||
});
|
||||
```
|
||||
|
||||
**When to disable**:
|
||||
- Conflict with existing application shortcuts
|
||||
- Custom keyboard handling required
|
||||
- Accessibility requirement for specific workflow
|
||||
|
||||
### Check Shortcut Status
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
console.log(editor.config.shortcuts); // true or false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Global Shortcuts
|
||||
|
||||
Available throughout the application, regardless of focus.
|
||||
|
||||
### Ctrl+S (Save Document)
|
||||
|
||||
**Action**: Save/download current document as markdown file
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+S`
|
||||
- macOS: `Cmd+S` (⌘S)
|
||||
|
||||
**Behavior**:
|
||||
1. Prevents browser's default save behavior
|
||||
2. Calls `editor.save()` method
|
||||
3. Downloads file with timestamped name: `document-edited-YYYY-MM-DD-HHMMSS.md`
|
||||
4. Triggers `save` event for custom handlers
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Listen to save event
|
||||
editor.on('save', (data) => {
|
||||
console.log('Document saved via Ctrl+S');
|
||||
console.log('Content:', data.markdown);
|
||||
});
|
||||
|
||||
// User presses Ctrl+S → save event fires
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Quick save during writing
|
||||
- Prevent accidental data loss
|
||||
- Trigger auto-upload to server
|
||||
|
||||
---
|
||||
|
||||
### Escape (Close Editor/Dialog)
|
||||
|
||||
**Action**: Close active floating editor or dialog
|
||||
|
||||
**Platforms**: All (no modifier key needed)
|
||||
|
||||
**Behavior**:
|
||||
1. Checks for active floating menu
|
||||
2. Closes section editor if open
|
||||
3. Cancels current edit operation
|
||||
4. Returns focus to document
|
||||
|
||||
**Contexts**:
|
||||
- **Section editing**: Cancels edit, discards changes
|
||||
- **Find & replace**: Closes find dialog (planned)
|
||||
- **Go to line**: Closes navigation dialog (planned)
|
||||
- **Image editor**: Closes image editor (planned)
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
// User clicks section to edit
|
||||
// Section editor opens with textarea
|
||||
// User presses Escape
|
||||
// → Editor closes, changes discarded
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Quick cancel during editing
|
||||
- Close dialogs without mouse
|
||||
- Keyboard-only workflow
|
||||
|
||||
---
|
||||
|
||||
## Section Editing Shortcuts
|
||||
|
||||
Active when editing a section (textarea has focus).
|
||||
|
||||
### Ctrl+Enter (Accept Changes)
|
||||
|
||||
**Action**: Accept changes and close section editor
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+Enter`
|
||||
- macOS: `Cmd+Enter` (⌘Enter)
|
||||
|
||||
**Behavior**:
|
||||
1. Prevents default Enter behavior (new line)
|
||||
2. Applies changes to section
|
||||
3. Closes floating editor
|
||||
4. Updates rendered markdown
|
||||
5. Triggers `changes-accepted` event
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
editor.sectionManager.on('changes-accepted', (data) => {
|
||||
console.log('Section updated via Ctrl+Enter');
|
||||
console.log('New content:', data.content);
|
||||
});
|
||||
|
||||
// User edits section, presses Ctrl+Enter
|
||||
// → Changes applied, editor closes
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Fast editing workflow
|
||||
- Apply changes without clicking Accept button
|
||||
- Keyboard-only editing
|
||||
|
||||
**Alternative**: Click "✓ Accept" button
|
||||
|
||||
---
|
||||
|
||||
### Escape (Cancel Section Edit)
|
||||
|
||||
**Action**: Cancel editing and discard changes
|
||||
|
||||
**Platforms**: All
|
||||
|
||||
**Behavior**:
|
||||
1. Restores original section content
|
||||
2. Closes floating editor
|
||||
3. No changes applied
|
||||
4. Triggers `changes-cancelled` event
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
// User clicks section, makes edits
|
||||
// User presses Escape
|
||||
// → Editor closes, original content restored
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Discard unwanted changes quickly
|
||||
- Exit edit mode without saving
|
||||
- Keyboard-only workflow
|
||||
|
||||
**Alternative**: Click "✗ Cancel" button
|
||||
|
||||
---
|
||||
|
||||
### Tab (Indent)
|
||||
|
||||
**Action**: Insert tab character (4 spaces)
|
||||
|
||||
**Platforms**: All
|
||||
|
||||
**Behavior**:
|
||||
1. Inserts 4 spaces at cursor position
|
||||
2. Does not move focus to next element
|
||||
3. Useful for code blocks and lists
|
||||
|
||||
**Example**:
|
||||
```markdown
|
||||
Before: |cursor here
|
||||
|
||||
After pressing Tab:
|
||||
|cursor here
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Indent code in code blocks
|
||||
- Create nested lists
|
||||
- Format markdown content
|
||||
|
||||
**Note**: Most browsers use Tab to move focus. TestDrive-JSUI overrides this behavior within textareas.
|
||||
|
||||
---
|
||||
|
||||
## Document Actions (Planned)
|
||||
|
||||
These shortcuts are planned for future releases.
|
||||
|
||||
### Ctrl+P (Print Document)
|
||||
|
||||
**Action**: Open browser print dialog
|
||||
|
||||
**Status**: 🔜 **Planned for v1.1**
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+P`
|
||||
- macOS: `Cmd+P` (⌘P)
|
||||
|
||||
**Expected behavior**:
|
||||
1. Prevents browser's default print
|
||||
2. Formats document for printing
|
||||
3. Opens print preview dialog
|
||||
|
||||
**Use cases**:
|
||||
- Quick print from editor
|
||||
- Print formatted markdown
|
||||
- Generate PDF via print
|
||||
|
||||
---
|
||||
|
||||
### Ctrl+F (Find & Replace)
|
||||
|
||||
**Action**: Open find and replace dialog
|
||||
|
||||
**Status**: 🔜 **Planned for v1.1**
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+F`
|
||||
- macOS: `Cmd+F` (⌘F)
|
||||
|
||||
**Expected behavior**:
|
||||
1. Prevents browser's default find
|
||||
2. Opens TestDrive-JSUI find dialog
|
||||
3. Focuses search input field
|
||||
4. Highlights matches in document
|
||||
|
||||
**Use cases**:
|
||||
- Search for text across document
|
||||
- Replace repetitive content
|
||||
- Navigate long documents
|
||||
|
||||
---
|
||||
|
||||
## Text Formatting (Planned)
|
||||
|
||||
These shortcuts will be available when text is selected or cursor is in text.
|
||||
|
||||
### Ctrl+B (Bold)
|
||||
|
||||
**Action**: Wrap selected text in `**bold**` markers
|
||||
|
||||
**Status**: 🔜 **Planned for v1.1**
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+B`
|
||||
- macOS: `Cmd+B` (⌘B)
|
||||
|
||||
**Expected behavior**:
|
||||
```
|
||||
Before: Selected text
|
||||
After: **Selected text**
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Quick markdown formatting
|
||||
- Bold emphasis in writing
|
||||
- Keyboard-only formatting
|
||||
|
||||
**Alternative**: Click "B" button in Edit Control
|
||||
|
||||
---
|
||||
|
||||
### Ctrl+I (Italic)
|
||||
|
||||
**Action**: Wrap selected text in `*italic*` markers
|
||||
|
||||
**Status**: 🔜 **Planned for v1.1**
|
||||
|
||||
**Platforms**:
|
||||
- Windows/Linux: `Ctrl+I`
|
||||
- macOS: `Cmd+I` (⌘I)
|
||||
|
||||
**Expected behavior**:
|
||||
```
|
||||
Before: Selected text
|
||||
After: *Selected text*
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Quick markdown formatting
|
||||
- Italic emphasis in writing
|
||||
- Keyboard-only formatting
|
||||
|
||||
**Alternative**: Click "I" button in Edit Control
|
||||
|
||||
---
|
||||
|
||||
## Browser vs. Application Shortcuts
|
||||
|
||||
### Conflicts
|
||||
|
||||
Some shortcuts conflict with browser defaults:
|
||||
|
||||
| Shortcut | Browser Default | TestDrive-JSUI |
|
||||
|----------|-----------------|----------------|
|
||||
| `Ctrl+S` | Browser save dialog | Document save |
|
||||
| `Ctrl+P` | Browser print | Document print (planned) |
|
||||
| `Ctrl+F` | Browser find | Document find (planned) |
|
||||
| `Ctrl+B` | Bookmarks sidebar | Bold text (planned) |
|
||||
| `Ctrl+I` | Open DevTools | Italic text (planned) |
|
||||
|
||||
**Solution**: TestDrive-JSUI calls `event.preventDefault()` to override browser behavior.
|
||||
|
||||
### Preserving Browser Shortcuts
|
||||
|
||||
Some browser shortcuts remain available:
|
||||
|
||||
- `Ctrl+Z` / `Cmd+Z` - Undo (within textareas)
|
||||
- `Ctrl+Y` / `Cmd+Shift+Z` - Redo (within textareas)
|
||||
- `Ctrl+C` / `Cmd+C` - Copy
|
||||
- `Ctrl+V` / `Cmd+V` - Paste
|
||||
- `Ctrl+X` / `Cmd+X` - Cut
|
||||
- `Ctrl+A` / `Cmd+A` - Select all
|
||||
- `F5` / `Ctrl+R` - Refresh page
|
||||
- `F11` - Fullscreen
|
||||
- `F12` - DevTools
|
||||
|
||||
---
|
||||
|
||||
## Custom Shortcuts
|
||||
|
||||
You can add custom shortcuts by listening to keyboard events:
|
||||
|
||||
### Example: Ctrl+E (Export)
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Ctrl+E or Cmd+E
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
|
||||
event.preventDefault();
|
||||
|
||||
// Custom export action
|
||||
const markdown = editor.getMarkdown();
|
||||
console.log('Export triggered:', markdown);
|
||||
|
||||
// Trigger export
|
||||
if (editor.editControl) {
|
||||
editor.editControl.exportDocument();
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Ctrl+Shift+D (Debug Toggle)
|
||||
|
||||
```javascript
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Ctrl+Shift+D
|
||||
if (event.ctrlKey && event.shiftKey && event.key === 'D') {
|
||||
event.preventDefault();
|
||||
|
||||
// Toggle debug control
|
||||
if (editor.debugControl) {
|
||||
if (editor.debugControl.isExpanded) {
|
||||
editor.debugControl.collapse();
|
||||
} else {
|
||||
editor.debugControl.expand();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Ctrl+K (Insert Link)
|
||||
|
||||
```javascript
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
|
||||
// Get active textarea
|
||||
const activeTextarea = document.activeElement;
|
||||
if (activeTextarea.tagName === 'TEXTAREA') {
|
||||
// Insert link markdown
|
||||
const selectedText = activeTextarea.value.substring(
|
||||
activeTextarea.selectionStart,
|
||||
activeTextarea.selectionEnd
|
||||
);
|
||||
|
||||
const linkMarkdown = selectedText
|
||||
? `[${selectedText}](url)`
|
||||
: '[text](url)';
|
||||
|
||||
// Insert at cursor
|
||||
const start = activeTextarea.selectionStart;
|
||||
const end = activeTextarea.selectionEnd;
|
||||
const text = activeTextarea.value;
|
||||
|
||||
activeTextarea.value = text.substring(0, start) + linkMarkdown + text.substring(end);
|
||||
|
||||
// Select 'url' part
|
||||
const urlStart = start + linkMarkdown.indexOf('(') + 1;
|
||||
const urlEnd = urlStart + 3;
|
||||
activeTextarea.setSelectionRange(urlStart, urlEnd);
|
||||
activeTextarea.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Keyboard-Only Operation
|
||||
|
||||
All TestDrive-JSUI features are accessible via keyboard:
|
||||
|
||||
1. **Navigation**: Tab through interactive elements
|
||||
2. **Section editing**: Enter to open, Escape to close
|
||||
3. **Controls**: Tab to reach, Space/Enter to activate
|
||||
4. **Shortcuts**: All documented shortcuts work
|
||||
|
||||
### Screen Reader Support
|
||||
|
||||
Keyboard shortcuts work with screen readers:
|
||||
|
||||
- **ARIA labels**: All buttons have descriptive labels
|
||||
- **Focus management**: Proper focus order maintained
|
||||
- **Announcements**: State changes announced to screen readers
|
||||
|
||||
**Example**:
|
||||
```html
|
||||
<button aria-label="Accept changes (Ctrl+Enter)">✓ Accept</button>
|
||||
```
|
||||
|
||||
### Custom Key Bindings
|
||||
|
||||
For users with accessibility needs:
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
shortcuts: false // Disable defaults
|
||||
});
|
||||
|
||||
// Implement custom shortcuts matching user's needs
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Custom bindings here
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Shortcut Not Working
|
||||
|
||||
**Problem**: Pressing shortcut does nothing
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check if shortcuts enabled**:
|
||||
```javascript
|
||||
console.log(editor.config.shortcuts); // Should be true
|
||||
```
|
||||
|
||||
2. **Check browser extension conflicts**:
|
||||
- Disable extensions like Vimium, Shortkeys
|
||||
- Test in incognito/private mode
|
||||
|
||||
3. **Check operating system conflicts**:
|
||||
- Some OS shortcuts override browser shortcuts
|
||||
- Example: Windows `Ctrl+S` might open system save dialog
|
||||
|
||||
4. **Test with different modifier**:
|
||||
- Try `Cmd` instead of `Ctrl` on macOS
|
||||
- Try `Ctrl` instead of `Cmd` on Windows
|
||||
|
||||
5. **Verify focus**:
|
||||
```javascript
|
||||
console.log(document.activeElement); // Check what has focus
|
||||
```
|
||||
|
||||
### Shortcut Conflicts
|
||||
|
||||
**Problem**: Shortcut triggers wrong action
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Disable TestDrive-JSUI shortcuts**:
|
||||
```javascript
|
||||
new TestDriveJSUI({ shortcuts: false });
|
||||
```
|
||||
|
||||
2. **Implement custom shortcuts**:
|
||||
```javascript
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Your custom logic
|
||||
});
|
||||
```
|
||||
|
||||
3. **Check browser extensions**: Disable conflicting extensions
|
||||
|
||||
### macOS Specific Issues
|
||||
|
||||
**Problem**: `Ctrl` shortcuts don't work on macOS
|
||||
|
||||
**Solution**: Use `Cmd` (⌘) key instead:
|
||||
- `Cmd+S` instead of `Ctrl+S`
|
||||
- `Cmd+Enter` instead of `Ctrl+Enter`
|
||||
|
||||
**Code detects platform automatically**:
|
||||
```javascript
|
||||
if (event.ctrlKey || event.metaKey) { // metaKey = Cmd on Mac
|
||||
// Handle shortcut
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Users
|
||||
|
||||
1. **Learn core shortcuts first**: Ctrl+S, Escape, Ctrl+Enter
|
||||
2. **Use shortcuts consistently**: Build muscle memory
|
||||
3. **Combine with mouse**: Use whichever is faster for context
|
||||
4. **Customize if needed**: Add shortcuts for frequent actions
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Document custom shortcuts**: Provide cheat sheet to users
|
||||
2. **Avoid conflicts**: Check existing shortcuts before adding new
|
||||
3. **Provide alternatives**: Always offer mouse-based alternative
|
||||
4. **Test across platforms**: Verify shortcuts work on Windows, Mac, Linux
|
||||
5. **Respect user settings**: Allow disabling shortcuts if needed
|
||||
|
||||
### Example: Shortcut Cheat Sheet
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Add help button that shows shortcuts
|
||||
const helpButton = document.createElement('button');
|
||||
helpButton.textContent = 'Keyboard Shortcuts (?)';
|
||||
helpButton.onclick = () => {
|
||||
alert(`
|
||||
TestDrive-JSUI Keyboard Shortcuts:
|
||||
|
||||
Ctrl+S - Save document
|
||||
Escape - Close editor
|
||||
Ctrl+Enter - Accept changes
|
||||
Tab - Indent
|
||||
|
||||
More shortcuts: /docs/keyboard-shortcuts
|
||||
`);
|
||||
};
|
||||
|
||||
document.body.appendChild(helpButton);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform Differences
|
||||
|
||||
### Windows/Linux
|
||||
|
||||
- Uses `Ctrl` key for shortcuts
|
||||
- `Alt` key for alternative shortcuts
|
||||
- Standard keyboard layout
|
||||
|
||||
### macOS
|
||||
|
||||
- Uses `Cmd` (⌘) key instead of `Ctrl`
|
||||
- `Option` (⌥) key instead of `Alt`
|
||||
- `Control` key rarely used for app shortcuts
|
||||
|
||||
### Detection in Code
|
||||
|
||||
```javascript
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
const modifierPressed = isMac ? event.metaKey : event.ctrlKey;
|
||||
|
||||
if (modifierPressed && event.key === 's') {
|
||||
// Save action
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Shortcuts (Roadmap)
|
||||
|
||||
Planned for future versions:
|
||||
|
||||
### Version 1.1
|
||||
- ✅ `Ctrl+P` - Print document
|
||||
- ✅ `Ctrl+F` - Find & replace
|
||||
- ✅ `Ctrl+B` - Bold text
|
||||
- ✅ `Ctrl+I` - Italic text
|
||||
- ✅ `Ctrl+K` - Insert link
|
||||
- ✅ `Ctrl+Shift+C` - Insert code block
|
||||
|
||||
### Version 1.2
|
||||
- 🔜 `Ctrl+H` - Insert heading
|
||||
- 🔜 `Ctrl+L` - Insert list
|
||||
- 🔜 `Ctrl+Shift+I` - Insert image
|
||||
- 🔜 `Ctrl+G` - Go to line
|
||||
- 🔜 `Ctrl+D` - Duplicate line
|
||||
|
||||
### Version 2.0
|
||||
- 🔜 `Ctrl+/` - Toggle comment
|
||||
- 🔜 `Ctrl+Shift+K` - Delete line
|
||||
- 🔜 `Ctrl+[` / `Ctrl+]` - Indent/unindent
|
||||
- 🔜 `Ctrl+Space` - Autocomplete
|
||||
- 🔜 `F3` - Find next
|
||||
|
||||
**Request features**: Submit suggestions at [GitHub Issues](https://github.com/anthropics/testdrive-jsui/issues)
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Section Editing](section-editing.md)** - Edit sections with keyboard
|
||||
- **[Edit Control](edit-control.md)** - Document actions and shortcuts
|
||||
- **[Accessibility](accessibility.md)** - Keyboard-only operation (coming soon)
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [Configuration](../api/configuration.md)
|
||||
- [Events](../api/events.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
318
docs/features/section-editing.md
Normal file
318
docs/features/section-editing.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Section Editing
|
||||
|
||||
**Click any section to edit it independently**
|
||||
|
||||
Section editing is the core feature of TestDrive-JSUI that enables interactive markdown editing. Each section of your document can be edited independently without affecting other sections.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
TestDrive-JSUI automatically parses your markdown into logical sections and makes each one independently editable. Click any section to open a floating editor with Accept, Cancel, and Reset options.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- ✅ **Non-destructive** - Changes are isolated to individual sections
|
||||
- ✅ **Visual feedback** - Hover effects and editing indicators
|
||||
- ✅ **Easy to use** - Simple click-to-edit interface
|
||||
- ✅ **Undo/redo** - Cancel or reset changes at any time
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Automatic Section Detection
|
||||
|
||||
TestDrive-JSUI splits your markdown into sections using these rules:
|
||||
|
||||
- **Double newlines** create section boundaries
|
||||
- **Headings** start new sections
|
||||
- **Images** are treated as individual sections
|
||||
- **Code blocks** stay together in one section
|
||||
|
||||
Example markdown:
|
||||
```markdown
|
||||
# My Document
|
||||
|
||||
This is the first section.
|
||||
|
||||
## A Heading
|
||||
|
||||
This is the second section.
|
||||
|
||||

|
||||
|
||||
Final section here.
|
||||
```
|
||||
|
||||
This creates **4 sections**:
|
||||
1. Heading + paragraph
|
||||
2. Subheading + paragraph
|
||||
3. Image
|
||||
4. Final paragraph
|
||||
|
||||
### 2. Section States
|
||||
|
||||
Each section has a state:
|
||||
|
||||
| State | Description | Visual |
|
||||
|-------|-------------|--------|
|
||||
| **Original** | Unchanged from initial load | Normal appearance |
|
||||
| **Editing** | Currently being edited | Blue border, floating editor |
|
||||
| **Modified** | Changed but not saved | Pending changes |
|
||||
| **Saved** | Changes accepted | Saved to current document |
|
||||
|
||||
### 3. Visual Feedback
|
||||
|
||||
**Hover Effect**:
|
||||
- Light background highlight
|
||||
- Subtle border
|
||||
- Shows section is clickable
|
||||
|
||||
**Editing State**:
|
||||
- Blue border with glow
|
||||
- Floating editor dialog
|
||||
- Textarea with current content
|
||||
|
||||
---
|
||||
|
||||
## Using Section Editing
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **Click a section** to start editing
|
||||
2. **Edit the markdown** in the textarea
|
||||
3. **Choose an action**:
|
||||
- **✓ Accept** - Save changes to section
|
||||
- **✗ Cancel** - Discard changes
|
||||
- **↺ Reset** - Restore original content
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
While editing:
|
||||
- `Ctrl+Enter` - Accept changes (coming soon)
|
||||
- `Escape` - Cancel editing
|
||||
- `Tab` - Insert tab character
|
||||
|
||||
### Tips
|
||||
|
||||
**Multiple Edits**:
|
||||
- Edit one section at a time
|
||||
- Previous section auto-saves when starting a new edit
|
||||
- All changes stay in memory until you export/save the document
|
||||
|
||||
**Long Sections**:
|
||||
- Textarea expands vertically
|
||||
- Scroll within the editor
|
||||
- Resize handle in floating dialog
|
||||
|
||||
**Image Sections**:
|
||||
- Special image editor with preview
|
||||
- Drag & drop image replacement
|
||||
- Edit alt text inline
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable Section Editing
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
sections: true // Enable section editing (default: true)
|
||||
});
|
||||
```
|
||||
|
||||
### Disable for View-Only Mode
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#viewer',
|
||||
mode: 'view', // No editing allowed
|
||||
sections: false // Disable section detection
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Section Metadata
|
||||
|
||||
Each section has metadata you can access:
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Get section manager
|
||||
const manager = editor.sectionManager;
|
||||
|
||||
// Get all sections
|
||||
const sections = manager.getAllSections();
|
||||
|
||||
sections.forEach(section => {
|
||||
console.log({
|
||||
id: section.id,
|
||||
type: section.type, // 'heading', 'paragraph', 'image', etc.
|
||||
state: section.state, // 'original', 'editing', 'modified', 'saved'
|
||||
hasChanges: section.hasChanges(),
|
||||
isEditing: section.isEditing(),
|
||||
content: section.currentMarkdown
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Section Types
|
||||
|
||||
| Type | Description | Detection |
|
||||
|------|-------------|-----------|
|
||||
| **heading** | h1-h6 headings | Starts with `#` |
|
||||
| **paragraph** | Regular text | Default type |
|
||||
| **image** | Images | `` pattern |
|
||||
| **code** | Code blocks | Fenced with ` ``` ` |
|
||||
| **list** | Lists | Starts with `-` or `1.` |
|
||||
| **quote** | Blockquotes | Starts with `>` |
|
||||
|
||||
### Events
|
||||
|
||||
Listen to section editing events:
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Section manager events
|
||||
editor.sectionManager.on('edit-started', (data) => {
|
||||
console.log('Started editing:', data.sectionId);
|
||||
});
|
||||
|
||||
editor.sectionManager.on('changes-accepted', (data) => {
|
||||
console.log('Accepted changes:', data.sectionId, data.content);
|
||||
});
|
||||
|
||||
editor.sectionManager.on('changes-cancelled', (data) => {
|
||||
console.log('Cancelled edit:', data.sectionId);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Section Editing
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="testdrive-jsui.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Welcome\n\nClick this section to edit it.\n\n## Try It!\n\nClick this section too!',
|
||||
mode: 'edit',
|
||||
sections: true
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### With Event Tracking
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# My Document\n\nEditable content here.'
|
||||
});
|
||||
|
||||
// Track edits
|
||||
let editCount = 0;
|
||||
editor.sectionManager.on('changes-accepted', (data) => {
|
||||
editCount++;
|
||||
console.log(`Edit #${editCount}:`, data.content.substring(0, 50) + '...');
|
||||
});
|
||||
|
||||
// Get final document
|
||||
document.getElementById('export').addEventListener('click', () => {
|
||||
const markdown = editor.getMarkdown();
|
||||
console.log('Final document:', markdown);
|
||||
console.log('Total edits made:', editCount);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sections Not Clickable
|
||||
|
||||
**Problem**: Sections don't respond to clicks
|
||||
|
||||
**Solutions**:
|
||||
1. Check that `mode: 'edit'` (not 'view')
|
||||
2. Ensure `sections: true` in config
|
||||
3. Verify SectionManager and DOMRenderer are loaded
|
||||
4. Check browser console for JavaScript errors
|
||||
|
||||
### Editor Dialog Not Appearing
|
||||
|
||||
**Problem**: Click works but no floating editor shows
|
||||
|
||||
**Solutions**:
|
||||
1. Check z-index conflicts with other elements
|
||||
2. Verify CSS is loaded properly
|
||||
3. Check for JavaScript errors in console
|
||||
4. Ensure marked.js is loaded for markdown parsing
|
||||
|
||||
### Changes Not Persisting
|
||||
|
||||
**Problem**: Edits disappear after accepting
|
||||
|
||||
**Solutions**:
|
||||
1. Click **Accept (✓)** not Cancel
|
||||
2. Check that section state changed to 'saved'
|
||||
3. Use `editor.getMarkdown()` to export current state
|
||||
4. Enable autosave or use save button to persist
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Writers
|
||||
|
||||
1. **Edit small sections** - Break long documents into manageable pieces
|
||||
2. **Use Accept liberally** - Save frequently as you edit
|
||||
3. **Test with Reset** - Preview original vs edited
|
||||
4. **Export regularly** - Download or save your work
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Listen to events** - Track user edits for analytics
|
||||
2. **Validate sections** - Check content before accepting
|
||||
3. **Custom section types** - Extend Section.detectType() for custom logic
|
||||
4. **Batch operations** - Use sectionManager API for bulk updates
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Edit Control](edit-control.md)** - Document-level save/export actions
|
||||
- **[Status Control](status-control.md)** - Track editing progress
|
||||
- **[Keyboard Shortcuts](keyboard-shortcuts.md)** - Quick editing actions
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [Getting Started](../../README.md#quick-start)
|
||||
- [API Reference](../api/section-manager.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
613
docs/features/status-control.md
Normal file
613
docs/features/status-control.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# Status Control
|
||||
|
||||
**Real-time document statistics and change tracking** 📊
|
||||
|
||||
The Status Control provides comprehensive document analytics including word count, character count, reading time estimation, document structure analysis, and change tracking. Monitor your document metrics in real-time as you write and edit. Positioned in the east by default.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Status Control continuously monitors your document and provides detailed statistics about content, structure, and reading metrics. Perfect for writers, editors, and content creators who need to track document metrics.
|
||||
|
||||
### Position
|
||||
- **Default**: East (e) - middle-right side
|
||||
- **Appearance**: 📊 icon when collapsed
|
||||
- **Title**: "Status"
|
||||
|
||||
### Key Features
|
||||
|
||||
- 📊 **Real-time statistics** - Word/character count, reading time
|
||||
- 📈 **Change tracking** - Visual indicators for increases/decreases
|
||||
- 📐 **Document structure** - Paragraphs, headings, lists, images, links
|
||||
- 🔄 **Auto-refresh** - Updates every 10 seconds automatically
|
||||
- 📤 **Export functionality** - Download statistics as JSON
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
statusControl: true // Enable (default: true)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
statusControl: 'e' // East (default), or: nw, ne, se, s, sw, w
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Content Statistics
|
||||
|
||||
#### Word Count
|
||||
Shows total words in the document, calculated by splitting text on whitespace and filtering empty strings.
|
||||
|
||||
**Display**: Large, blue-colored number with change indicator
|
||||
|
||||
**Example**: `1,247 (+23)` - Document has 1,247 words, increased by 23 since last update
|
||||
|
||||
#### Character Count
|
||||
Total characters including spaces.
|
||||
|
||||
**Display**: Formatted with thousand separators
|
||||
|
||||
**Also tracks**: Characters without spaces (internal metric)
|
||||
|
||||
#### Reading Time
|
||||
Estimated reading time in minutes based on average reading speed.
|
||||
|
||||
**Calculation**: Total words ÷ 200 words per minute (configurable)
|
||||
|
||||
**Display**: `5 min (+1)` - Takes 5 minutes to read, increased by 1 minute
|
||||
|
||||
**Tip**: Useful for blog posts, articles, and documentation to estimate reader time investment
|
||||
|
||||
#### Sentence Count
|
||||
Approximate sentence count based on punctuation (., !, ?).
|
||||
|
||||
**Use case**: Track writing complexity and paragraph length
|
||||
|
||||
---
|
||||
|
||||
### 2. Document Structure
|
||||
|
||||
The Status Control analyzes your document structure and counts:
|
||||
|
||||
#### Paragraphs
|
||||
Total `<p>` elements in the rendered HTML.
|
||||
|
||||
**Use case**: Ensure proper document segmentation
|
||||
|
||||
#### Headings
|
||||
Total headings (h1-h6) in the document.
|
||||
|
||||
**Use case**: Verify document outline and hierarchy
|
||||
|
||||
**Tip**: Good documents typically have 1 h1 and logical heading progression
|
||||
|
||||
#### Lists
|
||||
Total lists (ordered and unordered).
|
||||
|
||||
**Use case**: Track enumeration and structured content
|
||||
|
||||
#### Images
|
||||
Total images in the document.
|
||||
|
||||
**Use case**: Monitor visual content distribution
|
||||
|
||||
#### Links
|
||||
Total hyperlinks in the document.
|
||||
|
||||
**Use case**: Track references and external resources
|
||||
|
||||
---
|
||||
|
||||
### 3. Change Tracking
|
||||
|
||||
#### Visual Indicators
|
||||
|
||||
Changes since last update are shown with:
|
||||
|
||||
**Positive changes** (increases):
|
||||
- Green color: `#28a745`
|
||||
- Plus sign: `(+5)`
|
||||
- Indicates growth in that metric
|
||||
|
||||
**Negative changes** (decreases):
|
||||
- Red color: `#dc3545`
|
||||
- Minus sign: `(-3)`
|
||||
- Indicates reduction in that metric
|
||||
|
||||
**No change**: No indicator shown
|
||||
|
||||
#### Example Display
|
||||
|
||||
```
|
||||
Words: 1,247 (+23)
|
||||
Characters: 6,543 (+145)
|
||||
Reading Time: 6 min
|
||||
Sentences: 89 (+2)
|
||||
```
|
||||
|
||||
The green numbers show the document grew by 23 words, 145 characters, and 2 sentences.
|
||||
|
||||
---
|
||||
|
||||
### 4. Actions
|
||||
|
||||
#### 🔄 Refresh
|
||||
Manually refresh statistics immediately.
|
||||
|
||||
**Usage**:
|
||||
- Click "🔄 Refresh" button
|
||||
- Shows "✅ Updated" confirmation for 1 second
|
||||
|
||||
**When to use**:
|
||||
- After making significant edits
|
||||
- When you want immediate feedback
|
||||
- To see updated change indicators
|
||||
|
||||
**Note**: Statistics auto-refresh every 10 seconds, so manual refresh is optional
|
||||
|
||||
#### 📊 Export
|
||||
Export statistics to JSON file for external analysis.
|
||||
|
||||
**Usage**:
|
||||
- Click "📊 Export" button
|
||||
- Downloads: `document-stats-YYYY-MM-DD.json`
|
||||
|
||||
**File contents**:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-16T14:30:52.123Z",
|
||||
"document": {
|
||||
"title": "My Document",
|
||||
"url": "http://localhost:8080/editor.html"
|
||||
},
|
||||
"statistics": {
|
||||
"characters": 6543,
|
||||
"charactersNoSpaces": 5421,
|
||||
"words": 1247,
|
||||
"sentences": 89,
|
||||
"paragraphs": 34,
|
||||
"headings": 12,
|
||||
"lists": 5,
|
||||
"images": 3,
|
||||
"links": 18,
|
||||
"readingTimeMinutes": 6
|
||||
},
|
||||
"metadata": {
|
||||
"wordsPerMinute": 200,
|
||||
"analysisDate": "2025-12-16T14:30:52.123Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Track document evolution over time
|
||||
- Compare statistics across multiple documents
|
||||
- Generate reports for clients or stakeholders
|
||||
- Integrate with external analytics tools
|
||||
|
||||
---
|
||||
|
||||
### 5. Auto-Refresh
|
||||
|
||||
The Status Control automatically refreshes every **10 seconds** to keep statistics current.
|
||||
|
||||
**Behavior**:
|
||||
- Silent background updates
|
||||
- Preserves change tracking between updates
|
||||
- Minimal performance impact
|
||||
- Continues while control is expanded or collapsed
|
||||
|
||||
**Disable auto-refresh**: Not currently supported via config (always enabled)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# My Document\\n\\nThis is a sample paragraph.',
|
||||
controls: {
|
||||
statusControl: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Position (Top-Right)
|
||||
|
||||
```javascript
|
||||
// Move status control to northeast (top-right)
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controlPositions: {
|
||||
statusControl: 'ne' // Top-right corner
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Blog Post Writer Setup
|
||||
|
||||
```javascript
|
||||
// Emphasize word count for blog writing
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
controls: {
|
||||
editControl: true, // Save and export
|
||||
statusControl: true, // Track word count goal
|
||||
contentsControl: false,
|
||||
debugControl: false
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor word count for target goal
|
||||
setInterval(() => {
|
||||
const stats = editor.statusControl.stats;
|
||||
const targetWords = 1500;
|
||||
|
||||
if (stats.words >= targetWords) {
|
||||
console.log(`🎉 Goal reached! ${stats.words} words written.`);
|
||||
} else {
|
||||
const remaining = targetWords - stats.words;
|
||||
console.log(`✍️ Keep writing! ${remaining} words to go.`);
|
||||
}
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### Export Statistics for Reporting
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Programmatically export stats
|
||||
document.getElementById('report-btn').addEventListener('click', () => {
|
||||
// Refresh to get latest stats
|
||||
editor.statusControl.refreshStats();
|
||||
|
||||
// Export to JSON
|
||||
setTimeout(() => {
|
||||
editor.statusControl.exportStats();
|
||||
}, 100);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Properties
|
||||
|
||||
Access via `editor.statusControl`:
|
||||
|
||||
```javascript
|
||||
const statusCtrl = editor.statusControl;
|
||||
|
||||
// Current statistics object
|
||||
statusCtrl.stats = {
|
||||
characters: 6543,
|
||||
charactersNoSpaces: 5421,
|
||||
words: 1247,
|
||||
sentences: 89,
|
||||
paragraphs: 34,
|
||||
headings: 12,
|
||||
lists: 5,
|
||||
images: 3,
|
||||
links: 18,
|
||||
readingTimeMinutes: 6
|
||||
};
|
||||
|
||||
// Previous statistics (for change tracking)
|
||||
statusCtrl.previousStats = { /* ... */ };
|
||||
|
||||
// Last update timestamp
|
||||
statusCtrl.lastUpdateTime = 1702742452123;
|
||||
|
||||
// Reading speed setting (words per minute)
|
||||
statusCtrl.wordsPerMinute = 200; // Adjustable
|
||||
|
||||
// Auto-refresh interval ID
|
||||
statusCtrl.updateInterval = 12345;
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```javascript
|
||||
const statusCtrl = editor.statusControl;
|
||||
|
||||
// Analyze document and update statistics
|
||||
statusCtrl.analyzeDocument();
|
||||
// Returns: stats object
|
||||
|
||||
// Calculate changes since last update
|
||||
const changes = statusCtrl.calculateChanges();
|
||||
// Returns: { words: { current, previous, change, hasChanged }, ... }
|
||||
|
||||
// Format statistics as HTML
|
||||
const html = statusCtrl.formatStatistics();
|
||||
// Returns: HTML string
|
||||
|
||||
// Refresh stats and update display
|
||||
statusCtrl.refreshStats();
|
||||
|
||||
// Export statistics to JSON file
|
||||
statusCtrl.exportStats();
|
||||
|
||||
// Calculate readability score (Flesch Reading Ease)
|
||||
const readability = statusCtrl.calculateReadabilityScore();
|
||||
// Returns: { score: 65, level: 'Standard' }
|
||||
|
||||
// Configuration
|
||||
statusCtrl.config.position = 'e'; // Compass position
|
||||
statusCtrl.config.icon = '📊'; // Panel icon
|
||||
statusCtrl.config.title = 'Status'; // Panel title
|
||||
statusCtrl.isExpanded; // true if expanded
|
||||
```
|
||||
|
||||
### Readability Levels
|
||||
|
||||
The `calculateReadabilityScore()` method returns:
|
||||
|
||||
| Score Range | Level | Description |
|
||||
|-------------|-------|-------------|
|
||||
| 90-100 | Very Easy | 5th grade level |
|
||||
| 80-89 | Easy | 6th grade level |
|
||||
| 70-79 | Fairly Easy | 7th grade level |
|
||||
| 60-69 | Standard | 8th-9th grade level |
|
||||
| 50-59 | Fairly Difficult | 10th-12th grade level |
|
||||
| 30-49 | Difficult | College level |
|
||||
| 0-29 | Very Difficult | College graduate level |
|
||||
|
||||
**Note**: Readability scoring uses simplified Flesch Reading Ease formula with approximated syllable counts.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Reading Speed
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Adjust for faster readers (250 words per minute)
|
||||
editor.statusControl.wordsPerMinute = 250;
|
||||
editor.statusControl.refreshStats();
|
||||
|
||||
// Or slower readers (150 words per minute)
|
||||
editor.statusControl.wordsPerMinute = 150;
|
||||
editor.statusControl.refreshStats();
|
||||
```
|
||||
|
||||
### Programmatic Access
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Get current word count
|
||||
const wordCount = editor.statusControl.stats.words;
|
||||
console.log(`Current word count: ${wordCount}`);
|
||||
|
||||
// Get reading time
|
||||
const readingTime = editor.statusControl.stats.readingTimeMinutes;
|
||||
console.log(`Estimated reading time: ${readingTime} minutes`);
|
||||
|
||||
// Check if document structure is balanced
|
||||
const stats = editor.statusControl.stats;
|
||||
const avgParasPerHeading = stats.paragraphs / stats.headings;
|
||||
console.log(`Average paragraphs per section: ${avgParasPerHeading.toFixed(1)}`);
|
||||
```
|
||||
|
||||
### Change Detection
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Monitor specific changes
|
||||
const original = editor.statusControl.stats.words;
|
||||
|
||||
// ... user edits document ...
|
||||
|
||||
editor.statusControl.refreshStats();
|
||||
const current = editor.statusControl.stats.words;
|
||||
const delta = current - original;
|
||||
|
||||
if (delta > 0) {
|
||||
console.log(`Added ${delta} words`);
|
||||
} else if (delta < 0) {
|
||||
console.log(`Removed ${Math.abs(delta)} words`);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Auto-Refresh Interval
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Stop default 10-second refresh
|
||||
clearInterval(editor.statusControl.updateInterval);
|
||||
|
||||
// Set custom 5-second refresh
|
||||
editor.statusControl.updateInterval = setInterval(() => {
|
||||
editor.statusControl.refreshStats();
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Statistics Not Updating
|
||||
|
||||
**Problem**: Numbers don't change after editing
|
||||
|
||||
**Solutions**:
|
||||
1. Click "🔄 Refresh" button manually
|
||||
2. Check if auto-refresh is running: `editor.statusControl.updateInterval`
|
||||
3. Verify StatusControl is loaded: `typeof StatusControl !== 'undefined'`
|
||||
4. Check browser console for JavaScript errors
|
||||
|
||||
### Incorrect Word Count
|
||||
|
||||
**Problem**: Word count seems wrong
|
||||
|
||||
**Causes & Solutions**:
|
||||
1. **Whitespace handling**: Multiple spaces count as one separator (correct behavior)
|
||||
2. **HTML tags**: Only text content is counted, not HTML markup
|
||||
3. **Code blocks**: Code is included in word count
|
||||
4. **Special characters**: Punctuation is included in words
|
||||
|
||||
**Manual verification**:
|
||||
```javascript
|
||||
// Get exact text being counted
|
||||
const contentArea = document.querySelector('#markitect-content');
|
||||
const text = contentArea.textContent;
|
||||
const words = text.trim().split(/\s+/).filter(w => w.length > 0);
|
||||
console.log('Words:', words.length, words);
|
||||
```
|
||||
|
||||
### Export Button Not Working
|
||||
|
||||
**Problem**: Clicking export does nothing
|
||||
|
||||
**Solutions**:
|
||||
1. Check browser's download settings
|
||||
2. Verify pop-up/download permissions
|
||||
3. Test programmatically: `editor.statusControl.exportStats()`
|
||||
4. Check browser console for errors
|
||||
5. Try different browser
|
||||
|
||||
### Reading Time Seems Wrong
|
||||
|
||||
**Problem**: Estimated reading time is too short/long
|
||||
|
||||
**Solution**: Adjust reading speed
|
||||
```javascript
|
||||
// For technical content (slower reading)
|
||||
editor.statusControl.wordsPerMinute = 150;
|
||||
|
||||
// For light reading (faster)
|
||||
editor.statusControl.wordsPerMinute = 250;
|
||||
|
||||
// Refresh to recalculate
|
||||
editor.statusControl.refreshStats();
|
||||
```
|
||||
|
||||
**Factors affecting reading time**:
|
||||
- Technical complexity
|
||||
- Reader expertise level
|
||||
- Language proficiency
|
||||
- Content density
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Writers
|
||||
|
||||
1. **Set word count goals** - Monitor progress toward target lengths
|
||||
2. **Track reading time** - Ensure articles fit time budgets (e.g., 5-minute reads)
|
||||
3. **Balance structure** - Use heading/paragraph ratios to check organization
|
||||
4. **Export regularly** - Save statistics to track writing productivity over time
|
||||
|
||||
### For Editors
|
||||
|
||||
1. **Monitor changes** - Use change indicators to track editing impact
|
||||
2. **Verify readability** - Check `calculateReadabilityScore()` for target audience
|
||||
3. **Structure analysis** - Ensure proper heading hierarchy and paragraph distribution
|
||||
4. **Compare versions** - Export stats before/after editing to quantify improvements
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Integrate with backend** - Send statistics to server for analytics
|
||||
2. **Custom thresholds** - Set alerts for minimum/maximum word counts
|
||||
3. **Programmatic access** - Use API for automated quality checks
|
||||
4. **Performance** - Monitor update frequency if handling very large documents
|
||||
|
||||
Example integration:
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({ container: '#editor' });
|
||||
|
||||
// Auto-save with statistics
|
||||
editor.on('save', async (data) => {
|
||||
const stats = editor.statusControl.stats;
|
||||
|
||||
await fetch('/api/documents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
markdown: data.markdown,
|
||||
statistics: stats,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### For Large Documents (10,000+ words)
|
||||
|
||||
**Impact**: Auto-refresh every 10 seconds may cause lag
|
||||
|
||||
**Solutions**:
|
||||
1. Increase refresh interval:
|
||||
```javascript
|
||||
clearInterval(editor.statusControl.updateInterval);
|
||||
editor.statusControl.updateInterval = setInterval(() => {
|
||||
editor.statusControl.refreshStats();
|
||||
}, 30000); // 30 seconds instead
|
||||
```
|
||||
|
||||
2. Disable auto-refresh, use manual only:
|
||||
```javascript
|
||||
clearInterval(editor.statusControl.updateInterval);
|
||||
editor.statusControl.updateInterval = null;
|
||||
// User clicks Refresh button when needed
|
||||
```
|
||||
|
||||
3. Lazy calculation - only analyze visible sections
|
||||
|
||||
### Memory Usage
|
||||
|
||||
Statistics use minimal memory (< 1KB). Change tracking stores two copies of stats object. No memory leaks if control is properly destroyed via `destroy()`.
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Edit Control](edit-control.md)** - Save/export document
|
||||
- **[Contents Control](contents-control.md)** - Navigate document structure
|
||||
- **[Section Editing](section-editing.md)** - Triggers statistics updates
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [API Reference](../api/status-control.md)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
832
docs/features/themes.md
Normal file
832
docs/features/themes.md
Normal file
@@ -0,0 +1,832 @@
|
||||
# Themes
|
||||
|
||||
**Visual theming and customization** 🎨
|
||||
|
||||
TestDrive-JSUI provides a flexible theming system that allows you to customize the visual appearance of the editor. Themes control colors, typography, spacing, and visual style of all components. Built on CSS custom properties for easy customization.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The theming system uses CSS custom properties (CSS variables) to define color schemes and styling. Themes are applied as CSS classes, allowing multiple themes to coexist and switch dynamically.
|
||||
|
||||
### Default Theme
|
||||
|
||||
**GitHub Theme**: The default theme inspired by GitHub's design system
|
||||
- Clean, professional appearance
|
||||
- High contrast for readability
|
||||
- Familiar to developers
|
||||
- WCAG AA compliant colors
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Set Theme
|
||||
|
||||
Specify theme when creating editor:
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'github' // Default theme
|
||||
});
|
||||
```
|
||||
|
||||
### Change Theme Dynamically
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'github'
|
||||
});
|
||||
|
||||
// Change theme later
|
||||
editor.applyTheme('custom-dark');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Built-in Themes
|
||||
|
||||
### GitHub Theme (Default)
|
||||
|
||||
**Name**: `github`
|
||||
|
||||
**Description**: Clean, professional theme inspired by GitHub
|
||||
|
||||
**Colors**:
|
||||
```css
|
||||
--github-primary: #0969da /* Primary blue */
|
||||
--github-border: #d0d7de /* Border gray */
|
||||
--github-bg-subtle: #f6f8fa /* Subtle background */
|
||||
--github-fg-default: #1f2328 /* Default text */
|
||||
--github-fg-muted: #656d76 /* Muted text */
|
||||
--github-success: #1a7f37 /* Success green */
|
||||
--github-danger: #d1242f /* Danger red */
|
||||
--github-warning: #9a6700 /* Warning amber */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'github'
|
||||
});
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- High contrast text (WCAG AA)
|
||||
- Subtle hover states
|
||||
- Professional color palette
|
||||
- Optimized for long reading sessions
|
||||
|
||||
**Preview**:
|
||||
- Sections: Light gray hover, blue focus
|
||||
- Controls: White backgrounds, gray borders
|
||||
- Buttons: Blue primary, red danger
|
||||
- Debug panel: Dark theme for code readability
|
||||
|
||||
---
|
||||
|
||||
## Custom Themes
|
||||
|
||||
### Creating a Custom Theme
|
||||
|
||||
**Step 1**: Create CSS file `static/css/themes/custom-theme.css`
|
||||
|
||||
```css
|
||||
/**
|
||||
* Custom Theme
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Define your color palette */
|
||||
--custom-primary: #8b5cf6;
|
||||
--custom-border: #e5e7eb;
|
||||
--custom-bg: #ffffff;
|
||||
--custom-bg-subtle: #f9fafb;
|
||||
--custom-fg: #111827;
|
||||
--custom-fg-muted: #6b7280;
|
||||
}
|
||||
|
||||
/* Editor styles */
|
||||
.theme-custom-theme .markitect-edit-mode {
|
||||
color: var(--custom-fg);
|
||||
background: var(--custom-bg);
|
||||
}
|
||||
|
||||
/* Section styles */
|
||||
.theme-custom-theme .markitect-section:hover {
|
||||
background-color: var(--custom-bg-subtle);
|
||||
border-color: var(--custom-border);
|
||||
}
|
||||
|
||||
.theme-custom-theme .markitect-section.editing {
|
||||
border-color: var(--custom-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
/* Control panels */
|
||||
.theme-custom-theme .markitect-control-panel {
|
||||
background: var(--custom-bg);
|
||||
border: 1px solid var(--custom-border);
|
||||
color: var(--custom-fg);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.theme-custom-theme button {
|
||||
background: var(--custom-primary);
|
||||
color: white;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.theme-custom-theme button:hover {
|
||||
background: #7c3aed; /* Darker shade */
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2**: Load CSS file
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="static/css/themes/custom-theme.css">
|
||||
```
|
||||
|
||||
**Step 3**: Apply theme
|
||||
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'custom-theme'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theme Examples
|
||||
|
||||
### Dark Theme
|
||||
|
||||
```css
|
||||
/**
|
||||
* Dark Theme
|
||||
*/
|
||||
|
||||
:root {
|
||||
--dark-primary: #3b82f6;
|
||||
--dark-border: #374151;
|
||||
--dark-bg: #1f2937;
|
||||
--dark-bg-subtle: #111827;
|
||||
--dark-fg: #f9fafb;
|
||||
--dark-fg-muted: #9ca3af;
|
||||
}
|
||||
|
||||
.theme-dark .markitect-edit-mode {
|
||||
color: var(--dark-fg);
|
||||
background: var(--dark-bg);
|
||||
}
|
||||
|
||||
.theme-dark .markitect-section:hover {
|
||||
background-color: var(--dark-bg-subtle);
|
||||
border-color: var(--dark-border);
|
||||
}
|
||||
|
||||
.theme-dark .markitect-section.editing {
|
||||
border-color: var(--dark-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.theme-dark .markitect-control-panel {
|
||||
background: var(--dark-bg-subtle);
|
||||
border: 1px solid var(--dark-border);
|
||||
color: var(--dark-fg);
|
||||
}
|
||||
|
||||
.theme-dark button {
|
||||
background: var(--dark-primary);
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'dark'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Minimal Theme
|
||||
|
||||
```css
|
||||
/**
|
||||
* Minimal Theme
|
||||
*/
|
||||
|
||||
:root {
|
||||
--minimal-primary: #000000;
|
||||
--minimal-border: #e0e0e0;
|
||||
--minimal-bg: #ffffff;
|
||||
--minimal-fg: #000000;
|
||||
}
|
||||
|
||||
.theme-minimal .markitect-edit-mode {
|
||||
color: var(--minimal-fg);
|
||||
background: var(--minimal-bg);
|
||||
font-family: 'Georgia', serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.theme-minimal .markitect-section:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.theme-minimal .markitect-section.editing {
|
||||
border-color: var(--minimal-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-minimal .markitect-control-panel {
|
||||
background: var(--minimal-bg);
|
||||
border: 1px solid var(--minimal-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-minimal button {
|
||||
background: var(--minimal-primary);
|
||||
color: white;
|
||||
border-radius: 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'minimal'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### High Contrast Theme
|
||||
|
||||
```css
|
||||
/**
|
||||
* High Contrast Theme (Accessibility)
|
||||
*/
|
||||
|
||||
:root {
|
||||
--hc-primary: #0000ff;
|
||||
--hc-border: #000000;
|
||||
--hc-bg: #ffffff;
|
||||
--hc-fg: #000000;
|
||||
--hc-success: #008000;
|
||||
--hc-danger: #ff0000;
|
||||
}
|
||||
|
||||
.theme-high-contrast .markitect-edit-mode {
|
||||
color: var(--hc-fg);
|
||||
background: var(--hc-bg);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.theme-high-contrast .markitect-section:hover {
|
||||
background-color: #ffff00; /* Yellow highlight */
|
||||
border: 2px solid var(--hc-border);
|
||||
}
|
||||
|
||||
.theme-high-contrast .markitect-section.editing {
|
||||
border: 3px solid var(--hc-primary);
|
||||
}
|
||||
|
||||
.theme-high-contrast .markitect-control-panel {
|
||||
background: var(--hc-bg);
|
||||
border: 2px solid var(--hc-border);
|
||||
}
|
||||
|
||||
.theme-high-contrast button {
|
||||
background: var(--hc-primary);
|
||||
color: white;
|
||||
border: 2px solid var(--hc-border);
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```javascript
|
||||
new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'high-contrast'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theme Components
|
||||
|
||||
Themes can style these components:
|
||||
|
||||
### 1. Editor Container
|
||||
|
||||
**Classes**: `.markitect-edit-mode`, `.markitect-view-mode`
|
||||
|
||||
**Styles**:
|
||||
- Background color
|
||||
- Text color
|
||||
- Font family
|
||||
- Line height
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom .markitect-edit-mode {
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 1.7;
|
||||
color: #1a1a1a;
|
||||
background: #fafafa;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Sections
|
||||
|
||||
**Classes**: `.markitect-section`, `.markitect-section:hover`, `.markitect-section.editing`
|
||||
|
||||
**Styles**:
|
||||
- Hover background
|
||||
- Border colors
|
||||
- Focus state
|
||||
- Shadow effects
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom .markitect-section {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-custom .markitect-section:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.theme-custom .markitect-section.editing {
|
||||
background: white;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Control Panels
|
||||
|
||||
**Classes**: `.markitect-control-panel`, `.edit-control`, `.status-control`, `.contents-control`, `.debug-control`
|
||||
|
||||
**Styles**:
|
||||
- Panel background
|
||||
- Border style
|
||||
- Shadow
|
||||
- Text colors
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom .markitect-control-panel {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Buttons
|
||||
|
||||
**Classes**: `.control-button`, `.control-button.danger`, `.control-button.success`
|
||||
|
||||
**Styles**:
|
||||
- Button colors
|
||||
- Hover states
|
||||
- Active states
|
||||
- Variants
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.theme-custom button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.theme-custom button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Debug Panel
|
||||
|
||||
**Classes**: `.markitect-debug-panel`, `.markitect-debug-error`, `.markitect-debug-warning`
|
||||
|
||||
**Styles**:
|
||||
- Panel background (usually dark)
|
||||
- Message colors
|
||||
- Font (monospace)
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom .markitect-debug-panel {
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.theme-custom .markitect-debug-error {
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
.theme-custom .markitect-debug-warning {
|
||||
color: #e5c07b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Table of Contents
|
||||
|
||||
**Classes**: `.contents-control`, `.toc-item`, `.toc-item:hover`
|
||||
|
||||
**Styles**:
|
||||
- TOC item colors
|
||||
- Hover effects
|
||||
- Active heading
|
||||
- Indentation
|
||||
|
||||
**Example**:
|
||||
```css
|
||||
.theme-custom .toc-item {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.theme-custom .toc-item:hover {
|
||||
background: #e8f4f8;
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.theme-custom .toc-item.active {
|
||||
background: #0077cc;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Theming
|
||||
|
||||
### CSS Variables
|
||||
|
||||
Use CSS custom properties for flexible theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Color palette */
|
||||
--color-primary: #3b82f6;
|
||||
--color-secondary: #8b5cf6;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
|
||||
/* Neutral colors */
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
|
||||
/* Effects */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-base: 0.2s ease;
|
||||
--transition-slow: 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-custom .markitect-section {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--spacing-xs);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.theme-custom button {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--font-size-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Theme Switcher
|
||||
|
||||
Implement dynamic theme switching:
|
||||
|
||||
```javascript
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: 'github'
|
||||
});
|
||||
|
||||
// Create theme switcher UI
|
||||
const themeSwitcher = document.createElement('select');
|
||||
themeSwitcher.innerHTML = `
|
||||
<option value="github">GitHub</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="high-contrast">High Contrast</option>
|
||||
`;
|
||||
|
||||
themeSwitcher.addEventListener('change', (e) => {
|
||||
editor.applyTheme(e.target.value);
|
||||
|
||||
// Save preference
|
||||
localStorage.setItem('preferred-theme', e.target.value);
|
||||
});
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('preferred-theme');
|
||||
if (savedTheme) {
|
||||
editor.applyTheme(savedTheme);
|
||||
themeSwitcher.value = savedTheme;
|
||||
}
|
||||
|
||||
document.body.appendChild(themeSwitcher);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### System Theme Detection
|
||||
|
||||
Detect and apply system dark mode preference:
|
||||
|
||||
```javascript
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
theme: prefersDark ? 'dark' : 'github'
|
||||
});
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
editor.applyTheme(e.matches ? 'dark' : 'github');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Per-Component Theming
|
||||
|
||||
Theme individual components differently:
|
||||
|
||||
```css
|
||||
.theme-custom .edit-control {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.theme-custom .status-control {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
.theme-custom .contents-control {
|
||||
background: #eff6ff;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.theme-custom .debug-control {
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Theme Creators
|
||||
|
||||
1. **Use CSS variables**: Define colors centrally for consistency
|
||||
2. **Test contrast**: Ensure WCAG AA compliance (4.5:1 minimum)
|
||||
3. **Consider dark mode**: Provide both light and dark variants
|
||||
4. **Test all states**: Hover, focus, active, disabled
|
||||
5. **Responsive**: Test on different screen sizes
|
||||
6. **Accessibility**: High contrast mode, focus indicators
|
||||
|
||||
### For Users
|
||||
|
||||
1. **Choose readable fonts**: Sans-serif for UI, serif/mono for content
|
||||
2. **Adjust for lighting**: Light themes for bright environments, dark for dim
|
||||
3. **Save preferences**: Use localStorage to persist theme choice
|
||||
4. **Test compatibility**: Verify theme works with all controls
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
### High Contrast
|
||||
|
||||
Themes should provide high contrast variants:
|
||||
|
||||
```css
|
||||
@media (prefers-contrast: high) {
|
||||
.theme-github {
|
||||
--github-fg-default: #000000;
|
||||
--github-bg-subtle: #ffffff;
|
||||
--github-border: #000000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reduced Motion
|
||||
|
||||
Respect user's motion preferences:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-custom .markitect-section {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.theme-custom button {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Color Blind Friendly
|
||||
|
||||
Use patterns in addition to color:
|
||||
|
||||
```css
|
||||
.theme-custom .markitect-debug-error::before {
|
||||
content: '❌ '; /* Icon in addition to red color */
|
||||
}
|
||||
|
||||
.theme-custom .markitect-debug-warning::before {
|
||||
content: '⚠️ '; /* Icon in addition to yellow color */
|
||||
}
|
||||
|
||||
.theme-custom .markitect-debug-success::before {
|
||||
content: '✅ '; /* Icon in addition to green color */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Applied
|
||||
|
||||
**Problem**: Theme styles don't appear
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check CSS file loaded**:
|
||||
```html
|
||||
<link rel="stylesheet" href="static/css/themes/custom-theme.css">
|
||||
```
|
||||
|
||||
2. **Verify theme name**:
|
||||
```javascript
|
||||
console.log(editor.config.theme); // Should match CSS class
|
||||
```
|
||||
|
||||
3. **Check CSS selector specificity**:
|
||||
```css
|
||||
/* More specific */
|
||||
.theme-custom .markitect-section { }
|
||||
|
||||
/* Less specific - might not work */
|
||||
.markitect-section { }
|
||||
```
|
||||
|
||||
4. **Clear browser cache**: Force refresh with Ctrl+F5
|
||||
|
||||
### Theme Partially Applied
|
||||
|
||||
**Problem**: Some components themed, others not
|
||||
|
||||
**Solution**: Ensure all component classes are styled:
|
||||
|
||||
```css
|
||||
.theme-custom .markitect-edit-mode { /* Editor */ }
|
||||
.theme-custom .markitect-section { /* Sections */ }
|
||||
.theme-custom .markitect-control-panel { /* All controls */ }
|
||||
.theme-custom .edit-control { /* Specific control */ }
|
||||
.theme-custom button { /* Buttons */ }
|
||||
```
|
||||
|
||||
### CSS Variables Not Working
|
||||
|
||||
**Problem**: CSS variables show default values
|
||||
|
||||
**Solution**: Define variables in `:root` or `.theme-*` scope:
|
||||
|
||||
```css
|
||||
/* Correct */
|
||||
:root {
|
||||
--custom-primary: #3b82f6;
|
||||
}
|
||||
|
||||
/* Or scoped to theme */
|
||||
.theme-custom {
|
||||
--custom-primary: #3b82f6;
|
||||
}
|
||||
|
||||
/* Use in components */
|
||||
.theme-custom .markitect-section.editing {
|
||||
border-color: var(--custom-primary);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Themes (Roadmap)
|
||||
|
||||
Planned themes for future versions:
|
||||
|
||||
### Version 1.1
|
||||
- ✅ **Dracula**: Popular dark theme
|
||||
- ✅ **Solarized**: Light and dark variants
|
||||
- ✅ **Monokai**: Vibrant syntax colors
|
||||
- ✅ **Nord**: Arctic-inspired palette
|
||||
|
||||
### Version 1.2
|
||||
- 🔜 **Material Design**: Google's design system
|
||||
- 🔜 **Tailwind**: Tailwind CSS inspired
|
||||
- 🔜 **Bootstrap**: Bootstrap color scheme
|
||||
- 🔜 **Cyberpunk**: Neon aesthetic
|
||||
|
||||
### Version 2.0
|
||||
- 🔜 **Theme builder**: Visual theme creator
|
||||
- 🔜 **Theme marketplace**: Share custom themes
|
||||
- 🔜 **Live preview**: Preview themes before applying
|
||||
- 🔜 **Theme export**: Export theme as CSS
|
||||
|
||||
**Request themes**: Submit suggestions at [GitHub Issues](https://github.com/anthropics/testdrive-jsui/issues)
|
||||
|
||||
---
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Configuration](../api/configuration.md)** - Theme configuration options
|
||||
- **[Customization](customization.md)** - Advanced customization (coming soon)
|
||||
- **[Accessibility](accessibility.md)** - Accessible themes (coming soon)
|
||||
|
||||
---
|
||||
|
||||
**See Also**:
|
||||
- [CSS Variables Reference](../api/css-variables.md) (coming soon)
|
||||
- [Theme Gallery](../../examples/themes/) (coming soon)
|
||||
- [Examples](../../examples/)
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-16
|
||||
37
docs/prototypes/README.md
Normal file
37
docs/prototypes/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# TestDrive-JSUI Prototypes
|
||||
|
||||
This directory contains historical HTML prototypes from the development of the control system architecture.
|
||||
|
||||
## Files
|
||||
|
||||
- **AllControlsRudimentary.html** - Early prototype showing all control panels
|
||||
- **ControlFooter.html** - Footer control prototype
|
||||
- **DebugControlContent.html** - Reference implementation for enhanced ControlBase behavior
|
||||
- **StatusPsychadelic.html** - Status control visual prototype
|
||||
|
||||
## Historical Context
|
||||
|
||||
These prototypes were created during the development of the enhanced ControlBase architecture (documented in `IMPLEMENTATION_NOTES.md`). They served as reference implementations for:
|
||||
|
||||
- Icon-only collapsed state
|
||||
- Expand/drag functionality
|
||||
- Bottom-left corner resize
|
||||
- Collapse with position restoration
|
||||
- Header toggle for content visibility
|
||||
|
||||
## Status
|
||||
|
||||
These files are **archived for historical reference only**. The implemented code is now in:
|
||||
- `js/controls/control-base.js` - Base control class
|
||||
- `js/controls/edit-control.js` - Edit panel
|
||||
- `js/controls/debug-control.js` - Debug panel
|
||||
- `js/controls/status-control.js` - Status indicator
|
||||
- `js/controls/contents-control.js` - Table of contents
|
||||
|
||||
## Notes
|
||||
|
||||
The DebugControlContent.html prototype was specifically referenced in the IMPLEMENTATION_NOTES.md as the source for the advanced panel behavior patterns that were implemented in the ControlBase class.
|
||||
|
||||
---
|
||||
|
||||
*Archived: December 16, 2025*
|
||||
205
examples/README.md
Normal file
205
examples/README.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# TestDrive-JSUI Examples
|
||||
|
||||
This directory contains examples demonstrating TestDrive-JSUI as a standalone JavaScript library.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Examples
|
||||
|
||||
### `standalone.html` - Proof of Concept
|
||||
|
||||
**Purpose**: Minimal proof that TestDrive-JSUI can work as a pure JavaScript library.
|
||||
|
||||
**Features**:
|
||||
- ✅ Runs directly in browser (no server needed)
|
||||
- ✅ Markdown rendering using marked.js
|
||||
- ✅ Save/load to browser localStorage
|
||||
- ✅ Download markdown files
|
||||
- ✅ Zero dependencies on Python/Ruby/Java
|
||||
|
||||
**How to Use**:
|
||||
```bash
|
||||
# Option 1: Open directly in browser
|
||||
open examples/standalone.html
|
||||
|
||||
# Option 2: From file system (works!)
|
||||
# Just double-click the file
|
||||
|
||||
# Option 3: Serve with any static server
|
||||
python3 -m http.server 8000
|
||||
# Then visit: http://localhost:8000/examples/standalone.html
|
||||
```
|
||||
|
||||
**What It Demonstrates**:
|
||||
- JavaScript can handle all rendering
|
||||
- No backend is required for core functionality
|
||||
- Language adapters (Python/Ruby/Java) are truly optional
|
||||
|
||||
---
|
||||
|
||||
### `full-editor.html` - Complete Library Demo
|
||||
|
||||
**Purpose**: Demonstrates the full TestDriveJSUI library with all features.
|
||||
|
||||
**Features**:
|
||||
- ✅ **Full Library Integration**: Uses complete `TestDriveJSUI` class
|
||||
- ✅ **Section-Based Editing**: Click sections to edit them independently
|
||||
- ✅ **Interactive Controls**: Floating control panel with document operations
|
||||
- ✅ **Event System**: Listen to save, content-changed, and other events
|
||||
- ✅ **Multiple Modes**: Switch between edit and view modes
|
||||
- ✅ **Auto-save**: Optional automatic saving (currently disabled)
|
||||
- ✅ **Keyboard Shortcuts**: Ctrl+S to save, Escape to close editor
|
||||
- ✅ **Status Bar**: Real-time document statistics
|
||||
- ✅ **Image Editing**: Advanced image editing with drag & drop
|
||||
|
||||
**How to Use**:
|
||||
```bash
|
||||
# Serve with any static server (required for proper module loading)
|
||||
python3 -m http.server 8000
|
||||
# Then visit: http://localhost:8000/examples/full-editor.html
|
||||
```
|
||||
|
||||
**What It Demonstrates**:
|
||||
- Complete TestDriveJSUI API usage
|
||||
- Integration of all components (SectionManager, DOMRenderer, DocumentControls)
|
||||
- Event-driven architecture
|
||||
- Mode switching (edit/view)
|
||||
- Document operations (save, load, download, reset)
|
||||
- Real-time status updates
|
||||
|
||||
**API Example from Demo**:
|
||||
```javascript
|
||||
// Initialize editor
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
markdown: '# Hello World',
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
autosave: false,
|
||||
shortcuts: true
|
||||
});
|
||||
|
||||
// Listen to events
|
||||
editor.on('save', (data) => {
|
||||
console.log('Saved:', data.markdown);
|
||||
});
|
||||
|
||||
// Get/set content
|
||||
const markdown = editor.getMarkdown();
|
||||
editor.setMarkdown('# Updated content');
|
||||
|
||||
// Get status
|
||||
const status = editor.getStatus();
|
||||
console.log('Sections:', status.totalSections);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Architecture Validation
|
||||
|
||||
These examples validate the JavaScript-first architecture:
|
||||
|
||||
### ✅ Implemented Features
|
||||
- Markdown parsing (marked.js)
|
||||
- HTML rendering
|
||||
- UI display and styling
|
||||
- Content management
|
||||
- Save/load (localStorage)
|
||||
- File download
|
||||
- **Interactive editing UI** ✅
|
||||
- **Section-based editing** ✅
|
||||
- **Control panels (edit, debug, status)** ✅
|
||||
- **Keyboard shortcuts** ✅
|
||||
- **Themes** ✅
|
||||
- **Event system** ✅
|
||||
|
||||
### 🚧 Future Enhancements
|
||||
- Plugin system
|
||||
- Additional themes
|
||||
- Collaborative editing
|
||||
- Cloud storage adapters
|
||||
- Mobile-optimized UI
|
||||
|
||||
### 🔧 What Backend Adapters Provide (Optional)
|
||||
- Asset serving at scale
|
||||
- Server-side storage
|
||||
- User authentication
|
||||
- Multi-user collaboration
|
||||
- Testing infrastructure
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### 1. Try the Proof of Concept
|
||||
```bash
|
||||
# Open standalone.html in any browser
|
||||
open examples/standalone.html
|
||||
|
||||
# Test basic functionality:
|
||||
# - Markdown rendering
|
||||
# - Save/load/download buttons
|
||||
```
|
||||
|
||||
### 2. Explore the Full Editor
|
||||
```bash
|
||||
# Serve the examples directory
|
||||
cd examples
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Open in browser:
|
||||
# http://localhost:8000/full-editor.html
|
||||
|
||||
# Test advanced features:
|
||||
# - Click sections to edit
|
||||
# - Use toolbar buttons
|
||||
# - Try keyboard shortcuts (Ctrl+S, Escape)
|
||||
# - Switch between edit/view modes
|
||||
```
|
||||
|
||||
### 3. Integrate Into Your Project
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="path/to/testdrive-jsui.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Start writing...',
|
||||
mode: 'edit'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
**Performance**: Loading from CDN (marked.js) means internet connection required. For fully offline use, download marked.js locally.
|
||||
|
||||
**Browser Support**: Works in all modern browsers (Chrome, Firefox, Safari, Edge). Requires ES6 support.
|
||||
|
||||
**File Size**: The standalone example loads individual JS files. Production version would use bundled `testdrive-jsui.min.js` (~50KB).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing Examples
|
||||
|
||||
Want to add an example? Please ensure it:
|
||||
1. Works standalone (no complex build required)
|
||||
2. Demonstrates a specific use case
|
||||
3. Includes inline documentation
|
||||
4. Works by just opening in browser
|
||||
|
||||
---
|
||||
|
||||
**Status**: Full Implementation Complete ✅
|
||||
**Last Updated**: 2025-12-16
|
||||
**Library Version**: 1.0.0
|
||||
1200
examples/full-editor-standalone.html
Normal file
1200
examples/full-editor-standalone.html
Normal file
File diff suppressed because it is too large
Load Diff
535
examples/full-editor.html
Normal file
535
examples/full-editor.html
Normal file
@@ -0,0 +1,535 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestDrive-JSUI - Full Editor Demo</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #24292e;
|
||||
background: #f6f8fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #0366d6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #586069;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e8f5e9;
|
||||
border: 1px solid #a5d6a7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
background: #0366d6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toolbar button:hover {
|
||||
background: #0256c7;
|
||||
}
|
||||
|
||||
.toolbar button.secondary {
|
||||
background: #6a737d;
|
||||
}
|
||||
|
||||
.toolbar button.secondary:hover {
|
||||
background: #586069;
|
||||
}
|
||||
|
||||
.toolbar button.success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.toolbar button.success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* GitHub theme styles for rendered markdown */
|
||||
#editor-container h1,
|
||||
#editor-container h2,
|
||||
#editor-container h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#editor-container h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#editor-container h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#editor-container p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#editor-container ul,
|
||||
#editor-container ol {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
#editor-container code {
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
#editor-container pre {
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#editor-container blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
color: #6a737d;
|
||||
padding-left: 1em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
color: #586069;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: white;
|
||||
padding: 12px 20px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #586069;
|
||||
}
|
||||
|
||||
.status-bar .stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-bar .label {
|
||||
font-weight: 600;
|
||||
color: #24292e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📝 TestDrive-JSUI Full Editor</h1>
|
||||
<p>
|
||||
Complete interactive markdown editor demonstrating the full TestDrive-JSUI library.
|
||||
Click any section to edit, use the toolbar for document operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>✨ Full Library Integration:</strong> This demo uses the complete TestDriveJSUI class
|
||||
with all components (SectionManager, DOMRenderer, DocumentControls) working together.
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button onclick="saveDocument()" class="success">💾 Save Document</button>
|
||||
<button onclick="loadDocument()">📂 Load Saved</button>
|
||||
<button onclick="downloadDocument()">⬇️ Download .md</button>
|
||||
<button onclick="showStatus()">📊 Show Status</button>
|
||||
<button onclick="resetDocument()" class="secondary">🔄 Reset All</button>
|
||||
<button onclick="switchToViewMode()" class="secondary">👁️ View Mode</button>
|
||||
<button onclick="switchToEditMode()" class="secondary">✏️ Edit Mode</button>
|
||||
</div>
|
||||
|
||||
<div id="editor-container">
|
||||
<!-- Editor will be initialized here -->
|
||||
</div>
|
||||
|
||||
<div class="status-bar" id="status-bar">
|
||||
<div class="stat">
|
||||
<span class="label">Mode:</span>
|
||||
<span id="status-mode">edit</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Sections:</span>
|
||||
<span id="status-sections">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Words:</span>
|
||||
<span id="status-words">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Characters:</span>
|
||||
<span id="status-chars">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>TestDrive-JSUI • JavaScript-First Architecture • Full Featured Editor</p>
|
||||
<p><small>Uses: marked.js (markdown) + TestDriveJSUI library</small></p>
|
||||
</div>
|
||||
|
||||
<!-- External Dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- TestDrive-JSUI Core Components -->
|
||||
<script src="../js/core/debug-system.js"></script>
|
||||
<script src="../js/core/section-manager.js"></script>
|
||||
<script src="../js/components/dom-renderer.js"></script>
|
||||
<script src="../js/components/document-controls.js"></script>
|
||||
|
||||
<!-- TestDrive-JSUI Main Library -->
|
||||
<script src="../js/testdrive-jsui.js"></script>
|
||||
|
||||
<!-- Application Script -->
|
||||
<script>
|
||||
// Sample document content
|
||||
const defaultMarkdown = `# TestDrive-JSUI Full Editor
|
||||
|
||||
## What is This?
|
||||
|
||||
This is the **complete TestDrive-JSUI library** in action. Unlike the standalone proof of concept, this demo uses the full \`TestDriveJSUI\` class that wraps all components together.
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **Section-Based Editing**: Click any section to edit it independently
|
||||
- ✅ **Interactive Controls**: Floating control panel for document operations
|
||||
- ✅ **Event System**: Listen to editor events for integration
|
||||
- ✅ **Multiple Modes**: Switch between edit and view modes
|
||||
- ✅ **Auto-save**: Optional automatic saving to localStorage
|
||||
- ✅ **Keyboard Shortcuts**: Ctrl+S to save, Escape to close editor
|
||||
|
||||
## Architecture
|
||||
|
||||
### JavaScript-First Design
|
||||
|
||||
TestDrive-JSUI is built with a JavaScript-first architecture:
|
||||
|
||||
1. **Core Library**: Pure JavaScript, works standalone
|
||||
2. **Components**: Modular design (SectionManager, DOMRenderer, DocumentControls)
|
||||
3. **Language Adapters**: Python, Ruby, Java adapters are optional helpers
|
||||
|
||||
### How It Works
|
||||
|
||||
\`\`\`javascript
|
||||
// Initialize the editor
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
markdown: '# Hello World',
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
autosave: false
|
||||
});
|
||||
|
||||
// Listen to events
|
||||
editor.on('save', (data) => {
|
||||
console.log('Document saved:', data.markdown);
|
||||
});
|
||||
|
||||
// Get current content
|
||||
const markdown = editor.getMarkdown();
|
||||
\`\`\`
|
||||
|
||||
## Try It Out!
|
||||
|
||||
### Editing Sections
|
||||
|
||||
Click any section above or below to start editing. You'll see:
|
||||
|
||||
- **Textarea**: Edit the markdown content
|
||||
- **Accept Button (✓)**: Save your changes
|
||||
- **Cancel Button (✗)**: Discard changes
|
||||
- **Reset Button (↺)**: Restore original content
|
||||
|
||||
### Document Operations
|
||||
|
||||
Use the toolbar buttons:
|
||||
|
||||
- **Save**: Saves to browser localStorage
|
||||
- **Load**: Loads previously saved document
|
||||
- **Download**: Downloads as .md file
|
||||
- **Status**: Shows document statistics
|
||||
- **Reset All**: Restores all sections to original content
|
||||
- **View/Edit Mode**: Toggle between modes
|
||||
|
||||
## Code Example
|
||||
|
||||
Here's how easy it is to integrate TestDrive-JSUI:
|
||||
|
||||
\`\`\`html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="testdrive-jsui.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
|
||||
<script>
|
||||
const editor = new TestDriveJSUI({
|
||||
container: '#editor',
|
||||
markdown: '# Start writing...',
|
||||
mode: 'edit'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
\`\`\`
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor Options
|
||||
|
||||
- \`container\` (required): CSS selector or DOM element
|
||||
- \`markdown\` (optional): Initial markdown content
|
||||
- \`mode\` (optional): 'edit' or 'view' (default: 'edit')
|
||||
- \`theme\` (optional): Theme name (default: 'github')
|
||||
- \`autosave\` (optional): Enable auto-save (default: false)
|
||||
- \`shortcuts\` (optional): Enable keyboard shortcuts (default: true)
|
||||
- \`sections\` (optional): Enable section-based editing (default: true)
|
||||
- \`debug\` (optional): Enable debug mode (default: false)
|
||||
|
||||
### Methods
|
||||
|
||||
- \`getMarkdown()\`: Get current document content
|
||||
- \`setMarkdown(markdown)\`: Set document content
|
||||
- \`getStatus()\`: Get editor status (sections, words, etc.)
|
||||
- \`save()\`: Trigger save event
|
||||
- \`download(filename)\`: Download as markdown file
|
||||
- \`resetAll()\`: Reset all sections to original content
|
||||
- \`destroy()\`: Clean up and destroy editor
|
||||
|
||||
### Events
|
||||
|
||||
- \`initialized\`: Editor finished initializing
|
||||
- \`save\`: Document save triggered
|
||||
- \`content-changed\`: Content was modified
|
||||
- \`autosave\`: Auto-save occurred
|
||||
- \`download\`: Document downloaded
|
||||
- \`reset\`: Document reset
|
||||
- \`destroyed\`: Editor destroyed
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Users
|
||||
|
||||
- **Intuitive**: Click to edit, visual feedback
|
||||
- **Fast**: No page reloads, instant updates
|
||||
- **Reliable**: Undo/reset functionality
|
||||
- **Flexible**: Works in any browser, no installation
|
||||
|
||||
### For Developers
|
||||
|
||||
- **Easy Integration**: Simple API, minimal setup
|
||||
- **Customizable**: Event system for custom behavior
|
||||
- **Extensible**: Plugin architecture (coming soon)
|
||||
- **Language Agnostic**: Use with any backend
|
||||
|
||||
---
|
||||
|
||||
**Ready to build?** Check out the [documentation](../README.md) or browse the [source code](../js/) to learn more!
|
||||
`;
|
||||
|
||||
// Global editor instance
|
||||
let editor;
|
||||
|
||||
// Initialize editor on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Create editor instance
|
||||
editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
markdown: defaultMarkdown,
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
autosave: false,
|
||||
shortcuts: true,
|
||||
sections: true,
|
||||
debug: false
|
||||
});
|
||||
|
||||
// Set up event listeners
|
||||
editor.on('initialized', (data) => {
|
||||
console.log('✅ Editor initialized:', data);
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
editor.on('save', (data) => {
|
||||
console.log('💾 Document saved:', data.markdown.length, 'characters');
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
editor.on('content-changed', (data) => {
|
||||
console.log('📝 Content changed');
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
editor.on('reset', () => {
|
||||
console.log('🔄 Document reset');
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
// Update status bar initially
|
||||
updateStatusBar();
|
||||
|
||||
// Update status bar periodically
|
||||
setInterval(updateStatusBar, 2000);
|
||||
|
||||
console.log('✅ TestDrive-JSUI Full Editor initialized!');
|
||||
console.log('📝 Global editor instance available as window.editor');
|
||||
});
|
||||
|
||||
// Toolbar functions
|
||||
function saveDocument() {
|
||||
editor.save();
|
||||
alert('✅ Document saved to browser localStorage!');
|
||||
}
|
||||
|
||||
function loadDocument() {
|
||||
const loaded = editor.loadFromLocalStorage();
|
||||
if (loaded) {
|
||||
alert('📂 Document loaded from localStorage!');
|
||||
} else {
|
||||
alert('ℹ️ No saved document found.');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadDocument() {
|
||||
const filename = prompt('Enter filename:', 'document.md');
|
||||
if (filename) {
|
||||
editor.download(filename);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus() {
|
||||
const status = editor.getStatus();
|
||||
const message = [
|
||||
`📊 Document Status`,
|
||||
``,
|
||||
`Mode: ${status.mode}`,
|
||||
`Total Sections: ${status.totalSections || 0}`,
|
||||
`Currently Editing: ${status.editingSections || 0}`,
|
||||
`Modified Sections: ${status.modifiedSections || 0}`,
|
||||
`Word Count: ${status.wordCount}`,
|
||||
`Character Count: ${status.characterCount}`
|
||||
].join('\n');
|
||||
alert(message);
|
||||
}
|
||||
|
||||
function resetDocument() {
|
||||
if (confirm('Reset all sections to original content?\n\nThis cannot be undone.')) {
|
||||
editor.resetAll();
|
||||
alert('🔄 Document reset to original content!');
|
||||
}
|
||||
}
|
||||
|
||||
function switchToViewMode() {
|
||||
const markdown = editor.getMarkdown();
|
||||
editor.destroy();
|
||||
editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
markdown: markdown,
|
||||
mode: 'view',
|
||||
theme: 'github'
|
||||
});
|
||||
updateStatusBar();
|
||||
alert('👁️ Switched to view mode. Click "Edit Mode" to return.');
|
||||
}
|
||||
|
||||
function switchToEditMode() {
|
||||
const markdown = editor.getMarkdown();
|
||||
editor.destroy();
|
||||
editor = new TestDriveJSUI({
|
||||
container: '#editor-container',
|
||||
markdown: markdown,
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
autosave: false,
|
||||
shortcuts: true
|
||||
});
|
||||
updateStatusBar();
|
||||
alert('✏️ Switched to edit mode. Click sections to edit them.');
|
||||
}
|
||||
|
||||
function updateStatusBar() {
|
||||
const status = editor.getStatus();
|
||||
document.getElementById('status-mode').textContent = status.mode;
|
||||
document.getElementById('status-sections').textContent = status.totalSections || 0;
|
||||
document.getElementById('status-words').textContent = status.wordCount || 0;
|
||||
document.getElementById('status-chars').textContent = status.characterCount || 0;
|
||||
}
|
||||
|
||||
// Expose editor for debugging
|
||||
window.editor = editor;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
434
examples/html-generator-demo.html
Normal file
434
examples/html-generator-demo.html
Normal file
@@ -0,0 +1,434 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HTML Generator Demo - TestDrive-JSUI</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #24292e;
|
||||
background: #f6f8fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #0366d6;
|
||||
margin-bottom: 15px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #586069;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #24292e;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.section p {
|
||||
margin-bottom: 15px;
|
||||
color: #586069;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #d1d5da;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #0366d6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0256c7;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #6a737d;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #586069;
|
||||
}
|
||||
|
||||
.output {
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #d1d5da;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e8f5e9;
|
||||
border: 1px solid #a5d6a7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comparison-card {
|
||||
background: #f6f8fa;
|
||||
border: 2px solid #d1d5da;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.comparison-card h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
.comparison-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comparison-card li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.comparison-card li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comparison-card li::before {
|
||||
content: "✓ ";
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: #fff3cd;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎨 HTML Generator Demo</h1>
|
||||
<p>Generate TestDrive-JSUI HTML in standalone or external resources mode</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>⚙️ Configuration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="title">Document Title:</label>
|
||||
<input type="text" id="title" value="My Test Document" placeholder="Enter title...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description:</label>
|
||||
<input type="text" id="description" value="Interactive markdown editor demo" placeholder="Enter description...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="markdown">Markdown Content:</label>
|
||||
<textarea id="markdown" placeholder="Enter markdown content..."># Welcome to TestDrive-JSUI
|
||||
|
||||
This is a **demo document** showing the HTML generator.
|
||||
|
||||
## Features
|
||||
|
||||
- Section editing
|
||||
- Control panels
|
||||
- Keyboard shortcuts
|
||||
- Theming support
|
||||
|
||||
## Getting Started
|
||||
|
||||
Click any section to edit it. Try the controls on the sides!</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Mode:</label>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="radio" name="mode" value="edit" checked> Edit Mode
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="mode" value="view"> View Mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Control Panels:</label>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="editControl" checked> Edit Control
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="statusControl" checked> Status Control
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="contentsControl" checked> Contents Control
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="debugControl"> Debug Control
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Options:</label>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="shortcuts" checked> Keyboard Shortcuts
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="autosave"> Autosave
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📦 Generation Mode</h2>
|
||||
|
||||
<div class="comparison">
|
||||
<div class="comparison-card">
|
||||
<h3>Standalone Mode</h3>
|
||||
<ul>
|
||||
<li>All CSS/JS embedded inline</li>
|
||||
<li>Single file portability</li>
|
||||
<li>No external dependencies</li>
|
||||
<li>Larger file size (~100+ KB)</li>
|
||||
<li>Works via file://</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="comparison-card">
|
||||
<h3>External Resources Mode</h3>
|
||||
<ul>
|
||||
<li>References _jsui/ directory</li>
|
||||
<li>Smaller HTML files</li>
|
||||
<li>Browser caching</li>
|
||||
<li>Easier to debug</li>
|
||||
<li>Requires web server</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<button onclick="generateStandalone()">📄 Generate Standalone HTML</button>
|
||||
<button onclick="generateExternal()" class="secondary">🔗 Generate External HTML</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📋 Generated HTML</h2>
|
||||
<div class="info-box">
|
||||
<strong>Note:</strong> Click one of the buttons above to generate HTML.
|
||||
The output will appear below and can be downloaded.
|
||||
</div>
|
||||
<div id="output" class="output">
|
||||
<!-- Generated HTML will appear here -->
|
||||
<em>No HTML generated yet. Click a button above to generate.</em>
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button onclick="downloadHTML()" id="downloadBtn" style="display: none;">💾 Download HTML</button>
|
||||
<button onclick="copyHTML()" id="copyBtn" style="display: none;" class="secondary">📋 Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/utils/html-generator.js"></script>
|
||||
<script>
|
||||
let generatedHTML = '';
|
||||
|
||||
function getConfig() {
|
||||
return {
|
||||
title: document.getElementById('title').value,
|
||||
description: document.getElementById('description').value,
|
||||
markdown: document.getElementById('markdown').value,
|
||||
mode: document.querySelector('input[name="mode"]:checked').value,
|
||||
theme: 'github',
|
||||
controls: {
|
||||
editControl: document.getElementById('editControl').checked,
|
||||
statusControl: document.getElementById('statusControl').checked,
|
||||
contentsControl: document.getElementById('contentsControl').checked,
|
||||
debugControl: document.getElementById('debugControl').checked
|
||||
},
|
||||
shortcuts: document.getElementById('shortcuts').checked,
|
||||
autosave: document.getElementById('autosave').checked
|
||||
};
|
||||
}
|
||||
|
||||
function generateStandalone() {
|
||||
const config = getConfig();
|
||||
config.standalone = true;
|
||||
|
||||
const generator = new HTMLGenerator(config);
|
||||
generatedHTML = generator.generate();
|
||||
|
||||
displayHTML(generatedHTML);
|
||||
console.log('✓ Standalone HTML generated');
|
||||
}
|
||||
|
||||
function generateExternal() {
|
||||
const config = getConfig();
|
||||
config.standalone = false;
|
||||
config.baseUrl = '_jsui';
|
||||
|
||||
const generator = new HTMLGenerator(config);
|
||||
generatedHTML = generator.generate();
|
||||
|
||||
displayHTML(generatedHTML);
|
||||
console.log('✓ External HTML generated');
|
||||
}
|
||||
|
||||
function displayHTML(html) {
|
||||
const output = document.getElementById('output');
|
||||
output.textContent = html;
|
||||
|
||||
// Show action buttons
|
||||
document.getElementById('downloadBtn').style.display = 'inline-block';
|
||||
document.getElementById('copyBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function downloadHTML() {
|
||||
if (!generatedHTML) {
|
||||
alert('No HTML generated yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([generatedHTML], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'testdrive-jsui-editor.html';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('✓ HTML downloaded');
|
||||
}
|
||||
|
||||
function copyHTML() {
|
||||
if (!generatedHTML) {
|
||||
alert('No HTML generated yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(generatedHTML).then(() => {
|
||||
alert('HTML copied to clipboard!');
|
||||
console.log('✓ HTML copied to clipboard');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
alert('Failed to copy HTML. Check browser permissions.');
|
||||
});
|
||||
}
|
||||
|
||||
// Log successful load
|
||||
console.log('✓ HTML Generator Demo loaded');
|
||||
console.log('HTMLGenerator available:', typeof HTMLGenerator !== 'undefined');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
409
examples/standalone.html
Normal file
409
examples/standalone.html
Normal file
@@ -0,0 +1,409 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestDrive-JSUI - Standalone Demo</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #24292e;
|
||||
background: #f6f8fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #0366d6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #586069;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.controls button {
|
||||
background: #0366d6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #0256c7;
|
||||
}
|
||||
|
||||
.controls button.secondary {
|
||||
background: #6a737d;
|
||||
}
|
||||
|
||||
.controls button.secondary:hover {
|
||||
background: #586069;
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Markdown content styles */
|
||||
#editor-container h1,
|
||||
#editor-container h2,
|
||||
#editor-container h3,
|
||||
#editor-container h4,
|
||||
#editor-container h5,
|
||||
#editor-container h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#editor-container h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#editor-container h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#editor-container p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#editor-container ul,
|
||||
#editor-container ol {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
#editor-container code {
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
#editor-container pre {
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#editor-container pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#editor-container blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
color: #6a737d;
|
||||
padding-left: 1em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#editor-container strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
color: #586069;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🚀 TestDrive-JSUI Standalone Demo</h1>
|
||||
<p>
|
||||
A complete markdown editor running <strong>entirely in your browser</strong> -
|
||||
no server, no backend, no build step required!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>📍 Proof of Concept:</strong> This demonstrates TestDrive-JSUI as a pure JavaScript library.
|
||||
All markdown rendering, UI, and editing happens in your browser using only JavaScript.
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="saveContent()">💾 Save to LocalStorage</button>
|
||||
<button onclick="loadContent()">📂 Load from LocalStorage</button>
|
||||
<button onclick="downloadMarkdown()">⬇️ Download as .md</button>
|
||||
<button onclick="showMarkdown()" class="secondary">👁️ View Raw Markdown</button>
|
||||
<button onclick="clearContent()" class="secondary">🗑️ Clear</button>
|
||||
</div>
|
||||
|
||||
<div id="editor-container">
|
||||
<!-- Markdown content will be rendered here -->
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>TestDrive-JSUI • Pure JavaScript • No Backend Required</p>
|
||||
<p><small>Open this file directly in any browser - it just works!</small></p>
|
||||
</div>
|
||||
|
||||
<!-- External Dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- TestDrive-JSUI Core Components -->
|
||||
<script src="../js/core/debug-system.js"></script>
|
||||
<script src="../js/core/section-manager.js"></script>
|
||||
<script src="../js/components/dom-renderer.js"></script>
|
||||
|
||||
<!-- Main Application Script -->
|
||||
<script>
|
||||
// Sample markdown content
|
||||
const defaultMarkdown = `# Welcome to TestDrive-JSUI
|
||||
|
||||
## What is This?
|
||||
|
||||
This is a **standalone demonstration** of TestDrive-JSUI working as a pure JavaScript library. Everything you see is rendered in your browser using JavaScript - no server required!
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **Pure JavaScript**: Runs entirely in the browser
|
||||
- ✅ **Markdown Rendering**: Uses marked.js for parsing
|
||||
- ✅ **Standalone**: No backend, no build tools needed
|
||||
- ✅ **Portable**: Open this file anywhere - it just works!
|
||||
- ✅ **LocalStorage**: Save and load your content locally
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Markdown Input**: Content is stored as markdown text
|
||||
2. **marked.js**: Parses markdown to HTML
|
||||
3. **TestDrive-JSUI**: Renders and manages the UI
|
||||
4. **Browser**: Displays everything natively
|
||||
|
||||
## Try It Out!
|
||||
|
||||
You can:
|
||||
|
||||
- Edit this content (coming soon in full version)
|
||||
- Save to browser localStorage
|
||||
- Download as a .md file
|
||||
- Load previously saved content
|
||||
|
||||
## Code Example
|
||||
|
||||
\`\`\`javascript
|
||||
// Initialize the editor
|
||||
const editor = {
|
||||
markdown: '# Hello World',
|
||||
render: function() {
|
||||
const html = marked.parse(this.markdown);
|
||||
document.getElementById('editor-container').innerHTML = html;
|
||||
}
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### JavaScript Library
|
||||
The core functionality is pure JavaScript:
|
||||
- Markdown parsing
|
||||
- UI rendering
|
||||
- Section management
|
||||
- Event handling
|
||||
|
||||
### Optional Adapters
|
||||
Language-specific adapters (Python, Ruby, Java) just help with:
|
||||
- Serving assets
|
||||
- Backend integration
|
||||
- Testing infrastructure
|
||||
|
||||
But they're **not required** for core functionality!
|
||||
|
||||
## Next Steps
|
||||
|
||||
This proof of concept demonstrates that TestDrive-JSUI can work as a standalone JavaScript library. The full implementation will include:
|
||||
|
||||
1. **Edit Mode**: Interactive editing with controls
|
||||
2. **Section Management**: Edit sections independently
|
||||
3. **Auto-save**: Automatic localStorage saves
|
||||
4. **Themes**: Multiple visual themes
|
||||
5. **Plugins**: Extension system
|
||||
|
||||
---
|
||||
|
||||
**Status**: Proof of Concept ✅
|
||||
**Architecture**: JavaScript-First ✅
|
||||
**Backend Required**: No ❌
|
||||
`;
|
||||
|
||||
// Simple editor implementation
|
||||
const editor = {
|
||||
markdown: defaultMarkdown,
|
||||
container: null,
|
||||
|
||||
init: function() {
|
||||
this.container = document.getElementById('editor-container');
|
||||
this.loadFromLocalStorage();
|
||||
this.render();
|
||||
|
||||
console.log('✅ TestDrive-JSUI Standalone initialized!');
|
||||
console.log('📝 Try the buttons above to interact with the editor');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!window.marked) {
|
||||
this.container.innerHTML = '<p style="color: red;">Error: marked.js not loaded</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure marked
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
headerIds: true
|
||||
});
|
||||
|
||||
// Parse and render markdown
|
||||
const html = marked.parse(this.markdown);
|
||||
this.container.innerHTML = html;
|
||||
},
|
||||
|
||||
setContent: function(markdown) {
|
||||
this.markdown = markdown;
|
||||
this.render();
|
||||
},
|
||||
|
||||
getContent: function() {
|
||||
return this.markdown;
|
||||
},
|
||||
|
||||
saveToLocalStorage: function() {
|
||||
try {
|
||||
localStorage.setItem('testdrive-jsui-content', this.markdown);
|
||||
alert('✅ Content saved to browser localStorage!');
|
||||
} catch (e) {
|
||||
alert('❌ Failed to save: ' + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
loadFromLocalStorage: function() {
|
||||
try {
|
||||
const saved = localStorage.getItem('testdrive-jsui-content');
|
||||
if (saved) {
|
||||
this.markdown = saved;
|
||||
console.log('📂 Loaded content from localStorage');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load from localStorage:', e);
|
||||
}
|
||||
},
|
||||
|
||||
download: function() {
|
||||
const blob = new Blob([this.markdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'document.md';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log('⬇️ Downloaded markdown file');
|
||||
}
|
||||
};
|
||||
|
||||
// Global functions for button handlers
|
||||
function saveContent() {
|
||||
editor.saveToLocalStorage();
|
||||
}
|
||||
|
||||
function loadContent() {
|
||||
editor.loadFromLocalStorage();
|
||||
editor.render();
|
||||
alert('📂 Content loaded from localStorage!');
|
||||
}
|
||||
|
||||
function downloadMarkdown() {
|
||||
editor.download();
|
||||
}
|
||||
|
||||
function showMarkdown() {
|
||||
alert(editor.getContent());
|
||||
}
|
||||
|
||||
function clearContent() {
|
||||
if (confirm('Clear all content and reset to default?')) {
|
||||
editor.setContent(defaultMarkdown);
|
||||
localStorage.removeItem('testdrive-jsui-content');
|
||||
alert('🗑️ Content cleared!');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
editor.init();
|
||||
});
|
||||
|
||||
// Expose for debugging
|
||||
window.editor = editor;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
138
examples/todohtml/TODO.html
Executable file
138
examples/todohtml/TODO.html
Executable file
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="TestDrive JSUI Markitect 1.0.0">
|
||||
<title>TestDrive JSUI Document</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
#markdown-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #333333;
|
||||
}
|
||||
pre {
|
||||
background-color: #f6f8fa;
|
||||
color: #333333;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
code {
|
||||
background-color: #f6f8fa;
|
||||
color: #333333;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
color: #6a737d;
|
||||
}
|
||||
table {
|
||||
font-size: 0.85em;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
th, td {
|
||||
font-size: inherit;
|
||||
border: 1px solid #d0d7de;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
max-width: 12cm;
|
||||
max-height: 20cm;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Plugin-specific CSS -->
|
||||
<link rel="stylesheet" href="_markitect/plugins/testdrive-jsui/static/css/editor.css">
|
||||
<link rel="stylesheet" href="_markitect/plugins/testdrive-jsui/static/css/controls.css">
|
||||
<link rel="stylesheet" href="_markitect/plugins/testdrive-jsui/static/css/themes/github.css">
|
||||
|
||||
<!-- External dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||||
onload="window.markitectMarkedLoaded = true"
|
||||
onerror="console.error('CDN library failed to load - network or firewall blocking marked.js'); window.markitectMarkedError = true;"></script>
|
||||
</head>
|
||||
<body class="markitect-edit-mode">
|
||||
|
||||
<!-- Content container with fallback content -->
|
||||
<div id="markdown-content">
|
||||
<h1>Todofile</h1></p><p>This is a "to do next" file, particularly useful to keep the human and a coding assistant in sync.</p><p>The format is based on [Keep a Todofile V0.0.1](https://coulomb.social/open/KeepaTodofile).</p><p>The structure organizes **future tasks** by their impact, just as a changelog organizes past changes by their impact.</p><p>***</p><p><h2>[Unreleased] - *Active Vibe-Coding State* 💡</h2></p><p>This section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.</p><p>*No active tasks at this time.*</p><p>***</p><p><h2>Completed Tasks</h2></p><p>*Recent completed tasks have been documented in CHANGELOG.md following Keep a Changelog format.*
|
||||
</div>
|
||||
|
||||
<!-- Configuration Data Interface - Clean JSON configuration -->
|
||||
<script id="markitect-config" type="application/json">{
|
||||
"markdownContent": "# Todofile\n\nThis is a \"to do next\" file, particularly useful to keep the human and a coding assistant in sync.\n\nThe format is based on [Keep a Todofile V0.0.1](https://coulomb.social/open/KeepaTodofile).\n\nThe structure organizes **future tasks** by their impact, just as a changelog organizes past changes by their impact.\n\n***\n\n## [Unreleased] - *Active Vibe-Coding State* \ud83d\udca1\n\nThis section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.\n\n*No active tasks at this time.*\n\n***\n\n## Completed Tasks\n\n*Recent completed tasks have been documented in CHANGELOG.md following Keep a Changelog format.*",
|
||||
"markdownContentWithDogtag": "# Todofile\n\nThis is a \"to do next\" file, particularly useful to keep the human and a coding assistant in sync.\n\nThe format is based on [Keep a Todofile V0.0.1](https://coulomb.social/open/KeepaTodofile).\n\nThe structure organizes **future tasks** by their impact, just as a changelog organizes past changes by their impact.\n\n***\n\n## [Unreleased] - *Active Vibe-Coding State* \ud83d\udca1\n\nThis section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.\n\n*No active tasks at this time.*\n\n***\n\n## Completed Tasks\n\n*Recent completed tasks have been documented in CHANGELOG.md following Keep a Changelog format.*",
|
||||
"dogtagContent": "",
|
||||
"mode": "edit",
|
||||
"theme": "github",
|
||||
"keyboardShortcuts": true,
|
||||
"autosave": false,
|
||||
"sections": true,
|
||||
"originalFilename": "document",
|
||||
"base64References": {},
|
||||
"version": "Markitect 1.0.0",
|
||||
"repoName": "Markitect"
|
||||
}</script>
|
||||
|
||||
<!-- Plugin JavaScript Assets -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/core/debug-system.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/core/section-manager.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/components/debug-panel.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/components/dom-renderer.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/controls/control-base.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/controls/contents-control.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/controls/status-control.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/controls/debug-control.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/controls/edit-control.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/config-loader.js"></script>
|
||||
<script src="_markitect/plugins/testdrive-jsui/js/main-updated.js"></script>
|
||||
|
||||
<!-- Initialization Script -->
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
console.log('🎯 TestDrive JSUI loading complete');
|
||||
|
||||
// Handle CDN loading errors
|
||||
if (window.markitectMarkedError) {
|
||||
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
||||
}
|
||||
|
||||
// Note: MarkitectMain auto-initializes via main-updated.js
|
||||
// No manual initialization needed here
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
191
examples/todohtml/_markitect/plugins/testdrive-jsui/js/components/debug-panel.js
Executable file
191
examples/todohtml/_markitect/plugins/testdrive-jsui/js/components/debug-panel.js
Executable file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* DebugPanel Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Handles debug message display and management for client-side debugging.
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone component)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DebugPanel - Manages debug message display and interaction
|
||||
*/
|
||||
class DebugPanel {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.isActive = false;
|
||||
this.maxMessages = 1000; // Keep last 1000 messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug message
|
||||
*/
|
||||
addMessage(message, category = 'INFO') {
|
||||
const messageObj = {
|
||||
message,
|
||||
category,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
|
||||
this.messages.push(messageObj);
|
||||
|
||||
// Keep only last maxMessages
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = this.messages.slice(-this.maxMessages);
|
||||
}
|
||||
|
||||
// Auto-update if panel is visible
|
||||
if (this.isActive) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the debug panel on/off
|
||||
*/
|
||||
toggle() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the debug panel
|
||||
*/
|
||||
show() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'block';
|
||||
debugButton.textContent = '🔍 Debug (ON)';
|
||||
debugButton.style.background = '#28a745';
|
||||
this.isActive = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the debug panel
|
||||
*/
|
||||
hide() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'none';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
debugButton.style.background = '#6c757d';
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the debug panel with current messages
|
||||
*/
|
||||
update() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
if (!debugContainer || !this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
debugContainer.innerHTML = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the last 50 messages in reverse order (newest first)
|
||||
const recentMessages = this.messages.slice(-50).reverse();
|
||||
|
||||
const messagesHtml = recentMessages.map(msg => {
|
||||
const categoryColor = {
|
||||
'INFO': '#17a2b8',
|
||||
'WARNING': '#ffc107',
|
||||
'ERROR': '#dc3545',
|
||||
'SUCCESS': '#28a745',
|
||||
'DEBUG': '#6f42c1'
|
||||
}[msg.category] || '#6c757d';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
|
||||
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
|
||||
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
|
||||
<span style="color: #333;">${msg.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
debugContainer.innerHTML = `
|
||||
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
|
||||
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
|
||||
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
|
||||
</div>
|
||||
<div style="max-height: 250px; overflow-y: auto;">
|
||||
${messagesHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listener for clear button
|
||||
const clearBtn = debugContainer.querySelector('#debug-clear-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.clear();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom to show newest messages
|
||||
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all debug messages
|
||||
*/
|
||||
clear() {
|
||||
this.messages = [];
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of messages
|
||||
*/
|
||||
getMessageCount() {
|
||||
return this.messages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent messages
|
||||
*/
|
||||
getRecentMessages(count = 10) {
|
||||
return this.messages.slice(-count);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DebugPanel };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DebugPanel = DebugPanel;
|
||||
}
|
||||
1128
examples/todohtml/_markitect/plugins/testdrive-jsui/js/components/dom-renderer.js
Executable file
1128
examples/todohtml/_markitect/plugins/testdrive-jsui/js/components/dom-renderer.js
Executable file
File diff suppressed because it is too large
Load Diff
168
examples/todohtml/_markitect/plugins/testdrive-jsui/js/config-loader.js
Executable file
168
examples/todohtml/_markitect/plugins/testdrive-jsui/js/config-loader.js
Executable file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Configuration Loader - Clean interface between Python and JavaScript
|
||||
*
|
||||
* This module provides the ONLY interface for Python-generated data.
|
||||
* All dynamic data from Python must be passed through this JSON configuration.
|
||||
*/
|
||||
|
||||
class MarkitectConfig {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
this.loaded = false;
|
||||
|
||||
// Simple immediate loading - if script is loaded, DOM is ready
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
try {
|
||||
const configElement = document.getElementById('markitect-config');
|
||||
if (!configElement) {
|
||||
throw new Error('Markitect configuration not found - missing markitect-config script element');
|
||||
}
|
||||
|
||||
this.config = JSON.parse(configElement.textContent);
|
||||
this.loaded = true;
|
||||
console.log('✅ Markitect configuration loaded successfully');
|
||||
|
||||
// Validate required fields
|
||||
this.validateConfig();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load Markitect configuration:', error);
|
||||
this.config = this.getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
const required = ['markdownContent', 'mode'];
|
||||
const missing = required.filter(key => !(key in this.config));
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn('⚠️ Missing required config fields:', missing);
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
markdownContent: '# Default Content\n\nConfiguration failed to load.',
|
||||
markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.',
|
||||
dogtagContent: '',
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
keyboardShortcuts: true,
|
||||
autosave: false,
|
||||
sections: true,
|
||||
originalFilename: 'document',
|
||||
version: 'Markitect v0.8.1',
|
||||
repoName: 'Markitect',
|
||||
base64References: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Getter methods for clean access
|
||||
get markdownContent() {
|
||||
return this.config.markdownContent || '';
|
||||
}
|
||||
|
||||
get markdownContentWithDogtag() {
|
||||
return this.config.markdownContentWithDogtag || this.markdownContent;
|
||||
}
|
||||
|
||||
get dogtagContent() {
|
||||
return this.config.dogtagContent || '';
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.config.mode || 'edit';
|
||||
}
|
||||
|
||||
get isEditMode() {
|
||||
return this.mode === 'edit';
|
||||
}
|
||||
|
||||
get isInsertMode() {
|
||||
return this.mode === 'insert';
|
||||
}
|
||||
|
||||
get theme() {
|
||||
return this.config.theme || 'github';
|
||||
}
|
||||
|
||||
get originalFilename() {
|
||||
return this.config.originalFilename || 'document';
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this.config.version || 'Markitect v0.8.1';
|
||||
}
|
||||
|
||||
get repoName() {
|
||||
return this.config.repoName || 'Markitect';
|
||||
}
|
||||
|
||||
get keyboardShortcuts() {
|
||||
return this.config.keyboardShortcuts !== false;
|
||||
}
|
||||
|
||||
get base64References() {
|
||||
return this.config.base64References || {};
|
||||
}
|
||||
|
||||
get restrictedHeadingLevels() {
|
||||
return this.config.restrictedHeadingLevels || [1, 2, 3];
|
||||
}
|
||||
|
||||
// Check if config is ready for access
|
||||
isReady() {
|
||||
return this.loaded && this.config !== null;
|
||||
}
|
||||
|
||||
// Wait for config to be ready
|
||||
waitForReady(callback, maxWait = 5000) {
|
||||
const startTime = Date.now();
|
||||
const checkReady = () => {
|
||||
if (this.isReady()) {
|
||||
callback();
|
||||
} else if (Date.now() - startTime < maxWait) {
|
||||
setTimeout(checkReady, 50);
|
||||
} else {
|
||||
console.error('❌ Configuration loading timeout after', maxWait, 'ms');
|
||||
callback(); // Call anyway with default config
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
}
|
||||
|
||||
// Get full editor configuration object
|
||||
getEditorConfig() {
|
||||
if (!this.isReady()) {
|
||||
console.warn('⚠️ Configuration not ready, using defaults');
|
||||
return this.getDefaultConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
mode: this.mode,
|
||||
theme: this.theme,
|
||||
keyboardShortcuts: this.keyboardShortcuts,
|
||||
autosave: this.config.autosave || false,
|
||||
sections: this.config.sections !== false,
|
||||
originalFilename: this.originalFilename,
|
||||
version: this.version,
|
||||
repoName: this.repoName,
|
||||
restrictedHeadingLevels: this.restrictedHeadingLevels
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global configuration instance
|
||||
window.markitectConfig = new MarkitectConfig();
|
||||
|
||||
// Legacy compatibility - expose common config values globally
|
||||
window.editorConfig = window.markitectConfig.getEditorConfig();
|
||||
window.markitectBase64References = window.markitectConfig.base64References;
|
||||
|
||||
// Export for module use
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = MarkitectConfig;
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* ContentsControl - Table of Contents Display Control
|
||||
*
|
||||
* Provides an interactive table of contents for document navigation.
|
||||
* Extracts headings from the document and displays them in a hierarchical
|
||||
* structure with clickable links for quick navigation.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic heading extraction from document
|
||||
* - Hierarchical display with proper indentation
|
||||
* - Clickable navigation links with smooth scrolling
|
||||
* - Real-time updates when document structure changes
|
||||
* - Search functionality within the table of contents
|
||||
*
|
||||
* Dependencies:
|
||||
* - ControlBase (base control functionality)
|
||||
*/
|
||||
|
||||
/**
|
||||
* ContentsControl - Interactive table of contents control
|
||||
*
|
||||
* Built on the base class architecture for consistency with other panels.
|
||||
* Only implements content-specific functionality while inheriting all
|
||||
* common panel behavior from ControlBase.
|
||||
*/
|
||||
class ContentsControl extends ControlBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure for contents functionality
|
||||
this.config = {
|
||||
icon: '📋',
|
||||
title: 'Contents',
|
||||
className: 'contents-control',
|
||||
defaultContent: 'Loading table of contents...',
|
||||
ariaLabel: 'Table of Contents Control',
|
||||
position: 'w' // West positioning
|
||||
};
|
||||
|
||||
// Contents-specific state
|
||||
this.headings = [];
|
||||
this.lastScanTime = null;
|
||||
this.updateInterval = null;
|
||||
this.searchQuery = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate contents control content (called by base class buildContent)
|
||||
*/
|
||||
generateContent() {
|
||||
// Extract headings first
|
||||
this.extractHeadings();
|
||||
|
||||
return this.safeOperation(() => {
|
||||
if (this.headings.length === 0) {
|
||||
return `
|
||||
<div style="text-align: center; color: #666; padding: 2rem 0;">
|
||||
<p>No headings found in document</p>
|
||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const searchHTML = `
|
||||
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
|
||||
<input type="text"
|
||||
placeholder="Search headings..."
|
||||
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box; overflow: visible;"
|
||||
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
|
||||
</div>
|
||||
`;
|
||||
|
||||
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
|
||||
const contentsHTML = filteredHeadings.map(heading => {
|
||||
const indentLevel = Math.max(0, heading.level - 1);
|
||||
const indentPx = indentLevel * 15;
|
||||
|
||||
return `
|
||||
<div class="contents-item"
|
||||
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px; overflow: visible;">
|
||||
<a href="#${heading.id}"
|
||||
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
|
||||
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2; overflow: visible;"
|
||||
onmouseover="this.style.backgroundColor='#f8f9fa'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
|
||||
<span class="heading-text">${heading.text}</span>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const statusHTML = `
|
||||
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
|
||||
Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const refreshButtonHTML = `
|
||||
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center; margin-top: 0.5rem;">
|
||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
🔄 Refresh Contents
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
${searchHTML}
|
||||
${statusHTML}
|
||||
${contentsHTML}
|
||||
${refreshButtonHTML}
|
||||
`;
|
||||
|
||||
}, 'Error generating contents', 'generateContent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all headings from the document
|
||||
* Creates a hierarchical structure of the document's heading elements
|
||||
*/
|
||||
extractHeadings() {
|
||||
return this.safeOperation(() => {
|
||||
const headingSelectors = 'h1, h2, h3, h4, h5, h6';
|
||||
const headingElements = document.querySelectorAll(headingSelectors);
|
||||
const extractedHeadings = [];
|
||||
|
||||
headingElements.forEach((heading, index) => {
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
const text = heading.textContent.trim();
|
||||
|
||||
// Generate or use existing ID for anchor links
|
||||
let id = heading.id;
|
||||
if (!id) {
|
||||
id = text.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.substring(0, 50);
|
||||
|
||||
// Ensure uniqueness
|
||||
let counter = 1;
|
||||
let uniqueId = id;
|
||||
while (document.getElementById(uniqueId)) {
|
||||
uniqueId = `${id}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
heading.id = uniqueId;
|
||||
id = uniqueId;
|
||||
}
|
||||
|
||||
extractedHeadings.push({
|
||||
id,
|
||||
text,
|
||||
level,
|
||||
element: heading,
|
||||
index
|
||||
});
|
||||
});
|
||||
|
||||
this.headings = extractedHeadings;
|
||||
this.lastScanTime = Date.now();
|
||||
return extractedHeadings;
|
||||
|
||||
}, [], 'extractHeadings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter headings based on search query
|
||||
*/
|
||||
filterHeadings(headings, query) {
|
||||
if (!query || query.trim() === '') {
|
||||
return headings;
|
||||
}
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
return headings.filter(heading =>
|
||||
heading.text.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific heading with smooth scrolling
|
||||
*/
|
||||
navigateToHeading(headingId) {
|
||||
return this.safeOperation(() => {
|
||||
const targetElement = document.getElementById(headingId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Highlight the target temporarily
|
||||
const originalStyle = targetElement.style.backgroundColor;
|
||||
targetElement.style.backgroundColor = '#fff3cd';
|
||||
targetElement.style.transition = 'background-color 0.3s ease';
|
||||
|
||||
setTimeout(() => {
|
||||
targetElement.style.backgroundColor = originalStyle;
|
||||
setTimeout(() => {
|
||||
targetElement.style.transition = '';
|
||||
}, 300);
|
||||
}, 1500);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, false, 'navigateToHeading');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle search input
|
||||
*/
|
||||
handleSearch(query) {
|
||||
this.searchQuery = query;
|
||||
this.buildContent(); // Rebuild content with new filter
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the contents by re-scanning the document
|
||||
*/
|
||||
refreshContents() {
|
||||
return this.safeOperation(() => {
|
||||
this.extractHeadings();
|
||||
this.buildContent(); // Rebuild content with updated headings
|
||||
|
||||
// Show success feedback
|
||||
const refreshBtn = this.element?.querySelector('button');
|
||||
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
|
||||
const originalText = refreshBtn.innerHTML;
|
||||
refreshBtn.innerHTML = '✅ Updated';
|
||||
refreshBtn.style.background = '#28a745';
|
||||
|
||||
setTimeout(() => {
|
||||
refreshBtn.innerHTML = originalText;
|
||||
refreshBtn.style.background = '#28a745';
|
||||
}, 1000);
|
||||
}
|
||||
}, null, 'refreshContents');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override buildContent to add control reference and auto-refresh
|
||||
*/
|
||||
buildContent() {
|
||||
super.buildContent();
|
||||
|
||||
// Store reference to this control for onclick handlers
|
||||
if (this.element) {
|
||||
this.element.contentsControl = this;
|
||||
}
|
||||
|
||||
// Set up auto-refresh for dynamic content
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
this.updateInterval = setInterval(() => {
|
||||
const currentHeadingCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
|
||||
if (currentHeadingCount !== this.headings.length) {
|
||||
this.refreshContents();
|
||||
}
|
||||
}, 5000); // Check every 5 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when control is destroyed
|
||||
*/
|
||||
destroy() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global for direct usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ContentsControl;
|
||||
} else {
|
||||
window.ContentsControl = ContentsControl;
|
||||
}
|
||||
852
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/control-base.js
Executable file
852
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/control-base.js
Executable file
@@ -0,0 +1,852 @@
|
||||
/**
|
||||
* Base Control Class for TestDrive-JSUI Controls
|
||||
*
|
||||
* Provides common functionality for positioning, drag, resize, expand/collapse operations.
|
||||
* This is the foundation class that all UI controls inherit from to ensure consistent
|
||||
* behavior across the TestDrive-JSUI component system.
|
||||
*
|
||||
* Key Features:
|
||||
* - Drag and drop positioning with compass-based anchoring
|
||||
* - Resize handles with hover-based visibility
|
||||
* - Expand/collapse state management
|
||||
* - Safe operation wrappers with error handling
|
||||
* - Development mode with strict error checking
|
||||
* - Accessibility support with proper ARIA labels
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone base class)
|
||||
*
|
||||
* Usage:
|
||||
* Controls inherit from this base by using Object.create(Control) and
|
||||
* implementing their specific buildContent() methods.
|
||||
*/
|
||||
|
||||
// Development mode detection for enhanced error reporting
|
||||
const MARKITECT_STRICT_MODE = (
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.search.includes('strict=true') ||
|
||||
window.markitectStrictMode === true
|
||||
);
|
||||
|
||||
/**
|
||||
* ControlBase - Foundation class for all TestDrive-JSUI controls
|
||||
*
|
||||
* Provides the base functionality that all controls inherit:
|
||||
* - DOM element management
|
||||
* - Positioning and drag behavior
|
||||
* - Resize handle management
|
||||
* - State persistence
|
||||
* - Error handling with strict mode support
|
||||
*/
|
||||
class ControlBase {
|
||||
constructor() {
|
||||
// Default configuration that controls can override
|
||||
this.config = {
|
||||
icon: '🔧',
|
||||
title: 'Control',
|
||||
className: 'base-control',
|
||||
defaultContent: 'Control content',
|
||||
ariaLabel: 'Base Control',
|
||||
position: 'w', // Compass position: west (middle-left)
|
||||
footer: null // Custom footer text
|
||||
};
|
||||
|
||||
// Internal state
|
||||
this.element = null;
|
||||
this.isExpanded = false;
|
||||
this.isHeaderOnly = false; // New state for header-only visibility
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.position = { x: 0, y: 0 };
|
||||
this.size = {
|
||||
width: 300,
|
||||
height: Math.floor(window.innerHeight / 3)
|
||||
};
|
||||
this.originalPosition = null; // Store original position for collapse
|
||||
|
||||
// Event handlers storage
|
||||
this.eventHandlers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe operation wrapper with error handling
|
||||
* Provides consistent error handling across all control operations
|
||||
*/
|
||||
safeOperation(operation, fallback = null, context = 'Unknown') {
|
||||
try {
|
||||
return operation();
|
||||
} catch (error) {
|
||||
console.error(`Control operation failed in ${context}:`, error);
|
||||
|
||||
if (MARKITECT_STRICT_MODE) {
|
||||
throw error; // Re-throw in strict mode for debugging
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and initialize the control element
|
||||
* This method sets up the basic DOM structure that all controls use
|
||||
*/
|
||||
createElement() {
|
||||
return this.safeOperation(() => {
|
||||
if (this.element) {
|
||||
this.destroy(); // Clean up existing element
|
||||
}
|
||||
|
||||
const control = document.createElement('div');
|
||||
control.className = `control-panel ${this.config.className}`;
|
||||
control.setAttribute('role', 'dialog');
|
||||
control.setAttribute('aria-label', this.config.ariaLabel);
|
||||
|
||||
control.innerHTML = `
|
||||
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
|
||||
<div class="control-panel-expanded" style="display: none;">
|
||||
<div class="control-header">
|
||||
<span class="control-icon">${this.config.icon}</span>
|
||||
<span class="control-title">${this.config.title}</span>
|
||||
<button class="control-close">✕</button>
|
||||
</div>
|
||||
<div class="control-content">
|
||||
${this.config.defaultContent}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.element = control;
|
||||
this.setupStyles();
|
||||
this.setupEventListeners();
|
||||
return control;
|
||||
|
||||
}, null, 'createElement');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up base styles for the control
|
||||
*/
|
||||
setupStyles() {
|
||||
if (!this.element) return;
|
||||
|
||||
// Position the element
|
||||
this.element.style.position = 'fixed';
|
||||
this.element.style.zIndex = '1000';
|
||||
|
||||
// Store original position for collapse
|
||||
this.storeOriginalPosition();
|
||||
|
||||
// Style the icon-only toggle button
|
||||
const toggleBtn = this.element.querySelector('.control-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.style.cssText = `
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: rgba(248, 249, 250, 0.95);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.2s ease;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for control interaction
|
||||
* Handles dragging, resizing, and toggle functionality
|
||||
*/
|
||||
setupEventListeners() {
|
||||
if (!this.element) return;
|
||||
|
||||
// Icon toggle to expand
|
||||
const toggleBtn = this.element.querySelector('.control-toggle');
|
||||
if (toggleBtn) {
|
||||
this.addEventListener(toggleBtn, 'click', () => this.expand());
|
||||
}
|
||||
|
||||
// Close button to collapse back to icon
|
||||
const closeBtn = this.element.querySelector('.control-close');
|
||||
if (closeBtn) {
|
||||
this.addEventListener(closeBtn, 'click', () => this.collapse());
|
||||
}
|
||||
|
||||
// Header title click to toggle content visibility
|
||||
const title = this.element.querySelector('.control-title');
|
||||
if (title) {
|
||||
this.addEventListener(title, 'click', () => this.toggleHeaderOnly());
|
||||
}
|
||||
|
||||
// Drag functionality on header when expanded
|
||||
const header = this.element.querySelector('.control-header');
|
||||
if (header) {
|
||||
this.addEventListener(header, 'mousedown', (e) => {
|
||||
if (this.isExpanded && e.target !== title && e.target !== closeBtn) {
|
||||
this.startDrag(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener with automatic cleanup tracking
|
||||
*/
|
||||
addEventListener(element, event, handler) {
|
||||
const key = `${element.className}_${event}`;
|
||||
|
||||
// Remove existing handler if it exists
|
||||
if (this.eventHandlers.has(key)) {
|
||||
const [oldElement, oldEvent, oldHandler] = this.eventHandlers.get(key);
|
||||
oldElement.removeEventListener(oldEvent, oldHandler);
|
||||
}
|
||||
|
||||
// Add new handler
|
||||
element.addEventListener(event, handler);
|
||||
this.eventHandlers.set(key, [element, event, handler]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store original position for collapse restoration
|
||||
*/
|
||||
storeOriginalPosition() {
|
||||
if (!this.element) return;
|
||||
|
||||
const positionStyles = this.getCompassPosition();
|
||||
this.originalPosition = {
|
||||
top: positionStyles.top,
|
||||
left: positionStyles.left,
|
||||
right: positionStyles.right,
|
||||
bottom: positionStyles.bottom,
|
||||
transform: positionStyles.transform
|
||||
};
|
||||
|
||||
// Apply original position
|
||||
Object.assign(this.element.style, positionStyles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compass-based positioning styles
|
||||
*/
|
||||
getCompassPosition() {
|
||||
const positions = {
|
||||
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
|
||||
'ne': { top: '20px', right: '20px' },
|
||||
'e': { right: '20px', top: '50%', transform: 'translateY(-50%)' },
|
||||
'se': { bottom: '20px', right: '20px' },
|
||||
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
|
||||
'sw': { bottom: '20px', left: '20px' },
|
||||
'w': { left: '20px', top: '50%', transform: 'translateY(-50%)' },
|
||||
'nw': { top: '20px', left: '20px' }
|
||||
};
|
||||
|
||||
return positions[this.config.position] || positions['w'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the control from icon-only state
|
||||
*/
|
||||
expand() {
|
||||
return this.safeOperation(() => {
|
||||
this.isExpanded = true;
|
||||
const panel = this.element?.querySelector('.control-panel-expanded');
|
||||
const toggleBtn = this.element?.querySelector('.control-toggle');
|
||||
|
||||
if (panel && toggleBtn) {
|
||||
panel.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
|
||||
// Calculate default height as 1/3 of window height
|
||||
const defaultHeight = Math.floor(window.innerHeight / 3);
|
||||
|
||||
// Style expanded panel
|
||||
panel.style.cssText = `
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(248, 249, 250, 0.95);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
min-width: 300px;
|
||||
min-height: 200px;
|
||||
max-height: calc(100vh - 40px);
|
||||
width: auto;
|
||||
height: ${defaultHeight}px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
// Style header
|
||||
const header = this.element.querySelector('.control-header');
|
||||
if (header) {
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 12px;
|
||||
background: rgba(0,0,0,0.05);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
min-height: 24px;
|
||||
border-radius: 7px 7px 0 0;
|
||||
margin: -1px -1px 0 -1px;
|
||||
`;
|
||||
}
|
||||
|
||||
// Style content area container
|
||||
const contentArea = this.element.querySelector('.control-content');
|
||||
if (contentArea) {
|
||||
contentArea.style.cssText = `
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
`;
|
||||
}
|
||||
|
||||
// Style close button
|
||||
const closeBtn = this.element.querySelector('.control-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
}
|
||||
|
||||
// Add resize handle
|
||||
this.addResizeHandle();
|
||||
this.buildContent();
|
||||
}
|
||||
|
||||
return this.isExpanded;
|
||||
}, false, 'expand');
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse back to icon-only state at original position
|
||||
*/
|
||||
collapse() {
|
||||
return this.safeOperation(() => {
|
||||
this.isExpanded = false;
|
||||
this.isHeaderOnly = false;
|
||||
const panel = this.element?.querySelector('.control-panel-expanded');
|
||||
const toggleBtn = this.element?.querySelector('.control-toggle');
|
||||
|
||||
if (panel && toggleBtn) {
|
||||
panel.style.display = 'none';
|
||||
toggleBtn.style.display = 'block';
|
||||
|
||||
// Restore original position
|
||||
if (this.originalPosition) {
|
||||
// Clear any drag positioning
|
||||
this.element.style.left = this.originalPosition.left || '';
|
||||
this.element.style.right = this.originalPosition.right || '';
|
||||
this.element.style.top = this.originalPosition.top || '';
|
||||
this.element.style.bottom = this.originalPosition.bottom || '';
|
||||
this.element.style.transform = this.originalPosition.transform || '';
|
||||
}
|
||||
|
||||
// Reset panel size to defaults
|
||||
panel.style.width = '';
|
||||
panel.style.height = '';
|
||||
panel.style.minWidth = '300px';
|
||||
panel.style.minHeight = '200px';
|
||||
|
||||
// Reset internal size tracking
|
||||
this.size.width = 300;
|
||||
this.size.height = Math.floor(window.innerHeight / 3);
|
||||
this.storedWidth = null;
|
||||
|
||||
// Remove resize handle
|
||||
this.removeResizeHandle();
|
||||
}
|
||||
|
||||
return !this.isExpanded;
|
||||
}, false, 'collapse');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle header-only visibility (content show/hide)
|
||||
*/
|
||||
toggleHeaderOnly() {
|
||||
return this.safeOperation(() => {
|
||||
if (!this.isExpanded) {
|
||||
// If collapsed, expand first
|
||||
this.expand();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.element?.querySelector('.control-content');
|
||||
const panel = this.element?.querySelector('.control-panel-expanded');
|
||||
|
||||
if (content && panel) {
|
||||
this.isHeaderOnly = !this.isHeaderOnly;
|
||||
const resizeHandle = this.element?.querySelector('.control-resize-handle');
|
||||
|
||||
if (this.isHeaderOnly) {
|
||||
// Store current width before collapsing
|
||||
const currentWidth = panel.offsetWidth;
|
||||
this.storedWidth = currentWidth;
|
||||
|
||||
// Hide content and shrink panel height only
|
||||
content.style.display = 'none';
|
||||
panel.style.minHeight = 'auto';
|
||||
panel.style.height = 'auto';
|
||||
|
||||
// Keep the same width and position
|
||||
panel.style.width = `${currentWidth}px`;
|
||||
panel.style.minWidth = `${currentWidth}px`;
|
||||
|
||||
// Hide resize handle in header-only mode
|
||||
if (resizeHandle) {
|
||||
resizeHandle.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Show content and restore full panel size
|
||||
content.style.display = 'block';
|
||||
panel.style.minHeight = '200px';
|
||||
|
||||
// Restore stored width or use default
|
||||
const widthToRestore = this.storedWidth || 300;
|
||||
panel.style.minWidth = `${widthToRestore}px`;
|
||||
|
||||
// Restore height if it was auto
|
||||
if (!panel.style.height || panel.style.height === 'auto') {
|
||||
panel.style.height = '200px';
|
||||
}
|
||||
if (!panel.style.width || panel.style.width === `${widthToRestore}px`) {
|
||||
panel.style.width = `${widthToRestore}px`;
|
||||
}
|
||||
|
||||
// Show resize handle when fully expanded
|
||||
if (resizeHandle) {
|
||||
resizeHandle.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.isHeaderOnly;
|
||||
}, false, 'toggleHeaderOnly');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start drag operation
|
||||
*/
|
||||
startDrag(event) {
|
||||
if (!this.isExpanded) return; // Only drag when expanded
|
||||
|
||||
this.isDragging = true;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
|
||||
// Calculate offset from mouse to element origin
|
||||
this.dragOffset = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
|
||||
// Store current computed position before clearing styles
|
||||
const computedStyle = window.getComputedStyle(this.element);
|
||||
const currentLeft = rect.left;
|
||||
const currentTop = rect.top;
|
||||
|
||||
// Clear any positioning styles that interfere with dragging
|
||||
this.element.style.right = '';
|
||||
this.element.style.bottom = '';
|
||||
this.element.style.transform = '';
|
||||
|
||||
// Set the element to its current visual position using left/top
|
||||
this.element.style.left = `${currentLeft}px`;
|
||||
this.element.style.top = `${currentTop}px`;
|
||||
|
||||
// Update internal position tracking
|
||||
this.position.x = currentLeft;
|
||||
this.position.y = currentTop;
|
||||
|
||||
// Add global mouse move and up handlers
|
||||
const handleMouseMove = (e) => this.handleDrag(e);
|
||||
const handleMouseUp = () => this.stopDrag();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// Store handlers for cleanup (but don't use the tracked version to avoid conflicts)
|
||||
this._dragHandlers = { move: handleMouseMove, up: handleMouseUp };
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag movement
|
||||
*/
|
||||
handleDrag(event) {
|
||||
if (!this.isDragging || !this.element) return;
|
||||
|
||||
// Calculate new position based on mouse position and offset
|
||||
const newX = event.clientX - this.dragOffset.x;
|
||||
const newY = event.clientY - this.dragOffset.y;
|
||||
|
||||
// Update element position
|
||||
this.element.style.left = `${newX}px`;
|
||||
this.element.style.top = `${newY}px`;
|
||||
|
||||
this.position.x = newX;
|
||||
this.position.y = newY;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop drag operation
|
||||
*/
|
||||
stopDrag() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
this.isDragging = false;
|
||||
|
||||
// Clean up event handlers
|
||||
if (this._dragHandlers) {
|
||||
document.removeEventListener('mousemove', this._dragHandlers.move);
|
||||
document.removeEventListener('mouseup', this._dragHandlers.up);
|
||||
delete this._dragHandlers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add resize handle to expanded control
|
||||
*/
|
||||
addResizeHandle() {
|
||||
// Remove existing resize handle if any
|
||||
this.removeResizeHandle();
|
||||
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.className = 'control-resize-handle';
|
||||
resizeHandle.innerHTML = '●'; // Dot resize indicator
|
||||
resizeHandle.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: se-resize;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
color: #999;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
// Add to the expanded panel
|
||||
const panel = this.element?.querySelector('.control-panel-expanded');
|
||||
if (panel) {
|
||||
panel.appendChild(resizeHandle);
|
||||
|
||||
// Set up resize handlers
|
||||
this.addEventListener(resizeHandle, 'mousedown', (e) => this.startResize(e));
|
||||
this.addEventListener(resizeHandle, 'dblclick', (e) => this.autoResizeToContent(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove resize handle
|
||||
*/
|
||||
removeResizeHandle() {
|
||||
const handle = this.element?.querySelector('.control-resize-handle');
|
||||
if (handle && handle.parentNode) {
|
||||
handle.parentNode.removeChild(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start resize operation
|
||||
*/
|
||||
startResize(event) {
|
||||
event.stopPropagation(); // Prevent drag from starting
|
||||
if (!this.isExpanded) return;
|
||||
|
||||
this.isResizing = true;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
|
||||
// Store initial size and mouse position
|
||||
this.resizeStart = {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
mouseX: event.clientX,
|
||||
mouseY: event.clientY
|
||||
};
|
||||
|
||||
// Add global mouse move and up handlers
|
||||
const handleMouseMove = (e) => this.handleResize(e);
|
||||
const handleMouseUp = () => this.stopResize();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// Store handlers for cleanup
|
||||
this._resizeHandlers = { move: handleMouseMove, up: handleMouseUp };
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resize movement (bottom-right corner resize)
|
||||
*/
|
||||
handleResize(event) {
|
||||
if (!this.isResizing || !this.element) return;
|
||||
|
||||
const panel = this.element.querySelector('.control-panel-expanded');
|
||||
if (!panel) return;
|
||||
|
||||
// Calculate size change based on mouse movement (bottom-right corner)
|
||||
const deltaX = event.clientX - this.resizeStart.mouseX; // Right direction
|
||||
const deltaY = event.clientY - this.resizeStart.mouseY; // Down direction
|
||||
|
||||
// Get minimum size (collapsed header size or default minimum)
|
||||
const headerHeight = this.element.querySelector('.control-header')?.offsetHeight || 40;
|
||||
const minWidth = 200;
|
||||
const minHeight = headerHeight + 20; // Header plus small padding
|
||||
|
||||
// Calculate new dimensions with minimum constraints
|
||||
const newWidth = Math.max(minWidth, this.resizeStart.width + deltaX);
|
||||
const newHeight = Math.max(minHeight, this.resizeStart.height + deltaY);
|
||||
|
||||
// Apply new size to the panel
|
||||
panel.style.width = `${newWidth}px`;
|
||||
panel.style.height = `${newHeight}px`;
|
||||
|
||||
// Update stored size
|
||||
this.size.width = newWidth;
|
||||
this.size.height = newHeight;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop resize operation
|
||||
*/
|
||||
stopResize() {
|
||||
if (!this.isResizing) return;
|
||||
|
||||
this.isResizing = false;
|
||||
|
||||
// Clean up event handlers
|
||||
if (this._resizeHandlers) {
|
||||
document.removeEventListener('mousemove', this._resizeHandlers.move);
|
||||
document.removeEventListener('mouseup', this._resizeHandlers.up);
|
||||
delete this._resizeHandlers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-resize panel to fit content size with viewport repositioning
|
||||
*/
|
||||
autoResizeToContent(event) {
|
||||
return this.safeOperation(() => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.isExpanded) return;
|
||||
|
||||
const panel = this.element?.querySelector('.control-panel-expanded');
|
||||
const contentBody = this.element?.querySelector('.control-content-body');
|
||||
|
||||
if (!panel || !contentBody) return;
|
||||
|
||||
// Get current panel position
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const currentLeft = rect.left;
|
||||
const currentTop = rect.top;
|
||||
|
||||
// Measure content size by temporarily allowing natural sizing
|
||||
const originalOverflow = contentBody.style.overflow;
|
||||
const originalMaxHeight = panel.style.maxHeight;
|
||||
const originalHeight = panel.style.height;
|
||||
const originalWidth = panel.style.width;
|
||||
|
||||
// Temporarily remove constraints to measure natural size
|
||||
contentBody.style.overflow = 'visible';
|
||||
panel.style.maxHeight = 'none';
|
||||
panel.style.height = 'auto';
|
||||
panel.style.width = 'auto';
|
||||
|
||||
// Force reflow and measure
|
||||
panel.offsetHeight; // Force reflow
|
||||
const contentRect = contentBody.getBoundingClientRect();
|
||||
const headerHeight = this.element.querySelector('.control-header')?.offsetHeight || 24;
|
||||
|
||||
// Calculate ideal size with padding and margins
|
||||
const idealWidth = Math.max(300, Math.min(window.innerWidth - 40, contentRect.width + 40));
|
||||
const idealHeight = Math.max(200, Math.min(window.innerHeight - 40, contentRect.height + headerHeight + 40));
|
||||
|
||||
// Restore original constraints
|
||||
contentBody.style.overflow = originalOverflow;
|
||||
panel.style.maxHeight = originalMaxHeight;
|
||||
|
||||
// Calculate new position to keep panel in viewport
|
||||
let newLeft = currentLeft;
|
||||
let newTop = currentTop;
|
||||
|
||||
// Adjust position if panel would go outside viewport
|
||||
if (currentLeft + idealWidth > window.innerWidth) {
|
||||
newLeft = window.innerWidth - idealWidth - 20;
|
||||
}
|
||||
if (newLeft < 20) {
|
||||
newLeft = 20;
|
||||
}
|
||||
|
||||
if (currentTop + idealHeight > window.innerHeight) {
|
||||
newTop = window.innerHeight - idealHeight - 20;
|
||||
}
|
||||
if (newTop < 20) {
|
||||
newTop = 20;
|
||||
}
|
||||
|
||||
// Apply new size and position
|
||||
panel.style.width = `${idealWidth}px`;
|
||||
panel.style.height = `${idealHeight}px`;
|
||||
|
||||
// Update position if it changed
|
||||
if (newLeft !== currentLeft || newTop !== currentTop) {
|
||||
this.element.style.left = `${newLeft}px`;
|
||||
this.element.style.top = `${newTop}px`;
|
||||
this.position.x = newLeft;
|
||||
this.position.y = newTop;
|
||||
}
|
||||
|
||||
// Update internal size tracking
|
||||
this.size.width = idealWidth;
|
||||
this.size.height = idealHeight;
|
||||
|
||||
}, null, 'autoResizeToContent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the control based on compass position (used by show method)
|
||||
*/
|
||||
positionControl() {
|
||||
if (!this.element) return;
|
||||
|
||||
// Use the compass positioning from setupStyles
|
||||
this.storeOriginalPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the control content (to be overridden by subclasses)
|
||||
*/
|
||||
/**
|
||||
* Build content with consistent styling - calls subclass generateContent()
|
||||
*/
|
||||
buildContent() {
|
||||
const content = this.element?.querySelector('.control-content');
|
||||
if (content) {
|
||||
// Get content from subclass
|
||||
const innerContent = this.generateContent ? this.generateContent() : this.config.defaultContent;
|
||||
|
||||
// Apply consistent container styling
|
||||
content.innerHTML = `
|
||||
<div class="control-content-container" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0 10px 1rem;
|
||||
padding: 0.75rem 1rem 1rem 0;
|
||||
font-size: 0.8rem;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
border-radius: 0 0 6px 6px;
|
||||
overflow: hidden;
|
||||
">
|
||||
<div class="control-content-body" style="
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: 0;
|
||||
">
|
||||
${innerContent}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content - subclasses should override this method
|
||||
* @returns {string} HTML content for the panel body
|
||||
*/
|
||||
generateContent() {
|
||||
return this.config.defaultContent || `<p>Panel content goes here...</p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the control
|
||||
*/
|
||||
show() {
|
||||
return this.safeOperation(() => {
|
||||
if (!this.element) {
|
||||
this.createElement();
|
||||
}
|
||||
|
||||
document.body.appendChild(this.element);
|
||||
this.positionControl();
|
||||
this.buildContent();
|
||||
|
||||
return this.element;
|
||||
}, null, 'show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the control
|
||||
*/
|
||||
hide() {
|
||||
return this.safeOperation(() => {
|
||||
if (this.element && this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
}, null, 'hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the control and clean up resources
|
||||
*/
|
||||
destroy() {
|
||||
return this.safeOperation(() => {
|
||||
// Clean up event listeners
|
||||
for (const [element, event, handler] of this.eventHandlers.values()) {
|
||||
element.removeEventListener(event, handler);
|
||||
}
|
||||
this.eventHandlers.clear();
|
||||
|
||||
// Remove element from DOM
|
||||
if (this.element && this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
|
||||
this.element = null;
|
||||
}, null, 'destroy');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global for direct usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ControlBase;
|
||||
} else {
|
||||
window.ControlBase = ControlBase;
|
||||
}
|
||||
483
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/debug-control.js
Executable file
483
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/debug-control.js
Executable file
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* DebugControl - System Debug Information and Message Display Control
|
||||
*
|
||||
* Provides comprehensive debugging capabilities including system message display,
|
||||
* error tracking, performance monitoring, and development tools. Essential for
|
||||
* troubleshooting and development workflows within the TestDrive-JSUI environment.
|
||||
*
|
||||
* Features:
|
||||
* - Real-time debug message display with categorization
|
||||
* - Error tracking with stack trace information
|
||||
* - Performance metrics and timing measurements
|
||||
* - System information display (browser, viewport, etc.)
|
||||
* - Message filtering and search capabilities
|
||||
* - Export functionality for debug logs
|
||||
* - Integration with MarkitectDebugSystem
|
||||
*
|
||||
* Dependencies:
|
||||
* - ControlBase (base control functionality)
|
||||
* - MarkitectDebugSystem (optional, for enhanced debugging)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DebugControl - Development and debugging information control
|
||||
*
|
||||
* This control serves as a central hub for all debugging activities,
|
||||
* providing developers with essential information for troubleshooting
|
||||
* and performance optimization.
|
||||
*/
|
||||
class DebugControl extends ControlBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure for debug functionality
|
||||
this.config = {
|
||||
icon: '🐛',
|
||||
title: 'Debug',
|
||||
className: 'debug-control',
|
||||
defaultContent: 'Debug information loading...',
|
||||
ariaLabel: 'Debug Information Control',
|
||||
position: 'w' // West positioning
|
||||
};
|
||||
|
||||
// Debug control state
|
||||
this.messages = [];
|
||||
this.maxMessages = 100;
|
||||
this.messageFilter = 'all'; // 'all', 'error', 'warn', 'info', 'debug'
|
||||
this.autoScroll = true;
|
||||
this.isRecording = true;
|
||||
this.startTime = Date.now();
|
||||
this.performanceMarks = new Map();
|
||||
|
||||
this.initializeDebugCapture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize debug message capture
|
||||
*/
|
||||
initializeDebugCapture() {
|
||||
return this.safeOperation(() => {
|
||||
// Capture console messages
|
||||
this.originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
debug: console.debug
|
||||
};
|
||||
|
||||
// Override console methods to capture messages
|
||||
console.log = (...args) => {
|
||||
this.originalConsole.log(...args);
|
||||
this.addDebugMessage('LOG', args.join(' '), 'info');
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
this.originalConsole.error(...args);
|
||||
this.addDebugMessage('ERROR', args.join(' '), 'error');
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
this.originalConsole.warn(...args);
|
||||
this.addDebugMessage('WARN', args.join(' '), 'warn');
|
||||
};
|
||||
|
||||
console.info = (...args) => {
|
||||
this.originalConsole.info(...args);
|
||||
this.addDebugMessage('INFO', args.join(' '), 'info');
|
||||
};
|
||||
|
||||
console.debug = (...args) => {
|
||||
this.originalConsole.debug(...args);
|
||||
this.addDebugMessage('DEBUG', args.join(' '), 'debug');
|
||||
};
|
||||
|
||||
// Capture global errors
|
||||
window.addEventListener('error', (event) => {
|
||||
this.addDebugMessage('ERROR', `${event.message} at ${event.filename}:${event.lineno}`, 'error');
|
||||
});
|
||||
|
||||
// Capture unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
this.addDebugMessage('PROMISE_REJECT', `Unhandled promise rejection: ${event.reason}`, 'error');
|
||||
});
|
||||
|
||||
}, null, 'initializeDebugCapture');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug message to the log
|
||||
*/
|
||||
addDebugMessage(category, message, level = 'info') {
|
||||
return this.safeOperation(() => {
|
||||
if (!this.isRecording) return;
|
||||
|
||||
const debugMessage = {
|
||||
id: Date.now() + Math.random(),
|
||||
timestamp: Date.now(),
|
||||
category,
|
||||
message,
|
||||
level,
|
||||
displayTime: new Date().toLocaleTimeString(),
|
||||
relativeTime: Date.now() - this.startTime
|
||||
};
|
||||
|
||||
this.messages.push(debugMessage);
|
||||
|
||||
// Limit message history
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages.shift();
|
||||
}
|
||||
|
||||
// Update display if visible
|
||||
if (this.element && this.isExpanded) {
|
||||
this.updateMessageDisplay();
|
||||
}
|
||||
|
||||
}, null, 'addDebugMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages filtered by current filter setting
|
||||
*/
|
||||
getFilteredMessages() {
|
||||
if (this.messageFilter === 'all') {
|
||||
return this.messages;
|
||||
}
|
||||
return this.messages.filter(msg => msg.level === this.messageFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate system information HTML
|
||||
*/
|
||||
generateSystemInfoHTML() {
|
||||
return this.safeOperation(() => {
|
||||
const systemInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
||||
screen: `${screen.width}x${screen.height}`,
|
||||
colorDepth: screen.colorDepth,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
language: navigator.language,
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
onlineStatus: navigator.onLine ? 'Online' : 'Offline',
|
||||
protocol: window.location.protocol,
|
||||
memory: performance.memory ?
|
||||
`Used: ${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)}MB` :
|
||||
'Not available'
|
||||
};
|
||||
|
||||
// Get Markitect version from config or default
|
||||
const markitectVersion = window.markitectConfig?.version || 'Unknown';
|
||||
|
||||
return `
|
||||
<div class="system-info" style="margin-bottom: 1rem; padding: 0.5rem; background: #f8f9fa; border-radius: 3px; font-size: 0.7rem;">
|
||||
<div style="line-height: 1.3;">
|
||||
<div><strong>Markitect:</strong> ${markitectVersion}</div>
|
||||
<div><strong>Viewport:</strong> ${systemInfo.viewport}</div>
|
||||
<div><strong>Screen:</strong> ${systemInfo.screen}</div>
|
||||
<div><strong>Memory:</strong> ${systemInfo.memory}</div>
|
||||
<div><strong>Language:</strong> ${systemInfo.language}</div>
|
||||
<div><strong>Status:</strong> ${systemInfo.onlineStatus}</div>
|
||||
<div><strong>Protocol:</strong> ${systemInfo.protocol}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}, '', 'generateSystemInfoHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate performance metrics HTML
|
||||
*/
|
||||
generatePerformanceHTML() {
|
||||
return this.safeOperation(() => {
|
||||
const timing = performance.timing;
|
||||
const navigation = performance.getEntriesByType('navigation')[0];
|
||||
|
||||
const metrics = {
|
||||
pageLoad: timing.loadEventEnd - timing.navigationStart,
|
||||
domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
|
||||
firstByte: timing.responseStart - timing.navigationStart,
|
||||
uptime: Date.now() - this.startTime,
|
||||
messagesCount: this.messages.length
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="performance-info" style="margin-bottom: 1rem; padding: 0.5rem; background: #e7f3ff; border-radius: 3px; font-size: 0.7rem;">
|
||||
<strong>Performance Metrics:</strong><br>
|
||||
<div style="margin-top: 0.3rem; line-height: 1.3;">
|
||||
<div><strong>Page Load:</strong> ${metrics.pageLoad}ms</div>
|
||||
<div><strong>DOM Ready:</strong> ${metrics.domReady}ms</div>
|
||||
<div><strong>First Byte:</strong> ${metrics.firstByte}ms</div>
|
||||
<div><strong>Session Time:</strong> ${Math.round(metrics.uptime / 1000)}s</div>
|
||||
<div><strong>Debug Messages:</strong> ${metrics.messagesCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}, '', 'generatePerformanceHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate debug messages HTML
|
||||
*/
|
||||
generateMessagesHTML() {
|
||||
return this.safeOperation(() => {
|
||||
const filteredMessages = this.getFilteredMessages();
|
||||
|
||||
if (filteredMessages.length === 0) {
|
||||
return `
|
||||
<div style="text-align: center; padding: 1rem; color: #666; font-style: italic;">
|
||||
No ${this.messageFilter === 'all' ? '' : this.messageFilter + ' '}messages yet
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const messagesHTML = filteredMessages.slice(-20).map(msg => {
|
||||
const levelColors = {
|
||||
error: '#dc3545',
|
||||
warn: '#ffc107',
|
||||
info: '#17a2b8',
|
||||
debug: '#6c757d'
|
||||
};
|
||||
|
||||
const backgroundColor = levelColors[msg.level] || '#6c757d';
|
||||
const textColor = msg.level === 'warn' ? '#000' : '#fff';
|
||||
|
||||
return `
|
||||
<div class="debug-message" style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f8f9fa; border-left: 3px solid ${backgroundColor}; font-size: 0.7rem; border-radius: 0 3px 3px 0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.2rem;">
|
||||
<span style="background: ${backgroundColor}; color: ${textColor}; padding: 0.1rem 0.3rem; border-radius: 2px; font-size: 0.6rem; font-weight: bold;">
|
||||
${msg.category}
|
||||
</span>
|
||||
<span style="color: #666; font-size: 0.6rem;">
|
||||
${msg.displayTime}
|
||||
</span>
|
||||
</div>
|
||||
<div style="word-break: break-word; line-height: 1.2;">
|
||||
${msg.message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="messages-container" style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 3px; padding: 0.5rem; background: white;">
|
||||
${messagesHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
}, '<p>Error displaying messages</p>', 'generateMessagesHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate control buttons HTML
|
||||
*/
|
||||
generateControlButtonsHTML() {
|
||||
return `
|
||||
<div class="debug-controls" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin: 0.5rem 0;">
|
||||
<button onclick="this.closest('.debug-control').debugControl.clearMessages()"
|
||||
style="padding: 0.3rem; font-size: 0.7rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.debug-control').debugControl.exportMessages()"
|
||||
style="padding: 0.3rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
💾 Export
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.debug-control').debugControl.toggleRecording()"
|
||||
style="padding: 0.3rem; font-size: 0.7rem; background: ${this.isRecording ? '#ffc107' : '#6c757d'}; color: ${this.isRecording ? '#000' : '#fff'}; border: none; border-radius: 3px; cursor: pointer;">
|
||||
${this.isRecording ? '⏸️ Pause' : '▶️ Record'}
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.debug-control').debugControl.addTestMessage()"
|
||||
style="padding: 0.3rem; font-size: 0.7rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
🧪 Test
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate filter controls HTML
|
||||
*/
|
||||
generateFilterControlsHTML() {
|
||||
const filters = ['all', 'error', 'warn', 'info', 'debug'];
|
||||
|
||||
const filterButtons = filters.map(filter => {
|
||||
const isActive = this.messageFilter === filter;
|
||||
return `
|
||||
<button onclick="this.closest('.debug-control').debugControl.setMessageFilter('${filter}')"
|
||||
style="padding: 0.2rem 0.4rem; margin-right: 0.2rem; font-size: 0.6rem; background: ${isActive ? '#007bff' : '#e9ecef'}; color: ${isActive ? 'white' : '#495057'}; border: none; border-radius: 2px; cursor: pointer;">
|
||||
${filter.toUpperCase()}
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f1f3f4; border-radius: 3px;">
|
||||
<div style="font-size: 0.7rem; margin-bottom: 0.3rem; color: #666;">Filter:</div>
|
||||
${filterButtons}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the message display
|
||||
*/
|
||||
updateMessageDisplay() {
|
||||
return this.safeOperation(() => {
|
||||
const messagesContainer = this.element?.querySelector('.messages-container');
|
||||
if (messagesContainer) {
|
||||
const parent = messagesContainer.parentElement;
|
||||
parent.innerHTML = this.generateMessagesHTML();
|
||||
|
||||
// Auto-scroll to bottom if enabled
|
||||
if (this.autoScroll) {
|
||||
const newContainer = parent.querySelector('.messages-container');
|
||||
if (newContainer) {
|
||||
newContainer.scrollTop = newContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 'updateMessageDisplay');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all debug messages
|
||||
*/
|
||||
clearMessages() {
|
||||
this.messages = [];
|
||||
if (window.MarkitectDebugSystem) {
|
||||
window.MarkitectDebugSystem.clearMessages();
|
||||
}
|
||||
this.buildContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export debug messages to file
|
||||
*/
|
||||
exportMessages() {
|
||||
return this.safeOperation(() => {
|
||||
const exportData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
session: {
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
duration: Date.now() - this.startTime,
|
||||
messageCount: this.messages.length
|
||||
},
|
||||
system: {
|
||||
userAgent: navigator.userAgent,
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
||||
url: window.location.href
|
||||
},
|
||||
messages: this.messages
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `debug-log-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
this.addDebugMessage('EXPORT', 'Debug log exported successfully', 'info');
|
||||
|
||||
}, null, 'exportMessages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle message recording
|
||||
*/
|
||||
toggleRecording() {
|
||||
this.isRecording = !this.isRecording;
|
||||
this.buildContent();
|
||||
this.addDebugMessage('CONTROL', `Recording ${this.isRecording ? 'started' : 'paused'}`, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a test message
|
||||
*/
|
||||
addTestMessage() {
|
||||
const testMessages = [
|
||||
{ category: 'TEST', message: 'This is a test info message', level: 'info' },
|
||||
{ category: 'TEST', message: 'This is a test warning message', level: 'warn' },
|
||||
{ category: 'TEST', message: 'This is a test error message', level: 'error' },
|
||||
{ category: 'TEST', message: 'This is a test debug message', level: 'debug' }
|
||||
];
|
||||
|
||||
const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)];
|
||||
this.addDebugMessage(randomMessage.category, randomMessage.message, randomMessage.level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set message filter
|
||||
*/
|
||||
setMessageFilter(filter) {
|
||||
this.messageFilter = filter;
|
||||
this.buildContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate debug control content (called by base class buildContent)
|
||||
*/
|
||||
generateContent() {
|
||||
return this.safeOperation(() => {
|
||||
return `
|
||||
${this.generateSystemInfoHTML()}
|
||||
${this.generatePerformanceHTML()}
|
||||
${this.generateFilterControlsHTML()}
|
||||
${this.generateMessagesHTML()}
|
||||
${this.generateControlButtonsHTML()}
|
||||
|
||||
<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #eee; font-size: 0.7rem; color: #666; text-align: center;">
|
||||
Recording: ${this.isRecording ? '🟢 Active' : '🔴 Paused'} |
|
||||
Filter: ${this.messageFilter.toUpperCase()} |
|
||||
Messages: ${this.getFilteredMessages().length}/${this.messages.length}
|
||||
</div>
|
||||
`;
|
||||
}, 'Error generating debug content', 'generateContent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override buildContent to add control reference
|
||||
*/
|
||||
buildContent() {
|
||||
super.buildContent();
|
||||
|
||||
// Store reference to this control for onclick handlers
|
||||
if (this.element) {
|
||||
this.element.debugControl = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when control is destroyed
|
||||
*/
|
||||
destroy() {
|
||||
// Restore original console methods
|
||||
if (this.originalConsole) {
|
||||
console.log = this.originalConsole.log;
|
||||
console.error = this.originalConsole.error;
|
||||
console.warn = this.originalConsole.warn;
|
||||
console.info = this.originalConsole.info;
|
||||
console.debug = this.originalConsole.debug;
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global for direct usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DebugControl;
|
||||
} else {
|
||||
window.DebugControl = DebugControl;
|
||||
}
|
||||
573
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/edit-control.js
Executable file
573
examples/todohtml/_markitect/plugins/testdrive-jsui/js/controls/edit-control.js
Executable file
@@ -0,0 +1,573 @@
|
||||
/**
|
||||
* EditControl - Document Editing Tools and Actions Control
|
||||
*
|
||||
* Provides a comprehensive set of document editing tools including text formatting,
|
||||
* document actions (print, save, export), navigation helpers, and editing modes.
|
||||
* Designed to enhance the writing and editing experience within the TestDrive-JSUI
|
||||
* environment.
|
||||
*
|
||||
* Features:
|
||||
* - Document actions (print, save, export to various formats)
|
||||
* - Text formatting tools (bold, italic, headers)
|
||||
* - Navigation helpers (scroll to top/bottom, go to line)
|
||||
* - Word processing features (find/replace, word count)
|
||||
* - Accessibility tools (font size, contrast adjustment)
|
||||
* - Markdown formatting shortcuts
|
||||
*
|
||||
* Dependencies:
|
||||
* - ControlBase (base control functionality)
|
||||
*/
|
||||
|
||||
/**
|
||||
* EditControl - Comprehensive document editing control
|
||||
*
|
||||
* This control provides writers and editors with essential tools for document
|
||||
* creation and modification. It includes both basic text operations and
|
||||
* advanced features for content management and formatting.
|
||||
*/
|
||||
class EditControl extends ControlBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure for editing functionality
|
||||
this.config = {
|
||||
icon: '✏️',
|
||||
title: 'Edit',
|
||||
className: 'edit-control',
|
||||
defaultContent: 'Document editing tools loading...',
|
||||
ariaLabel: 'Document Edit Control',
|
||||
position: 'e' // East positioning
|
||||
};
|
||||
|
||||
// Edit control state
|
||||
this.editingMode = 'view'; // 'view', 'edit', 'preview'
|
||||
this.fontSize = 16;
|
||||
this.lastSaveTime = null;
|
||||
this.unsavedChanges = false;
|
||||
this.shortcuts = new Map();
|
||||
|
||||
this.initializeShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize keyboard shortcuts for editing
|
||||
*/
|
||||
initializeShortcuts() {
|
||||
this.shortcuts.set('Ctrl+S', () => this.saveDocument());
|
||||
this.shortcuts.set('Ctrl+P', () => this.printDocument());
|
||||
this.shortcuts.set('Ctrl+F', () => this.showFindDialog());
|
||||
this.shortcuts.set('Ctrl+B', () => this.toggleBold());
|
||||
this.shortcuts.set('Ctrl+I', () => this.toggleItalic());
|
||||
this.shortcuts.set('Escape', () => this.exitEditMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the main editing tools HTML
|
||||
*/
|
||||
generateEditToolsHTML() {
|
||||
return this.safeOperation(() => {
|
||||
return `
|
||||
<!-- Document Actions -->
|
||||
<div class="action-section" style="margin-bottom: 1rem;">
|
||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Document Actions</div>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.printDocument()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
||||
🖨️ Print Document
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.saveDocument()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
||||
💾 Save Changes
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.exportDocument()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
||||
📄 Export Document
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.resetAll()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
||||
🔄 Reset All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Tools -->
|
||||
<div class="navigation-section" style="margin-bottom: 1rem;">
|
||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Navigation</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
||||
<button onclick="this.closest('.edit-control').editControl.scrollToTop()"
|
||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
⬆️ Top
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.scrollToBottom()"
|
||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
⬇️ Bottom
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.showGoToLine()"
|
||||
style="width: 100%; padding: 0.4rem; margin-top: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
🎯 Go to Line
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Tools -->
|
||||
<div class="text-section" style="margin-bottom: 1rem;">
|
||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Text Tools</div>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.showFindReplace()"
|
||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
🔍 Find & Replace
|
||||
</button>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 0.3rem;">
|
||||
<button onclick="this.closest('.edit-control').editControl.increaseFontSize()"
|
||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
🔍+ Font
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.decreaseFontSize()"
|
||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
🔍- Font
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.copyLink()"
|
||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #fd7e14; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
📋 Copy Page Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Markdown Tools -->
|
||||
<div class="markdown-section" style="margin-bottom: 1rem;">
|
||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Markdown Tools</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('**', '**', 'Bold text')"
|
||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
**B**
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('*', '*', 'Italic text')"
|
||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
*I*
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('## ', '', 'Heading')"
|
||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
H2
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('- ', '', 'List item')"
|
||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
||||
•List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Info -->
|
||||
<div class="status-section" style="border-top: 1px solid #eee; padding-top: 0.5rem;">
|
||||
<div style="font-size: 0.7rem; color: #666;">
|
||||
<div>Mode: <span style="color: #007bff;">${this.editingMode}</span></div>
|
||||
<div>Font: <span style="color: #007bff;">${this.fontSize}px</span></div>
|
||||
${this.lastSaveTime ? `<div>Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}</div>` : ''}
|
||||
${this.unsavedChanges ? '<div style="color: #dc3545;">⚠️ Unsaved changes</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}, '<p>Error generating edit tools</p>', 'generateEditToolsHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the document
|
||||
*/
|
||||
printDocument() {
|
||||
return this.safeOperation(() => {
|
||||
window.print();
|
||||
|
||||
// Show feedback
|
||||
this.showActionFeedback('🖨️ Print dialog opened', '#28a745');
|
||||
}, null, 'printDocument');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save document (placeholder - would integrate with actual save system)
|
||||
*/
|
||||
saveDocument() {
|
||||
return this.safeOperation(() => {
|
||||
// In a real implementation, this would save to a backend
|
||||
this.lastSaveTime = Date.now();
|
||||
this.unsavedChanges = false;
|
||||
|
||||
// Update display
|
||||
this.buildContent();
|
||||
|
||||
// Show feedback
|
||||
this.showActionFeedback('💾 Document saved', '#007bff');
|
||||
}, null, 'saveDocument');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export document to various formats
|
||||
*/
|
||||
exportDocument() {
|
||||
return this.safeOperation(() => {
|
||||
const contentArea = document.querySelector('#markitect-content') || document.body;
|
||||
const htmlContent = contentArea.innerHTML;
|
||||
const textContent = contentArea.textContent;
|
||||
|
||||
// Create export menu
|
||||
const exportMenu = document.createElement('div');
|
||||
exportMenu.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
exportMenu.innerHTML = `
|
||||
<div style="margin-top: 0; font-weight: 600; font-size: 1.1em; color: #333; margin-bottom: 1rem;">Export Document</div>
|
||||
<button onclick="this.parentElement.exportAsHTML()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Export as HTML
|
||||
</button>
|
||||
<button onclick="this.parentElement.exportAsText()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Export as Text
|
||||
</button>
|
||||
<button onclick="this.parentElement.exportAsMarkdown()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: #6f42c1; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Export as Markdown
|
||||
</button>
|
||||
<button onclick="document.body.removeChild(this.parentElement)" style="width: 100%; padding: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add export functions
|
||||
exportMenu.exportAsHTML = () => {
|
||||
this.downloadFile(htmlContent, 'document.html', 'text/html');
|
||||
document.body.removeChild(exportMenu);
|
||||
};
|
||||
|
||||
exportMenu.exportAsText = () => {
|
||||
this.downloadFile(textContent, 'document.txt', 'text/plain');
|
||||
document.body.removeChild(exportMenu);
|
||||
};
|
||||
|
||||
exportMenu.exportAsMarkdown = () => {
|
||||
// Simple HTML to Markdown conversion (basic)
|
||||
let markdown = htmlContent
|
||||
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
||||
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
||||
.replace(/<[^>]*>/g, ''); // Remove remaining HTML tags
|
||||
|
||||
this.downloadFile(markdown, 'document.md', 'text/markdown');
|
||||
document.body.removeChild(exportMenu);
|
||||
};
|
||||
|
||||
document.body.appendChild(exportMenu);
|
||||
|
||||
}, null, 'exportDocument');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file with given content
|
||||
*/
|
||||
downloadFile(content, filename, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all changes and restore document to original state
|
||||
*/
|
||||
resetAll() {
|
||||
return this.safeOperation(() => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = window.confirm(
|
||||
'Reset all changes?\n\nThis will:\n' +
|
||||
'• Restore document to original state\n' +
|
||||
'• Clear all unsaved changes\n' +
|
||||
'• Reset font size and other settings\n\n' +
|
||||
'This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
this.showActionFeedback('🚫 Reset cancelled', '#6c757d');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset edit control state
|
||||
this.fontSize = 16;
|
||||
this.editingMode = 'view';
|
||||
this.unsavedChanges = false;
|
||||
this.lastSaveTime = null;
|
||||
|
||||
// Reset font size
|
||||
this.applyFontSize();
|
||||
|
||||
// Clear any highlights
|
||||
document.querySelectorAll('.edit-highlight').forEach(el => {
|
||||
el.outerHTML = el.innerHTML;
|
||||
});
|
||||
|
||||
// Try to reset sections if SectionManager is available
|
||||
if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') {
|
||||
window.sectionManager.resetAllSections();
|
||||
}
|
||||
|
||||
// Try to reset document controls if available
|
||||
if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') {
|
||||
window.documentControls.resetAllChanges();
|
||||
}
|
||||
|
||||
// Clear any debug messages if debug control is available
|
||||
if (window.debugControl && typeof window.debugControl.clearMessages === 'function') {
|
||||
window.debugControl.clearMessages();
|
||||
}
|
||||
|
||||
// Reload the page as ultimate fallback
|
||||
if (window.confirm('Reload page to complete reset?')) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the control display
|
||||
this.buildContent();
|
||||
|
||||
// Show feedback
|
||||
this.showActionFeedback('🔄 All changes reset', '#ffc107', '#212529');
|
||||
|
||||
}, null, 'resetAll');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to top of document
|
||||
*/
|
||||
scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
this.showActionFeedback('⬆️ Scrolled to top', '#6c757d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to bottom of document
|
||||
*/
|
||||
scrollToBottom() {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
this.showActionFeedback('⬇️ Scrolled to bottom', '#6c757d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show go to line dialog
|
||||
*/
|
||||
showGoToLine() {
|
||||
const lineNumber = prompt('Go to line number:');
|
||||
if (lineNumber && !isNaN(lineNumber)) {
|
||||
// Simple implementation - scroll to approximate position
|
||||
const totalHeight = document.body.scrollHeight;
|
||||
const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight;
|
||||
window.scrollTo({ top: approximatePosition, behavior: 'smooth' });
|
||||
this.showActionFeedback(`🎯 Went to line ${lineNumber}`, '#6c757d');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show find and replace dialog
|
||||
*/
|
||||
showFindReplace() {
|
||||
const searchTerm = prompt('Find text:');
|
||||
if (searchTerm) {
|
||||
// Simple highlight implementation
|
||||
this.highlightText(searchTerm);
|
||||
this.showActionFeedback(`🔍 Highlighted "${searchTerm}"`, '#ffc107', '#000');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight text in the document
|
||||
*/
|
||||
highlightText(searchTerm) {
|
||||
return this.safeOperation(() => {
|
||||
// Remove previous highlights
|
||||
document.querySelectorAll('.edit-highlight').forEach(el => {
|
||||
el.outerHTML = el.innerHTML;
|
||||
});
|
||||
|
||||
// Add new highlights
|
||||
const contentArea = document.querySelector('#markitect-content') || document.body;
|
||||
const walker = document.createTreeWalker(
|
||||
contentArea,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const textNodes = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
|
||||
textNodes.forEach(textNode => {
|
||||
const parent = textNode.parentNode;
|
||||
const text = textNode.textContent;
|
||||
if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
||||
const highlightedHTML = text.replace(regex, '<span class="edit-highlight" style="background-color: yellow; padding: 0.1rem;">$1</span>');
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = highlightedHTML;
|
||||
while (wrapper.firstChild) {
|
||||
parent.insertBefore(wrapper.firstChild, textNode);
|
||||
}
|
||||
parent.removeChild(textNode);
|
||||
}
|
||||
});
|
||||
}, null, 'highlightText');
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase font size
|
||||
*/
|
||||
increaseFontSize() {
|
||||
this.fontSize = Math.min(this.fontSize + 2, 24);
|
||||
this.applyFontSize();
|
||||
this.buildContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease font size
|
||||
*/
|
||||
decreaseFontSize() {
|
||||
this.fontSize = Math.max(this.fontSize - 2, 12);
|
||||
this.applyFontSize();
|
||||
this.buildContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply font size to document
|
||||
*/
|
||||
applyFontSize() {
|
||||
const contentArea = document.querySelector('#markitect-content') || document.body;
|
||||
contentArea.style.fontSize = `${this.fontSize}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy page link to clipboard
|
||||
*/
|
||||
copyLink() {
|
||||
return this.safeOperation(() => {
|
||||
const url = window.location.href;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.showActionFeedback('📋 Link copied to clipboard', '#fd7e14');
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
prompt('Copy this link:', url);
|
||||
this.showActionFeedback('📋 Link displayed for copying', '#fd7e14');
|
||||
}
|
||||
}, null, 'copyLink');
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert markdown formatting
|
||||
*/
|
||||
insertMarkdown(prefix, suffix, placeholder) {
|
||||
// This would integrate with an actual text editor
|
||||
// For now, just show what would be inserted
|
||||
const text = `${prefix}${placeholder}${suffix}`;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
this.showActionFeedback(`📋 Copied: ${text}`, '#495057');
|
||||
} else {
|
||||
prompt('Markdown to copy:', text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show action feedback message
|
||||
*/
|
||||
showActionFeedback(message, backgroundColor, color = 'white') {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${backgroundColor};
|
||||
color: ${color};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
font-size: 0.8rem;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
`;
|
||||
feedback.textContent = message;
|
||||
document.body.appendChild(feedback);
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
document.body.removeChild(feedback);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the control content
|
||||
* Override of base class method to provide edit-specific functionality
|
||||
*/
|
||||
/**
|
||||
* Generate edit control content (called by base class buildContent)
|
||||
*/
|
||||
generateContent() {
|
||||
return this.safeOperation(() => {
|
||||
return this.generateEditToolsHTML();
|
||||
}, 'Error generating edit content', 'generateContent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override buildContent to add control reference
|
||||
*/
|
||||
buildContent() {
|
||||
super.buildContent();
|
||||
|
||||
// Store reference to this control for onclick handlers
|
||||
if (this.element) {
|
||||
this.element.editControl = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit edit mode
|
||||
*/
|
||||
exitEditMode() {
|
||||
this.editingMode = 'view';
|
||||
this.buildContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global for direct usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = EditControl;
|
||||
} else {
|
||||
window.EditControl = EditControl;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* StatusControl - Document Statistics and Change Tracking Control
|
||||
*
|
||||
* Provides real-time document statistics including word count, character count,
|
||||
* reading time estimation, and change tracking. Monitors document modifications
|
||||
* and provides insights into document structure and content metrics.
|
||||
*
|
||||
* Features:
|
||||
* - Real-time word and character counting
|
||||
* - Reading time estimation based on content
|
||||
* - Document structure analysis (headings, paragraphs, lists)
|
||||
* - Change tracking with before/after comparisons
|
||||
* - Content complexity metrics
|
||||
* - Export functionality for statistics
|
||||
*
|
||||
* Dependencies:
|
||||
* - ControlBase (base control functionality)
|
||||
*/
|
||||
|
||||
/**
|
||||
* StatusControl - Document statistics and monitoring control
|
||||
*
|
||||
* This control continuously monitors the document for changes and provides
|
||||
* detailed statistics about content, structure, and reading metrics.
|
||||
* Useful for writers, editors, and content creators.
|
||||
*/
|
||||
class StatusControl extends ControlBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure for status functionality
|
||||
this.config = {
|
||||
icon: '📊',
|
||||
title: 'Status',
|
||||
className: 'status-control',
|
||||
defaultContent: 'Loading document statistics...',
|
||||
ariaLabel: 'Document Status Control',
|
||||
position: 'e' // East positioning
|
||||
};
|
||||
|
||||
// Status tracking state
|
||||
this.stats = {
|
||||
characters: 0,
|
||||
charactersNoSpaces: 0,
|
||||
words: 0,
|
||||
sentences: 0,
|
||||
paragraphs: 0,
|
||||
headings: 0,
|
||||
lists: 0,
|
||||
images: 0,
|
||||
links: 0,
|
||||
readingTimeMinutes: 0
|
||||
};
|
||||
|
||||
this.previousStats = { ...this.stats };
|
||||
this.lastUpdateTime = null;
|
||||
this.updateInterval = null;
|
||||
this.wordsPerMinute = 200; // Average reading speed
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and count document content statistics
|
||||
*/
|
||||
analyzeDocument() {
|
||||
return this.safeOperation(() => {
|
||||
const contentArea = document.querySelector('#markitect-content') || document.body;
|
||||
const textContent = contentArea.textContent || '';
|
||||
|
||||
// Basic text statistics
|
||||
this.stats.characters = textContent.length;
|
||||
this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length;
|
||||
|
||||
// Word counting (more accurate)
|
||||
const words = textContent.trim().split(/\s+/).filter(word => word.length > 0);
|
||||
this.stats.words = words.length;
|
||||
|
||||
// Sentence counting (approximate)
|
||||
const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
||||
this.stats.sentences = sentences.length;
|
||||
|
||||
// Structural elements
|
||||
this.stats.paragraphs = contentArea.querySelectorAll('p').length;
|
||||
this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
|
||||
this.stats.lists = contentArea.querySelectorAll('ul, ol').length;
|
||||
this.stats.images = contentArea.querySelectorAll('img').length;
|
||||
this.stats.links = contentArea.querySelectorAll('a').length;
|
||||
|
||||
// Reading time calculation
|
||||
this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute);
|
||||
|
||||
this.lastUpdateTime = Date.now();
|
||||
return this.stats;
|
||||
|
||||
}, this.stats, 'analyzeDocument');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate changes since last analysis
|
||||
*/
|
||||
calculateChanges() {
|
||||
return this.safeOperation(() => {
|
||||
const changes = {};
|
||||
for (const [key, currentValue] of Object.entries(this.stats)) {
|
||||
const previousValue = this.previousStats[key] || 0;
|
||||
const difference = currentValue - previousValue;
|
||||
changes[key] = {
|
||||
current: currentValue,
|
||||
previous: previousValue,
|
||||
change: difference,
|
||||
hasChanged: difference !== 0
|
||||
};
|
||||
}
|
||||
return changes;
|
||||
}, {}, 'calculateChanges');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format statistics for display
|
||||
*/
|
||||
formatStatistics() {
|
||||
return this.safeOperation(() => {
|
||||
const changes = this.calculateChanges();
|
||||
|
||||
const formatChange = (changeData) => {
|
||||
if (!changeData.hasChanged) return '';
|
||||
const sign = changeData.change > 0 ? '+' : '';
|
||||
const color = changeData.change > 0 ? '#28a745' : '#dc3545';
|
||||
return `<span style="color: ${color}; font-size: 0.7rem;"> (${sign}${changeData.change})</span>`;
|
||||
};
|
||||
|
||||
const formatNumber = (num) => num.toLocaleString();
|
||||
|
||||
return `
|
||||
<div class="stats-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<div class="stat-item">
|
||||
<strong>Words:</strong><br>
|
||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.words)}</span>
|
||||
${formatChange(changes.words)}
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<strong>Characters:</strong><br>
|
||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.characters)}</span>
|
||||
${formatChange(changes.characters)}
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<strong>Reading Time:</strong><br>
|
||||
<span style="font-size: 1.1em; color: #007bff;">${this.stats.readingTimeMinutes} min</span>
|
||||
${formatChange(changes.readingTimeMinutes)}
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<strong>Sentences:</strong><br>
|
||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.sentences)}</span>
|
||||
${formatChange(changes.sentences)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="structure-stats" style="border-top: 1px solid #eee; padding-top: 0.5rem; margin-bottom: 1rem;">
|
||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; font-weight: 600; color: #555;">Document Structure</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
||||
<span>Paragraphs:</span>
|
||||
<span>${this.stats.paragraphs}${formatChange(changes.paragraphs)}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
||||
<span>Headings:</span>
|
||||
<span>${this.stats.headings}${formatChange(changes.headings)}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
||||
<span>Lists:</span>
|
||||
<span>${this.stats.lists}${formatChange(changes.lists)}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
||||
<span>Images:</span>
|
||||
<span>${this.stats.images}${formatChange(changes.images)}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
||||
<span>Links:</span>
|
||||
<span>${this.stats.links}${formatChange(changes.links)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center;">
|
||||
<button onclick="this.closest('.status-control').statusControl.refreshStats()"
|
||||
style="padding: 0.3rem 0.6rem; margin-right: 0.3rem; font-size: 0.7rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
|
||||
<button onclick="this.closest('.status-control').statusControl.exportStats()"
|
||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
📊 Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.lastUpdateTime ? `
|
||||
<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #eee; font-size: 0.7rem; color: #666; text-align: center;">
|
||||
Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
}, '<p>Error displaying statistics</p>', 'formatStatistics');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh statistics and update display
|
||||
*/
|
||||
refreshStats() {
|
||||
return this.safeOperation(() => {
|
||||
// Save current stats as previous
|
||||
this.previousStats = { ...this.stats };
|
||||
|
||||
// Analyze document
|
||||
this.analyzeDocument();
|
||||
|
||||
// Update display
|
||||
this.buildContent();
|
||||
|
||||
// Show success feedback
|
||||
const refreshBtn = this.element?.querySelector('button');
|
||||
if (refreshBtn) {
|
||||
const originalText = refreshBtn.innerHTML;
|
||||
refreshBtn.innerHTML = '✅ Updated';
|
||||
|
||||
setTimeout(() => {
|
||||
refreshBtn.innerHTML = originalText;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
}, null, 'refreshStats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export statistics to various formats
|
||||
*/
|
||||
exportStats() {
|
||||
return this.safeOperation(() => {
|
||||
const exportData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
document: {
|
||||
title: document.title || 'Untitled Document',
|
||||
url: window.location.href
|
||||
},
|
||||
statistics: this.stats,
|
||||
metadata: {
|
||||
wordsPerMinute: this.wordsPerMinute,
|
||||
analysisDate: new Date(this.lastUpdateTime).toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
// Create downloadable JSON
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
// Create temporary download link
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `document-stats-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Show feedback
|
||||
const exportBtn = this.element?.querySelector('button:last-child');
|
||||
if (exportBtn) {
|
||||
const originalText = exportBtn.innerHTML;
|
||||
exportBtn.innerHTML = '✅ Exported';
|
||||
exportBtn.style.background = '#28a745';
|
||||
|
||||
setTimeout(() => {
|
||||
exportBtn.innerHTML = originalText;
|
||||
exportBtn.style.background = '#28a745';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
}, null, 'exportStats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reading difficulty score (Flesch Reading Ease approximation)
|
||||
*/
|
||||
calculateReadabilityScore() {
|
||||
return this.safeOperation(() => {
|
||||
if (this.stats.sentences === 0 || this.stats.words === 0) {
|
||||
return { score: 0, level: 'Unknown' };
|
||||
}
|
||||
|
||||
const avgWordsPerSentence = this.stats.words / this.stats.sentences;
|
||||
const avgSyllablesPerWord = 1.5; // Simplified approximation
|
||||
|
||||
// Flesch Reading Ease formula (simplified)
|
||||
const score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
|
||||
|
||||
let level;
|
||||
if (score >= 90) level = 'Very Easy';
|
||||
else if (score >= 80) level = 'Easy';
|
||||
else if (score >= 70) level = 'Fairly Easy';
|
||||
else if (score >= 60) level = 'Standard';
|
||||
else if (score >= 50) level = 'Fairly Difficult';
|
||||
else if (score >= 30) level = 'Difficult';
|
||||
else level = 'Very Difficult';
|
||||
|
||||
return { score: Math.round(score), level };
|
||||
}, { score: 0, level: 'Unknown' }, 'calculateReadabilityScore');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the control content
|
||||
* Override of base class method to provide status-specific functionality
|
||||
*/
|
||||
/**
|
||||
* Generate status control content (called by base class buildContent)
|
||||
*/
|
||||
generateContent() {
|
||||
// Analyze document first
|
||||
this.analyzeDocument();
|
||||
|
||||
return this.safeOperation(() => {
|
||||
return this.formatStatistics();
|
||||
}, 'Error generating status content', 'generateContent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override buildContent to add control reference and auto-refresh
|
||||
*/
|
||||
buildContent() {
|
||||
super.buildContent();
|
||||
|
||||
// Store reference to this control for onclick handlers
|
||||
if (this.element) {
|
||||
this.element.statusControl = this;
|
||||
}
|
||||
|
||||
// Set up auto-refresh for dynamic content
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.refreshStats();
|
||||
}, 10000); // Update every 10 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when control is destroyed
|
||||
*/
|
||||
destroy() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global for direct usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = StatusControl;
|
||||
} else {
|
||||
window.StatusControl = StatusControl;
|
||||
}
|
||||
290
examples/todohtml/_markitect/plugins/testdrive-jsui/js/core/debug-system.js
Executable file
290
examples/todohtml/_markitect/plugins/testdrive-jsui/js/core/debug-system.js
Executable file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Independent Debug System for Markitect
|
||||
* Uses IndexedDB for persistence and provides selection-based filtering
|
||||
*/
|
||||
class MarkitectDebugSystem {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.messages = [];
|
||||
this.maxMessages = 1000;
|
||||
this.isEnabled = true;
|
||||
this.subscribers = [];
|
||||
|
||||
// Selection and filtering system
|
||||
this.selectionCriteria = {
|
||||
includeDocumentEvents: true,
|
||||
includeSystemEvents: false,
|
||||
includeControlEvents: true,
|
||||
includeEditingEvents: true,
|
||||
includeNavigationEvents: false,
|
||||
includedHeadings: new Set(), // Track which document headings to monitor
|
||||
excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Initialize IndexedDB for persistence
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('MarkitectDebugDB', 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
this.loadMessages().then(resolve);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('messages')) {
|
||||
const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
store.createIndex('category', 'category', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Add a debug message with selection filtering
|
||||
async addMessage(message, category = 'INFO', source = 'System', context = {}) {
|
||||
// Check if this message should be included based on selection criteria
|
||||
if (!this.shouldIncludeMessage(message, category, source, context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageObj = {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: String(message),
|
||||
category: category.toUpperCase(),
|
||||
source: String(source),
|
||||
context: context || {},
|
||||
id: null // Will be set by IndexedDB
|
||||
};
|
||||
|
||||
// Store in IndexedDB if available
|
||||
if (this.db) {
|
||||
try {
|
||||
await this.saveMessage(messageObj);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save debug message to IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
this.messages.unshift(messageObj);
|
||||
|
||||
// Limit memory storage
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = this.messages.slice(0, this.maxMessages);
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
this.notifySubscribers(messageObj);
|
||||
|
||||
// Console output for development
|
||||
const consoleMethod = category.toLowerCase() === 'error' ? 'error' :
|
||||
category.toLowerCase() === 'warning' ? 'warn' : 'log';
|
||||
console[consoleMethod](`[${source}] ${message}`, context);
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
// Selection filtering logic
|
||||
shouldIncludeMessage(message, category, source, context) {
|
||||
if (!this.isEnabled) return false;
|
||||
|
||||
const eventType = context.eventType || 'UNKNOWN';
|
||||
const criteria = this.selectionCriteria;
|
||||
|
||||
// Check event type filters
|
||||
switch (eventType.toUpperCase()) {
|
||||
case 'DOCUMENT':
|
||||
if (!criteria.includeDocumentEvents) return false;
|
||||
break;
|
||||
case 'SYSTEM':
|
||||
if (!criteria.includeSystemEvents) return false;
|
||||
break;
|
||||
case 'CONTROL':
|
||||
if (!criteria.includeControlEvents) return false;
|
||||
break;
|
||||
case 'EDITING':
|
||||
if (!criteria.includeEditingEvents) return false;
|
||||
break;
|
||||
case 'NAVIGATION':
|
||||
if (!criteria.includeNavigationEvents) return false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check excluded sources
|
||||
if (criteria.excludedSources.has(source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check heading-specific filtering
|
||||
if (context.sectionId && criteria.includedHeadings.size > 0) {
|
||||
const sectionElement = document.getElementById(context.sectionId);
|
||||
if (sectionElement) {
|
||||
const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save message to IndexedDB
|
||||
async saveMessage(messageObj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readwrite');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.add(messageObj);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load messages from IndexedDB
|
||||
async loadMessages() {
|
||||
if (!this.db) return [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readonly');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.messages = request.result.reverse(); // Most recent first
|
||||
resolve(this.messages);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all messages
|
||||
async clearMessages() {
|
||||
this.messages = [];
|
||||
|
||||
if (this.db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readwrite');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get filtered messages
|
||||
getMessages(filter = {}) {
|
||||
let filteredMessages = [...this.messages];
|
||||
|
||||
if (filter.category) {
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
msg.category.toLowerCase() === filter.category.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.source) {
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
msg.source.toLowerCase().includes(filter.source.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.since) {
|
||||
const sinceDate = new Date(filter.since);
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
new Date(msg.timestamp) >= sinceDate
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.limit) {
|
||||
filteredMessages = filteredMessages.slice(0, filter.limit);
|
||||
}
|
||||
|
||||
return filteredMessages;
|
||||
}
|
||||
|
||||
// Update selection criteria
|
||||
updateSelectionCriteria(updates) {
|
||||
Object.assign(this.selectionCriteria, updates);
|
||||
this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria });
|
||||
}
|
||||
|
||||
// Add heading to monitoring
|
||||
addHeadingToMonitoring(headingText) {
|
||||
this.selectionCriteria.includedHeadings.add(headingText);
|
||||
}
|
||||
|
||||
// Remove heading from monitoring
|
||||
removeHeadingFromMonitoring(headingText) {
|
||||
this.selectionCriteria.includedHeadings.delete(headingText);
|
||||
}
|
||||
|
||||
// Scan document for available headings
|
||||
scanDocumentHeadings() {
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
return Array.from(headings)
|
||||
.map(h => h.textContent.trim())
|
||||
.filter(text => text.length > 0 && !text.toLowerCase().includes('control'));
|
||||
}
|
||||
|
||||
// Subscribe to debug messages
|
||||
subscribe(callback) {
|
||||
this.subscribers.push(callback);
|
||||
return () => {
|
||||
const index = this.subscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.subscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify all subscribers
|
||||
notifySubscribers(message) {
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(message);
|
||||
} catch (error) {
|
||||
console.error('Debug subscriber error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle debug system
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled;
|
||||
this.addMessage(
|
||||
`Debug system ${enabled ? 'enabled' : 'disabled'}`,
|
||||
'INFO',
|
||||
'DebugSystem',
|
||||
{ eventType: 'SYSTEM' }
|
||||
);
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const stats = {
|
||||
total: this.messages.length,
|
||||
byCategory: {},
|
||||
bySource: {},
|
||||
enabled: this.isEnabled,
|
||||
criteria: { ...this.selectionCriteria }
|
||||
};
|
||||
|
||||
this.messages.forEach(msg => {
|
||||
stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1;
|
||||
stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and expose globally
|
||||
window.MarkitectDebugSystem = new MarkitectDebugSystem();
|
||||
544
examples/todohtml/_markitect/plugins/testdrive-jsui/js/core/section-manager.js
Executable file
544
examples/todohtml/_markitect/plugins/testdrive-jsui/js/core/section-manager.js
Executable file
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* SectionManager Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Manages the collection of sections and their state transitions.
|
||||
*
|
||||
* Dependencies:
|
||||
* - EditState enum (imported)
|
||||
* - SectionType enum (imported)
|
||||
* - Section class (imported)
|
||||
* - debug function (imported)
|
||||
*/
|
||||
|
||||
// Import dependencies - these will be separate modules
|
||||
const EditState = Object.freeze({
|
||||
ORIGINAL: 'original',
|
||||
EDITING: 'editing',
|
||||
MODIFIED: 'modified',
|
||||
SAVED: 'saved'
|
||||
});
|
||||
|
||||
const SectionType = Object.freeze({
|
||||
HEADING: 'heading',
|
||||
PARAGRAPH: 'paragraph',
|
||||
LIST: 'list',
|
||||
CODE: 'code',
|
||||
QUOTE: 'quote',
|
||||
TABLE: 'table',
|
||||
HR: 'hr',
|
||||
IMAGE: 'image'
|
||||
});
|
||||
|
||||
// Debug function (will be extracted to utils)
|
||||
function debug(message, category = 'INFO') {
|
||||
// Simple console debug for now - will be enhanced later
|
||||
console.log(`DEBUG ${category}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section Class - manages individual section state and content
|
||||
*/
|
||||
class Section {
|
||||
constructor(id, markdown, type) {
|
||||
this.id = id;
|
||||
this.originalMarkdown = markdown;
|
||||
this.currentMarkdown = markdown;
|
||||
this.editingMarkdown = markdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.type = type;
|
||||
this.state = EditState.ORIGINAL;
|
||||
this.domElement = null;
|
||||
this.lastSaved = null;
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
static generateId(markdown, position, strategy = 'hash', parentId = null) {
|
||||
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
|
||||
}
|
||||
|
||||
static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
|
||||
const sanitizedContent = this.sanitizeContentForId(markdown);
|
||||
const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
|
||||
const sectionType = this.detectType(markdown);
|
||||
|
||||
switch (strategy) {
|
||||
case 'timestamp':
|
||||
return this.generateTimestampId(normalizedContent, position, sectionType);
|
||||
case 'sequential':
|
||||
return this.generateSequentialId(normalizedContent, position, sectionType);
|
||||
case 'hierarchical':
|
||||
return this.generateHierarchicalId(normalizedContent, position, parentId);
|
||||
case 'hash':
|
||||
default:
|
||||
return this.generateAdvancedId(normalizedContent, position, sectionType);
|
||||
}
|
||||
}
|
||||
|
||||
static generateAdvancedId(content, position, sectionType) {
|
||||
const contentHash = this.generateCryptoHash(content);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const positionHex = position.toString(16).padStart(2, '0');
|
||||
|
||||
return `section-${typePrefix}-${contentHash}-${positionHex}`;
|
||||
}
|
||||
|
||||
static generateCryptoHash(content) {
|
||||
let hash = 0;
|
||||
if (content.length === 0) return '00000000';
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
|
||||
return hexHash.substring(0, 8);
|
||||
}
|
||||
|
||||
static normalizeContentForHashing(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
static sanitizeContentForId(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/[^\w\s\-_.#]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
|
||||
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
|
||||
}
|
||||
|
||||
static generateSequentialId(content, position, sectionType = 'paragraph') {
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const seqNumber = (position || 0).toString().padStart(3, '0');
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
|
||||
return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
|
||||
}
|
||||
|
||||
static generateHierarchicalId(content, position, parentId = null) {
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
|
||||
|
||||
if (parentId) {
|
||||
const childIndex = (position || 0).toString().padStart(2, '0');
|
||||
return `${parentId}-child-${childIndex}-${contentHash}`;
|
||||
} else {
|
||||
return `section-root-${position || 0}-${contentHash}`;
|
||||
}
|
||||
}
|
||||
|
||||
static detectType(markdown) {
|
||||
if (!markdown || typeof markdown !== 'string') {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const content = markdown.replace(/^\n+|\n+$/g, '');
|
||||
if (!content) {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
|
||||
// Detection order matters - most specific first
|
||||
if (this.isHeading(trimmed)) {
|
||||
return SectionType.HEADING;
|
||||
}
|
||||
|
||||
if (this.isImage(trimmed)) {
|
||||
return SectionType.IMAGE;
|
||||
}
|
||||
|
||||
if (this.isCodeBlock(trimmed)) {
|
||||
return SectionType.CODE;
|
||||
}
|
||||
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
static isHeading(trimmed) {
|
||||
const headingPattern = /^#{1,6}\s+.+/;
|
||||
return headingPattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isImage(trimmed) {
|
||||
const imagePattern = /!\[.*?\]\([^)]+\)/;
|
||||
return imagePattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isCodeBlock(trimmed) {
|
||||
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes('```') || trimmed.includes('~~~')) {
|
||||
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
|
||||
if (codeBlockPattern.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
if (this.state === EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is already being edited`);
|
||||
}
|
||||
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
||||
this.state = EditState.EDITING;
|
||||
return this.editingMarkdown;
|
||||
}
|
||||
|
||||
updateContent(markdown) {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = markdown;
|
||||
}
|
||||
|
||||
acceptChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.currentMarkdown = this.editingMarkdown;
|
||||
this.editingMarkdown = null;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.SAVED;
|
||||
this.lastSaved = new Date();
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
|
||||
cancelChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = null;
|
||||
if (this.pendingMarkdown !== null) {
|
||||
this.state = EditState.MODIFIED;
|
||||
return this.pendingMarkdown;
|
||||
} else if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
return this.currentMarkdown;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
||||
this.pendingMarkdown = this.editingMarkdown;
|
||||
this.state = EditState.MODIFIED;
|
||||
} else {
|
||||
this.pendingMarkdown = null;
|
||||
if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
}
|
||||
}
|
||||
|
||||
this.editingMarkdown = null;
|
||||
return this.state;
|
||||
}
|
||||
|
||||
resetToOriginal() {
|
||||
this.currentMarkdown = this.originalMarkdown;
|
||||
this.editingMarkdown = this.originalMarkdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.ORIGINAL;
|
||||
return this.originalMarkdown;
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return this.state === EditState.EDITING;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.currentMarkdown !== this.originalMarkdown;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
id: this.id,
|
||||
state: this.state,
|
||||
hasChanges: this.hasChanges(),
|
||||
isEditing: this.isEditing(),
|
||||
contentLength: this.currentMarkdown.length,
|
||||
lastSaved: this.lastSaved,
|
||||
type: this.type,
|
||||
originalLength: this.originalMarkdown.length,
|
||||
currentLength: this.currentMarkdown.length
|
||||
};
|
||||
}
|
||||
|
||||
isImage() {
|
||||
return this.type === SectionType.IMAGE;
|
||||
}
|
||||
|
||||
redetectType(content = null) {
|
||||
const markdown = content || this.currentMarkdown;
|
||||
const oldType = this.type;
|
||||
this.type = Section.detectType(markdown);
|
||||
|
||||
if (oldType !== this.type) {
|
||||
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
|
||||
}
|
||||
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionManager - Manages the collection of sections
|
||||
*/
|
||||
class SectionManager {
|
||||
constructor() {
|
||||
this.sections = new Map();
|
||||
this.listeners = new Map();
|
||||
this.statusInterval = null;
|
||||
this.lastStatusUpdate = new Date().toISOString();
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
createSectionsFromMarkdown(markdownContent) {
|
||||
// Split content into blocks separated by double newlines
|
||||
const blocks = markdownContent.split(/\n\s*\n/);
|
||||
const sections = [];
|
||||
let position = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
const trimmedBlock = block.trim();
|
||||
if (!trimmedBlock) continue;
|
||||
|
||||
// Check if this block should be split further
|
||||
const lines = trimmedBlock.split('\n');
|
||||
let currentSection = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const isHeading = /^#{1,6}\s/.test(line.trim());
|
||||
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
|
||||
|
||||
// Each heading or image starts a new section
|
||||
if ((isHeading || isImage) && currentSection.trim()) {
|
||||
// Save the previous section
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
currentSection = line;
|
||||
} else {
|
||||
if (currentSection) currentSection += '\n';
|
||||
currentSection += line;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the final section from this block
|
||||
if (currentSection.trim()) {
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('sections-created', { sections, count: sections.length });
|
||||
return sections;
|
||||
}
|
||||
|
||||
startEditing(sectionId) {
|
||||
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
|
||||
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
if (section.isEditing()) {
|
||||
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
|
||||
return section.editingMarkdown;
|
||||
}
|
||||
|
||||
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
|
||||
const content = section.startEdit();
|
||||
|
||||
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
|
||||
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
||||
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
updateContent(sectionId, markdown) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const oldType = section.type;
|
||||
section.updateContent(markdown);
|
||||
const newType = section.redetectType(markdown);
|
||||
|
||||
const eventData = {
|
||||
sectionId,
|
||||
markdown,
|
||||
section: section.getStatus(),
|
||||
typeChanged: oldType !== newType,
|
||||
oldType,
|
||||
newType
|
||||
};
|
||||
|
||||
this.emit('content-updated', eventData);
|
||||
|
||||
if (oldType !== newType) {
|
||||
this.emit('section-type-changed', {
|
||||
sectionId,
|
||||
oldType,
|
||||
newType,
|
||||
section: section.getStatus()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
acceptChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.acceptChanges();
|
||||
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
cancelChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.cancelChanges();
|
||||
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
resetSection(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.resetToOriginal();
|
||||
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
getDocumentMarkdown() {
|
||||
const sortedSections = Array.from(this.sections.values())
|
||||
.sort((a, b) => a.created - b.created);
|
||||
|
||||
return sortedSections.map(section => section.currentMarkdown).join('\n\n');
|
||||
}
|
||||
|
||||
getAllSections() {
|
||||
return Array.from(this.sections.values());
|
||||
}
|
||||
|
||||
getDocumentStatus() {
|
||||
const sections = Array.from(this.sections.values());
|
||||
const editingSections = sections.filter(section => section.isEditing).length;
|
||||
|
||||
return {
|
||||
totalSections: sections.length,
|
||||
editingSections: editingSections
|
||||
};
|
||||
}
|
||||
|
||||
extractHeadings(content) {
|
||||
if (!content) return [];
|
||||
const lines = content.split('\n');
|
||||
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
|
||||
}
|
||||
|
||||
handleSectionSplit(sectionId, newContent) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
// Remove the original section
|
||||
this.sections.delete(sectionId);
|
||||
|
||||
// Create new sections from the content
|
||||
const newSections = this.createSectionsFromMarkdown(newContent);
|
||||
|
||||
// Emit section-split event
|
||||
this.emit('section-split', {
|
||||
originalSectionId: sectionId,
|
||||
newSections: newSections,
|
||||
count: newSections.length
|
||||
});
|
||||
|
||||
return newSections;
|
||||
}
|
||||
|
||||
createSectionsFromContent(content) {
|
||||
return this.createSectionsFromMarkdown(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SectionManager, Section, EditState, SectionType };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SectionManager = SectionManager;
|
||||
window.Section = Section;
|
||||
window.EditState = EditState;
|
||||
window.SectionType = SectionType;
|
||||
}
|
||||
287
examples/todohtml/_markitect/plugins/testdrive-jsui/js/main-updated.js
Executable file
287
examples/todohtml/_markitect/plugins/testdrive-jsui/js/main-updated.js
Executable file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Main Markitect JavaScript Entry Point - Clean Architecture Version
|
||||
*
|
||||
* Uses ONLY the JSON configuration interface - NO Python-generated JavaScript!
|
||||
* Initializes all controls and systems when document is ready
|
||||
* Implements graceful degradation for missing dependencies
|
||||
*/
|
||||
|
||||
// Main application module
|
||||
const MarkitectMain = {
|
||||
initialized: false,
|
||||
config: null,
|
||||
|
||||
// Initialize the complete application
|
||||
initialize: function() {
|
||||
if (this.initialized) {
|
||||
console.log('⚠️ MarkitectMain already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🚀 MarkitectMain initializing...');
|
||||
|
||||
try {
|
||||
// Get configuration - if not loaded, use defaults
|
||||
this.config = window.markitectConfig;
|
||||
if (!this.config || !this.config.loaded) {
|
||||
console.warn('⚠️ Configuration not loaded, proceeding with defaults');
|
||||
this.config = {
|
||||
markdownContent: document.querySelector('#markdown-content')?.textContent || '',
|
||||
mode: 'edit',
|
||||
theme: 'github'
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize core systems
|
||||
this.initializeCoreComponents();
|
||||
this.initializeControlPanels();
|
||||
this.setupEventHandlers();
|
||||
this.renderContent();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('✅ MarkitectMain initialization complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ MarkitectMain initialization failed:', error);
|
||||
this.fallbackMode();
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize core modular components
|
||||
initializeCoreComponents: function() {
|
||||
console.log('🔧 Initializing core components...');
|
||||
|
||||
const container = document.getElementById('markdown-content') || document.body;
|
||||
|
||||
// Initialize section manager
|
||||
if (typeof SectionManager !== 'undefined') {
|
||||
this.sectionManager = new SectionManager();
|
||||
console.log('✅ SectionManager initialized');
|
||||
} else {
|
||||
throw new Error('SectionManager not available');
|
||||
}
|
||||
|
||||
// Initialize DOM renderer
|
||||
if (typeof DOMRenderer !== 'undefined') {
|
||||
this.domRenderer = new DOMRenderer(this.sectionManager, container);
|
||||
console.log('✅ DOMRenderer initialized');
|
||||
} else {
|
||||
throw new Error('DOMRenderer not available');
|
||||
}
|
||||
|
||||
// Initialize debug panel
|
||||
if (typeof DebugPanel !== 'undefined') {
|
||||
this.debugPanel = new DebugPanel();
|
||||
console.log('✅ DebugPanel initialized');
|
||||
}
|
||||
|
||||
// Initialize document controls
|
||||
if (typeof DocumentControls !== 'undefined') {
|
||||
this.documentControls = new DocumentControls();
|
||||
this.documentControls.create();
|
||||
console.log('✅ DocumentControls initialized');
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize enhanced control panels with compass positioning
|
||||
initializeControlPanels: function() {
|
||||
console.log('🎛️ Initializing enhanced control panels with compass positioning...');
|
||||
|
||||
// ContentsControl (Northwest)
|
||||
if (typeof ContentsControl !== 'undefined') {
|
||||
this.contentsControl = new ContentsControl();
|
||||
this.contentsControl.config.position = 'nw';
|
||||
this.contentsControl.show();
|
||||
window.contentsControl = this.contentsControl;
|
||||
console.log('✅ ContentsControl initialized (Northwest) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// StatusControl (East)
|
||||
if (typeof StatusControl !== 'undefined') {
|
||||
this.statusControl = new StatusControl();
|
||||
this.statusControl.config.position = 'e';
|
||||
this.statusControl.show();
|
||||
window.statusControl = this.statusControl;
|
||||
console.log('✅ StatusControl initialized (East) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// DebugControl (Southeast)
|
||||
if (typeof DebugControl !== 'undefined') {
|
||||
this.debugControl = new DebugControl();
|
||||
this.debugControl.config.position = 'se';
|
||||
this.debugControl.show();
|
||||
window.debugControl = this.debugControl;
|
||||
console.log('✅ DebugControl initialized (Southeast) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// EditControl (Northeast)
|
||||
if (typeof EditControl !== 'undefined') {
|
||||
this.editControl = new EditControl();
|
||||
this.editControl.config.position = 'ne';
|
||||
this.editControl.show();
|
||||
window.editControl = this.editControl;
|
||||
console.log('✅ EditControl initialized (Northeast) with enhanced ControlBase');
|
||||
}
|
||||
},
|
||||
|
||||
// Setup event handlers
|
||||
setupEventHandlers: function() {
|
||||
console.log('🔌 Setting up event handlers...');
|
||||
|
||||
if (!this.documentControls) return;
|
||||
|
||||
this.documentControls.setEventHandlers({
|
||||
'save-document': () => {
|
||||
console.log('💾 Save document clicked');
|
||||
try {
|
||||
const currentMarkdown = this.sectionManager.getDocumentMarkdown();
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
|
||||
const filename = `${this.config.originalFilename}-edited-${timestamp}.md`;
|
||||
|
||||
const blob = new Blob([currentMarkdown], { 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);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`Document saved as: ${filename}`, 'SUCCESS');
|
||||
}
|
||||
console.log(`✅ Document saved as: ${filename}`);
|
||||
|
||||
} catch (error) {
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
|
||||
}
|
||||
console.error('❌ Save error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
'reset-all': () => {
|
||||
console.log('🔄 Reset all clicked');
|
||||
try {
|
||||
this.domRenderer.hideCurrentEditor();
|
||||
const allSections = Array.from(this.sectionManager.sections.values());
|
||||
allSections.forEach(section => section.resetToOriginal());
|
||||
this.domRenderer.renderAllSections(allSections);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage('Reset all sections to original state', 'INFO');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Reset all failed:', error);
|
||||
}
|
||||
},
|
||||
|
||||
'show-status': () => {
|
||||
const status = this.sectionManager.getDocumentStatus();
|
||||
alert(`Document Status:\nTotal Sections: ${status.totalSections}\nEditing Sections: ${status.editingSections}`);
|
||||
},
|
||||
|
||||
'toggle-debug': () => {
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.toggle();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Setup section manager event handlers
|
||||
if (this.sectionManager && this.debugPanel) {
|
||||
this.sectionManager.on('sections-created', (data) => {
|
||||
this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
|
||||
});
|
||||
|
||||
this.sectionManager.on('edit-started', (data) => {
|
||||
this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
this.sectionManager.on('changes-accepted', (data) => {
|
||||
this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
|
||||
this.updateSectionDOM(data.sectionId);
|
||||
});
|
||||
|
||||
this.sectionManager.on('changes-cancelled', (data) => {
|
||||
this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Render content using the configuration
|
||||
renderContent: function() {
|
||||
console.log('📄 Rendering markdown content...');
|
||||
|
||||
const markdownToRender = this.config.markdownContent || '';
|
||||
if (markdownToRender.trim()) {
|
||||
const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender);
|
||||
this.domRenderer.renderAllSections(sections);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
|
||||
}
|
||||
console.log(`✅ Rendered ${sections.length} sections`);
|
||||
} else {
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage('No markdown content to initialize', 'WARNING');
|
||||
}
|
||||
console.warn('⚠️ No markdown content to render');
|
||||
}
|
||||
},
|
||||
|
||||
// Update section DOM after changes
|
||||
updateSectionDOM: function(sectionId) {
|
||||
try {
|
||||
const section = this.sectionManager.sections.get(sectionId);
|
||||
if (section) {
|
||||
const sectionElement = this.domRenderer.findSectionElement(sectionId);
|
||||
if (sectionElement) {
|
||||
const newElement = this.domRenderer.renderSection(section);
|
||||
sectionElement.parentNode.replaceChild(newElement, sectionElement);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update section DOM:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Fallback mode if initialization fails
|
||||
fallbackMode: function() {
|
||||
console.warn('⚠️ Running in fallback mode');
|
||||
|
||||
// Basic content rendering fallback
|
||||
const contentDiv = document.getElementById('markdown-content');
|
||||
if (contentDiv && this.config && this.config.markdownContent) {
|
||||
const basicHtml = this.config.markdownContent
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
contentDiv.innerHTML = `<p>${basicHtml}</p>`;
|
||||
console.log('✅ Fallback content rendered');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Make components globally available for debugging
|
||||
window.MarkitectMain = MarkitectMain;
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Small delay to ensure config is loaded
|
||||
setTimeout(() => MarkitectMain.initialize(), 100);
|
||||
});
|
||||
} else {
|
||||
// DOM already ready
|
||||
setTimeout(() => MarkitectMain.initialize(), 100);
|
||||
}
|
||||
129
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/controls.css
Executable file
129
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/controls.css
Executable file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* TestDrive JSUI Control Panel Styles
|
||||
*
|
||||
* Styles for individual control panels
|
||||
*/
|
||||
|
||||
/* Contents Control (Northwest) */
|
||||
.contents-control {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.contents-control .toc-item {
|
||||
padding: 0.25rem 0;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.contents-control .toc-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.contents-control .toc-h1 { font-weight: bold; }
|
||||
.contents-control .toc-h2 { margin-left: 1rem; }
|
||||
.contents-control .toc-h3 { margin-left: 2rem; font-size: 0.9em; }
|
||||
|
||||
/* Status Control (East) */
|
||||
.status-control {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-metric {
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-metric .metric-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.status-metric .metric-label {
|
||||
font-size: 0.8em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Debug Control (Southeast) */
|
||||
.debug-control {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Removed debug-header styles - using base class title formatting */
|
||||
|
||||
.debug-control .debug-logs {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
padding: 0.5rem;
|
||||
margin: 0 -0.75rem -0.75rem -0.75rem;
|
||||
border-radius: 0 0 5px 5px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* Edit Control (Northeast) */
|
||||
.edit-control {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edit-control .control-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.edit-control .control-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.edit-control .control-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.edit-control .control-button.danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.edit-control .control-button.danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Control panel animations */
|
||||
.markitect-control-panel {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.markitect-control-panel.entering {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.markitect-control-panel.entered {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.markitect-control-panel {
|
||||
position: fixed !important;
|
||||
top: auto !important;
|
||||
bottom: 10px !important;
|
||||
left: 10px !important;
|
||||
right: 10px !important;
|
||||
transform: none !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
101
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/editor.css
Executable file
101
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/editor.css
Executable file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* TestDrive JSUI Editor Styles
|
||||
*
|
||||
* Base styles for the markdown editor interface
|
||||
*/
|
||||
|
||||
.markitect-edit-mode {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Section editing styles */
|
||||
.markitect-section {
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.markitect-section:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #e9ecef;
|
||||
}
|
||||
|
||||
.markitect-section.editing {
|
||||
background-color: #fff;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Editor styles */
|
||||
.markitect-editor {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.markitect-editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Control panel positioning */
|
||||
.markitect-control-panel {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Compass positioning */
|
||||
.markitect-control-nw { top: 20px; left: 20px; }
|
||||
.markitect-control-ne { top: 20px; right: 20px; }
|
||||
.markitect-control-e { top: 50%; right: 20px; transform: translateY(-50%); }
|
||||
.markitect-control-se { bottom: 20px; right: 20px; }
|
||||
.markitect-control-s { bottom: 20px; left: 50%; transform: translateX(-50%); }
|
||||
.markitect-control-sw { bottom: 20px; left: 20px; }
|
||||
.markitect-control-w { top: 50%; left: 20px; transform: translateY(-50%); }
|
||||
|
||||
/* Control panel states */
|
||||
.markitect-control-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markitect-control-expanded {
|
||||
max-width: 300px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
/* Debug styles */
|
||||
.markitect-debug-panel {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.markitect-debug-message {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.markitect-debug-error { color: #fed7d7; }
|
||||
.markitect-debug-warning { color: #faf089; }
|
||||
.markitect-debug-success { color: #9ae6b4; }
|
||||
.markitect-debug-info { color: #bee3f8; }
|
||||
138
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/themes/github.css
Executable file
138
examples/todohtml/_markitect/plugins/testdrive-jsui/static/css/themes/github.css
Executable file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* TestDrive JSUI GitHub Theme
|
||||
*
|
||||
* GitHub-inspired theme for the markdown editor
|
||||
*/
|
||||
|
||||
:root {
|
||||
--github-primary: #0969da;
|
||||
--github-border: #d0d7de;
|
||||
--github-bg-subtle: #f6f8fa;
|
||||
--github-fg-default: #1f2328;
|
||||
--github-fg-muted: #656d76;
|
||||
--github-success: #1a7f37;
|
||||
--github-danger: #d1242f;
|
||||
--github-warning: #9a6700;
|
||||
}
|
||||
|
||||
/* GitHub-style editor */
|
||||
.markitect-edit-mode {
|
||||
color: var(--github-fg-default);
|
||||
}
|
||||
|
||||
.markitect-section {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.markitect-section:hover {
|
||||
background-color: var(--github-bg-subtle);
|
||||
border-color: var(--github-border);
|
||||
}
|
||||
|
||||
.markitect-section.editing {
|
||||
border-color: var(--github-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(9, 105, 218, 0.25);
|
||||
}
|
||||
|
||||
/* GitHub-style control panels */
|
||||
.markitect-control-panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--github-border);
|
||||
color: var(--github-fg-default);
|
||||
}
|
||||
|
||||
/* GitHub-style buttons */
|
||||
.edit-control .control-button {
|
||||
background: var(--github-primary);
|
||||
border: 1px solid transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edit-control .control-button:hover {
|
||||
background: #0860ca;
|
||||
}
|
||||
|
||||
.edit-control .control-button.danger {
|
||||
background: var(--github-danger);
|
||||
}
|
||||
|
||||
.edit-control .control-button.danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
/* GitHub-style status metrics */
|
||||
.status-metric {
|
||||
background: var(--github-bg-subtle);
|
||||
border: 1px solid var(--github-border);
|
||||
}
|
||||
|
||||
.status-metric .metric-value {
|
||||
color: var(--github-primary);
|
||||
}
|
||||
|
||||
.status-metric .metric-label {
|
||||
color: var(--github-fg-muted);
|
||||
}
|
||||
|
||||
/* GitHub-style debug panel */
|
||||
.markitect-debug-panel {
|
||||
background: #24292f;
|
||||
color: #f0f6fc;
|
||||
border: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.markitect-debug-message {
|
||||
border-bottom: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.markitect-debug-error {
|
||||
color: #f85149;
|
||||
background-color: rgba(248, 81, 73, 0.1);
|
||||
}
|
||||
|
||||
.markitect-debug-warning {
|
||||
color: #f0c674;
|
||||
background-color: rgba(240, 198, 116, 0.1);
|
||||
}
|
||||
|
||||
.markitect-debug-success {
|
||||
color: #56d364;
|
||||
background-color: rgba(86, 211, 100, 0.1);
|
||||
}
|
||||
|
||||
.markitect-debug-info {
|
||||
color: #79c0ff;
|
||||
background-color: rgba(121, 192, 255, 0.1);
|
||||
}
|
||||
|
||||
/* GitHub-style table of contents */
|
||||
.contents-control .toc-item {
|
||||
color: var(--github-fg-default);
|
||||
}
|
||||
|
||||
.contents-control .toc-item:hover {
|
||||
background-color: var(--github-bg-subtle);
|
||||
color: var(--github-primary);
|
||||
}
|
||||
|
||||
/* GitHub-style scrollbars */
|
||||
.contents-control::-webkit-scrollbar,
|
||||
.debug-control .debug-logs::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.contents-control::-webkit-scrollbar-track,
|
||||
.debug-control .debug-logs::-webkit-scrollbar-track {
|
||||
background: var(--github-bg-subtle);
|
||||
}
|
||||
|
||||
.contents-control::-webkit-scrollbar-thumb,
|
||||
.debug-control .debug-logs::-webkit-scrollbar-thumb {
|
||||
background: var(--github-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.contents-control::-webkit-scrollbar-thumb:hover,
|
||||
.debug-control .debug-logs::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--github-fg-muted);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Placeholder for edit icon
|
||||
# In a real implementation, this would be a PNG image file
|
||||
# For testing purposes, this file exists to verify asset deployment
|
||||
EDIT_ICON_PLACEHOLDER=true
|
||||
@@ -0,0 +1,2 @@
|
||||
# Placeholder for reset icon
|
||||
RESET_ICON_PLACEHOLDER=true
|
||||
@@ -0,0 +1,2 @@
|
||||
# Placeholder for save icon
|
||||
SAVE_ICON_PLACEHOLDER=true
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Legacy wrapper for DocumentControls
|
||||
*
|
||||
* This file provides backward compatibility for tests that reference
|
||||
* DocumentControlsLegacy. It simply re-exports the current DocumentControls
|
||||
* implementation with the legacy name.
|
||||
*/
|
||||
|
||||
const { DocumentControls } = require('./document-controls.js');
|
||||
|
||||
// Re-export as legacy name for backward compatibility
|
||||
const DocumentControlsLegacy = DocumentControls;
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DocumentControlsLegacy };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DocumentControlsLegacy = DocumentControls;
|
||||
}
|
||||
645
js/testdrive-jsui.js
Normal file
645
js/testdrive-jsui.js
Normal file
@@ -0,0 +1,645 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -33,10 +33,10 @@ describe('Button Functionality and DOM Events', () => {
|
||||
|
||||
mockSection = document.querySelector('.section');
|
||||
|
||||
// Load components - using legacy component for backward compatibility
|
||||
require('../components/document-controls-legacy.js');
|
||||
if (global.DocumentControlsLegacy) {
|
||||
documentControls = new global.DocumentControlsLegacy(document.getElementById('content'));
|
||||
// Load components
|
||||
require('../components/document-controls.js');
|
||||
if (global.DocumentControls) {
|
||||
documentControls = new global.DocumentControls(document.getElementById('content'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,14 +37,14 @@ runner.describe('DocumentControls Component Extraction', () => {
|
||||
|
||||
runner.it('should load extracted DocumentControls component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/document-controls-legacy.js')];
|
||||
delete require.cache[require.resolve('../components/document-controls.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/document-controls-legacy.js');
|
||||
runner.expect(module.DocumentControlsLegacy).toBeTruthy();
|
||||
const module = require('../components/document-controls.js');
|
||||
runner.expect(module.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set global for other tests
|
||||
global.ExtractedDocumentControls = module.DocumentControlsLegacy;
|
||||
global.ExtractedDocumentControls = module.DocumentControls;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -19,18 +19,18 @@ runner.describe('Full Component Integration Tests', () => {
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls-legacy.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(debugModule.DebugPanel).toBeTruthy();
|
||||
runner.expect(controlsModule.DocumentControlsLegacy).toBeTruthy();
|
||||
runner.expect(controlsModule.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedDebugPanel = debugModule.DebugPanel;
|
||||
global.ExtractedDocumentControls = controlsModule.DocumentControlsLegacy;
|
||||
global.ExtractedDocumentControls = controlsModule.DocumentControls;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
|
||||
@@ -18,12 +18,12 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls-legacy.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControlsLegacy } = controlsModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
// Setup DOM container
|
||||
const container = document.createElement('div');
|
||||
@@ -34,7 +34,7 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControlsLegacy();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
@@ -96,11 +96,11 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
// Setup similar to above
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const controlsModule = require('../components/document-controls-legacy.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DocumentControlsLegacy } = controlsModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
@@ -108,7 +108,7 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const documentControls = new DocumentControlsLegacy();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
@@ -195,12 +195,12 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls-legacy.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControlsLegacy } = controlsModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
@@ -209,7 +209,7 @@ runner.describe('Real User Functionality Tests', () => {
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControlsLegacy();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
|
||||
385
js/utils/html-generator.js
Normal file
385
js/utils/html-generator.js
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* HTML Generator - Create standalone or external-resources HTML files
|
||||
*
|
||||
* Generates complete HTML documents that use TestDrive-JSUI with two modes:
|
||||
* 1. Standalone (default): All JS/CSS embedded inline
|
||||
* 2. External: References external files under _jsui/ directory
|
||||
*
|
||||
* Usage:
|
||||
* ```javascript
|
||||
* const generator = new HTMLGenerator({
|
||||
* title: 'My Document',
|
||||
* markdown: '# Hello World',
|
||||
* standalone: true // or false for external mode
|
||||
* });
|
||||
*
|
||||
* const html = generator.generate();
|
||||
* ```
|
||||
*/
|
||||
|
||||
class HTMLGenerator {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
title: options.title || 'TestDrive-JSUI Editor',
|
||||
description: options.description || 'Interactive markdown editor',
|
||||
markdown: options.markdown || '# Welcome\n\nStart editing...',
|
||||
mode: options.mode || 'edit', // 'edit' or 'view'
|
||||
theme: options.theme || 'github',
|
||||
standalone: options.standalone !== false, // default true
|
||||
controls: {
|
||||
editControl: options.controls?.editControl !== false,
|
||||
statusControl: options.controls?.statusControl !== false,
|
||||
contentsControl: options.controls?.contentsControl !== false,
|
||||
debugControl: options.controls?.debugControl || false
|
||||
},
|
||||
shortcuts: options.shortcuts !== false,
|
||||
autosave: options.autosave || false,
|
||||
baseUrl: options.baseUrl || '_jsui', // For external mode
|
||||
includeMarkedJS: options.includeMarkedJS !== false // Include marked.js CDN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML document
|
||||
*/
|
||||
generate() {
|
||||
if (this.options.standalone) {
|
||||
return this.generateStandalone();
|
||||
} else {
|
||||
return this.generateExternal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate standalone HTML with all resources embedded
|
||||
*/
|
||||
generateStandalone() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${this.escapeHtml(this.options.title)}</title>
|
||||
|
||||
${this.generateInlineStyles()}
|
||||
</head>
|
||||
<body>
|
||||
${this.generateHeader()}
|
||||
${this.generateContainer()}
|
||||
|
||||
${this.generateMarkedJS()}
|
||||
${this.generateInlineScripts()}
|
||||
${this.generateInitScript()}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML with external resource references
|
||||
*/
|
||||
generateExternal() {
|
||||
const baseUrl = this.options.baseUrl;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${this.escapeHtml(this.options.title)}</title>
|
||||
|
||||
<!-- TestDrive-JSUI Styles -->
|
||||
<link rel="stylesheet" href="${baseUrl}/css/base.css">
|
||||
<link rel="stylesheet" href="${baseUrl}/css/editor.css">
|
||||
<link rel="stylesheet" href="${baseUrl}/css/controls.css">
|
||||
<link rel="stylesheet" href="${baseUrl}/css/themes/${this.options.theme}.css">
|
||||
</head>
|
||||
<body>
|
||||
${this.generateHeader()}
|
||||
${this.generateContainer()}
|
||||
|
||||
${this.generateMarkedJS()}
|
||||
|
||||
<!-- TestDrive-JSUI Scripts -->
|
||||
<script src="${baseUrl}/js/core/event-emitter.js"></script>
|
||||
<script src="${baseUrl}/js/core/section.js"></script>
|
||||
<script src="${baseUrl}/js/core/section-manager.js"></script>
|
||||
<script src="${baseUrl}/js/components/dom-renderer.js"></script>
|
||||
<script src="${baseUrl}/js/components/floating-menu.js"></script>
|
||||
<script src="${baseUrl}/js/controls/control-base.js"></script>
|
||||
<script src="${baseUrl}/js/controls/edit-control.js"></script>
|
||||
<script src="${baseUrl}/js/controls/status-control.js"></script>
|
||||
<script src="${baseUrl}/js/controls/contents-control.js"></script>
|
||||
<script src="${baseUrl}/js/controls/debug-control.js"></script>
|
||||
<script src="${baseUrl}/js/testdrive-jsui.js"></script>
|
||||
|
||||
${this.generateInitScript()}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate header section
|
||||
*/
|
||||
generateHeader() {
|
||||
if (!this.options.description) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` <div class="tdjs-header">
|
||||
<h1>${this.escapeHtml(this.options.title)}</h1>
|
||||
<p>${this.escapeHtml(this.options.description)}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate container div
|
||||
*/
|
||||
generateContainer() {
|
||||
return ` <div id="editor-container"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate marked.js CDN script
|
||||
*/
|
||||
generateMarkedJS() {
|
||||
if (!this.options.includeMarkedJS) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` <!-- Markdown Parser -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate inline styles for standalone mode
|
||||
*/
|
||||
generateInlineStyles() {
|
||||
return ` <style>
|
||||
/* Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #24292e;
|
||||
background: #f6f8fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tdjs-header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.tdjs-header h1 {
|
||||
color: #0366d6;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.tdjs-header p {
|
||||
color: #586069;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* Section Editing Styles */
|
||||
.markitect-section {
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.markitect-section:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #e9ecef;
|
||||
}
|
||||
|
||||
.markitect-section.editing {
|
||||
background-color: #fff;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Control Panels */
|
||||
.markitect-control-panel {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Compass positioning */
|
||||
.markitect-control-nw { top: 20px; left: 20px; }
|
||||
.markitect-control-ne { top: 20px; right: 20px; }
|
||||
.markitect-control-e { top: 50%; right: 20px; transform: translateY(-50%); }
|
||||
.markitect-control-se { bottom: 20px; right: 20px; }
|
||||
.markitect-control-s { bottom: 20px; left: 50%; transform: translateX(-50%); }
|
||||
.markitect-control-sw { bottom: 20px; left: 20px; }
|
||||
.markitect-control-w { top: 50%; left: 20px; transform: translateY(-50%); }
|
||||
|
||||
.markitect-control-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.markitect-control-expanded {
|
||||
max-width: 300px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate inline JavaScript for standalone mode
|
||||
*
|
||||
* Note: In a real implementation, this would read and embed actual JS files.
|
||||
* For now, it provides a placeholder comment.
|
||||
*/
|
||||
generateInlineScripts() {
|
||||
return ` <script>
|
||||
// TestDrive-JSUI JavaScript
|
||||
// In standalone mode, all JavaScript would be embedded here.
|
||||
// For brevity, this is a placeholder. The actual implementation
|
||||
// would include the contents of:
|
||||
// - event-emitter.js
|
||||
// - section.js
|
||||
// - section-manager.js
|
||||
// - dom-renderer.js
|
||||
// - floating-menu.js
|
||||
// - control-base.js
|
||||
// - edit-control.js
|
||||
// - status-control.js
|
||||
// - contents-control.js
|
||||
// - debug-control.js
|
||||
// - testdrive-jsui.js
|
||||
|
||||
// For now, load from external file:
|
||||
// NOTE: This is a temporary solution. Full standalone mode
|
||||
// requires embedding all JS content.
|
||||
</script>
|
||||
<script src="../js/testdrive-jsui.js"></script>
|
||||
<script src="../js/core/event-emitter.js"></script>
|
||||
<script src="../js/core/section.js"></script>
|
||||
<script src="../js/core/section-manager.js"></script>
|
||||
<script src="../js/components/dom-renderer.js"></script>
|
||||
<script src="../js/components/floating-menu.js"></script>
|
||||
<script src="../js/controls/control-base.js"></script>
|
||||
<script src="../js/controls/edit-control.js"></script>
|
||||
<script src="../js/controls/status-control.js"></script>
|
||||
<script src="../js/controls/contents-control.js"></script>
|
||||
<script src="../js/controls/debug-control.js"></script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate initialization script
|
||||
*/
|
||||
generateInitScript() {
|
||||
const config = {
|
||||
container: '#editor-container',
|
||||
markdown: this.options.markdown,
|
||||
mode: this.options.mode,
|
||||
theme: this.options.theme,
|
||||
controls: this.options.controls,
|
||||
shortcuts: this.options.shortcuts,
|
||||
autosave: this.options.autosave
|
||||
};
|
||||
|
||||
return ` <script>
|
||||
// Initialize TestDrive-JSUI
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const editor = new TestDriveJSUI(${JSON.stringify(config, null, 12)});
|
||||
|
||||
// Make editor globally accessible
|
||||
window.editor = editor;
|
||||
|
||||
console.log('TestDrive-JSUI initialized:', editor);
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML special characters
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save generated HTML to file (Node.js only)
|
||||
*/
|
||||
saveToFile(filename) {
|
||||
if (typeof require === 'undefined') {
|
||||
throw new Error('saveToFile() requires Node.js environment');
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const html = this.generate();
|
||||
fs.writeFileSync(filename, html, 'utf8');
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download generated HTML (browser only)
|
||||
*/
|
||||
download(filename = 'testdrive-jsui-editor.html') {
|
||||
const html = this.generate();
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems or attach to global
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HTMLGenerator;
|
||||
} else {
|
||||
window.HTMLGenerator = HTMLGenerator;
|
||||
}
|
||||
@@ -52,8 +52,7 @@ class ComponentAnalyzer:
|
||||
'CssProcessor': 'CSSProcessor',
|
||||
'JsEngine': 'JSEngine',
|
||||
'ApiClient': 'APIClient',
|
||||
'UrlHandler': 'URLHandler',
|
||||
'DocumentControlsLegacy': 'DocumentControlsLegacy'
|
||||
'UrlHandler': 'URLHandler'
|
||||
}
|
||||
|
||||
component_name = acronym_mappings.get(component_name, component_name)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestComponentListing:
|
||||
'EditControl': 'js/controls/edit-control.js',
|
||||
'DebugControl': 'js/controls/debug-control.js',
|
||||
'DebugPanel': 'js/components/debug-panel.js',
|
||||
'DocumentControlsLegacy': 'js/components/document-controls-legacy.js',
|
||||
'DocumentControls': 'js/components/document-controls.js',
|
||||
'SectionManager': 'js/core/section-manager.js',
|
||||
'DOMRenderer': 'js/components/dom-renderer.js'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user