testdrive-jsui/examples/full-editor-standalone.html

1201 lines
41 KiB
HTML
Raw Normal View History

<!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 (Standalone)</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;
}
/* Section styles */
.ui-edit-section {
margin: 16px 0;
padding: 12px;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.ui-edit-section:hover {
background-color: rgba(0, 122, 204, 0.05);
border-color: rgba(0, 122, 204, 0.2);
}
/* 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;
font-family: 'Courier New', monospace;
}
#editor-container pre {
background-color: #f6f8fa;
border-radius: 6px;
font-size: 85%;
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;
}
.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;
}
/* Control panel styles */
#markitect-global-controls {
position: fixed;
top: 20px;
right: 20px;
background: rgba(248, 249, 250, 0.95);
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
backdrop-filter: blur(8px);
min-width: 200px;
}
</style>
</head>
<body>
<div class="header">
<h1>📝 TestDrive-JSUI Full Editor (Standalone)</h1>
<p>
Complete interactive markdown editor - all JavaScript embedded in this single file.
Click any section to edit, use the toolbar for document operations.
</p>
</div>
<div class="info-box">
<strong>✨ Single File:</strong> This version includes all JavaScript code embedded,
so you can copy this one file anywhere and it will work!
</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>
</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 • Standalone Single File</p>
<p><small>Uses: marked.js (CDN) + embedded TestDriveJSUI library</small></p>
</div>
<!-- External Dependencies -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Embedded TestDrive-JSUI Library -->
<script>
// This file embeds all the necessary JavaScript components inline
// so it can be used as a single standalone file
// ============= Section Manager =============
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'
});
function debug(message, category = 'INFO') {
console.log(`DEBUG ${category}: ${message}`);
}
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) {
const content = (markdown || '').trim().replace(/\s+/g, ' ').toLowerCase();
let hash = 0;
for (let i = 0; i < content.length; i++) {
hash = ((hash << 5) - hash) + content.charCodeAt(i);
hash = hash & hash;
}
const hexHash = Math.abs(hash).toString(16).padStart(8, '0').substring(0, 8);
const type = this.detectType(markdown);
return `section-${type.substring(0, 3)}-${hexHash}-${position.toString(16).padStart(2, '0')}`;
}
static detectType(markdown) {
if (!markdown) return SectionType.PARAGRAPH;
const trimmed = markdown.trim();
if (/^#{1,6}\s+.+/.test(trimmed)) return SectionType.HEADING;
if (/!\[.*?\]\([^)]+\)/.test(trimmed)) return SectionType.IMAGE;
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) return SectionType.CODE;
return SectionType.PARAGRAPH;
}
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;
this.state = this.lastSaved ? EditState.SAVED : (this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL);
return this.currentMarkdown;
}
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(),
type: this.type
};
}
}
class SectionManager {
constructor() {
this.sections = new Map();
this.listeners = new Map();
}
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) {
const blocks = markdownContent.split(/\n\s*\n/);
const sections = [];
let position = 0;
for (const block of blocks) {
const trimmedBlock = block.trim();
if (!trimmedBlock) continue;
const sectionId = Section.generateId(trimmedBlock, position);
const sectionType = Section.detectType(trimmedBlock);
const section = new Section(sectionId, trimmedBlock, sectionType);
sections.push(section);
this.sections.set(sectionId, section);
position++;
}
this.emit('sections-created', { sections, count: sections.length });
return sections;
}
startEditing(sectionId) {
const section = this.sections.get(sectionId);
if (!section) throw new Error(`Section ${sectionId} not found`);
if (section.isEditing()) {
return section.editingMarkdown;
}
const content = section.startEdit();
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
return content;
}
updateContent(sectionId, markdown) {
const section = this.sections.get(sectionId);
if (!section) throw new Error(`Section ${sectionId} not found`);
section.updateContent(markdown);
this.emit('content-updated', { sectionId, markdown, 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());
}
}
// ============= DOM Renderer =============
class FloatingMenu {
constructor(sectionId, type, renderer) {
this.sectionId = sectionId;
this.type = type;
this.renderer = renderer;
this.element = null;
this.isVisible = false;
}
show(contentElement) {
if (this.isVisible) this.hide();
const targetElement = this.renderer.findSectionElement(this.sectionId);
if (!targetElement) return null;
const rect = targetElement.getBoundingClientRect();
const viewport = { width: window.innerWidth, height: window.innerHeight };
this.element = document.createElement('div');
this.element.className = 'ui-edit-floating-menu';
this.element.style.cssText = `
position: fixed;
z-index: 10000;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 16px;
max-width: 600px;
min-width: 400px;
`;
const closeButton = document.createElement('button');
closeButton.textContent = '✕';
closeButton.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #666;
`;
closeButton.addEventListener('click', () => this.hide());
this.element.appendChild(closeButton);
this.element.appendChild(contentElement);
let left = rect.left;
let top = rect.top;
if (left + 400 > viewport.width) left = viewport.width - 420;
if (left < 10) left = 10;
if (top + 300 > viewport.height) top = viewport.height - 320;
if (top < 10) top = 10;
this.element.style.left = `${left}px`;
this.element.style.top = `${top}px`;
document.body.appendChild(this.element);
this.isVisible = true;
return this.element;
}
hide() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
this.isVisible = false;
const section = this.renderer.sectionManager.sections.get(this.sectionId);
if (section && section.isEditing()) {
section.state = EditState.ORIGINAL;
}
this.renderer.editingSections.delete(this.sectionId);
}
}
class DOMRenderer {
constructor(sectionManager, container) {
this.sectionManager = sectionManager;
this.container = container;
this.editingSections = new Set();
this.currentFloatingMenu = null;
this.eventListenersAttached = false;
this.handleSectionClick = this.handleSectionClick.bind(this);
this.setupEventListeners();
}
setupEventListeners() {
this.sectionManager.on('sections-created', (data) => {
this.renderAllSections(data.sections);
});
this.sectionManager.on('edit-started', (data) => {
this.showEditor(data.sectionId, data.content);
});
}
renderAllSections(sections) {
this.container.innerHTML = '';
sections.forEach((section) => {
const element = this.renderSection(section);
if (element) {
this.container.appendChild(element);
}
});
if (!this.eventListenersAttached) {
this.container.addEventListener('click', this.handleSectionClick);
this.eventListenersAttached = true;
}
}
renderSection(section) {
const element = document.createElement('div');
element.className = 'ui-edit-section';
element.setAttribute('data-section-id', section.id);
const html = marked.parse(section.currentMarkdown);
element.innerHTML = html;
return element;
}
findSectionElement(sectionId) {
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
}
handleSectionClick(event) {
if (event.target.closest('textarea, button, input, a')) return;
const sectionElement = event.target.closest('.ui-edit-section');
if (!sectionElement) return;
const sectionId = sectionElement.getAttribute('data-section-id');
if (!sectionId) return;
const section = this.sectionManager.sections.get(sectionId);
if (section && section.isEditing()) {
const existingDialog = document.querySelector('.ui-edit-floating-menu');
if (existingDialog) return;
}
this.sectionManager.startEditing(sectionId);
}
showEditor(sectionId, content) {
const element = this.findSectionElement(sectionId);
if (!element) return;
this.hideCurrentEditor();
const editorContent = document.createElement('div');
editorContent.style.cssText = 'display: flex; flex-direction: column; gap: 12px; margin-top: 20px;';
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.cssText = `
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 14px;
resize: vertical;
`;
const controls = document.createElement('div');
controls.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end;';
const acceptButton = document.createElement('button');
acceptButton.textContent = '✓ Accept';
acceptButton.style.cssText = `
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
`;
const cancelButton = document.createElement('button');
cancelButton.textContent = '✗ Cancel';
cancelButton.style.cssText = `
background: #dc3545;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
`;
const resetButton = document.createElement('button');
resetButton.textContent = '↺ Reset';
resetButton.style.cssText = `
background: #fd7e14;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
`;
controls.appendChild(acceptButton);
controls.appendChild(cancelButton);
controls.appendChild(resetButton);
editorContent.appendChild(textarea);
editorContent.appendChild(controls);
const floatingMenu = new FloatingMenu(sectionId, 'text', this);
this.currentFloatingMenu = floatingMenu;
this.editingSections.add(sectionId);
floatingMenu.show(editorContent);
acceptButton.addEventListener('click', () => {
this.sectionManager.updateContent(sectionId, textarea.value);
this.sectionManager.acceptChanges(sectionId);
floatingMenu.hide();
this.currentFloatingMenu = null;
// Re-render the section
const section = this.sectionManager.sections.get(sectionId);
const element = this.findSectionElement(sectionId);
if (element && section) {
element.innerHTML = marked.parse(section.currentMarkdown);
}
});
cancelButton.addEventListener('click', () => {
this.sectionManager.cancelChanges(sectionId);
floatingMenu.hide();
this.currentFloatingMenu = null;
});
resetButton.addEventListener('click', () => {
const section = this.sectionManager.sections.get(sectionId);
if (section) {
textarea.value = section.originalMarkdown;
this.sectionManager.updateContent(sectionId, section.originalMarkdown);
this.sectionManager.acceptChanges(sectionId);
floatingMenu.hide();
this.currentFloatingMenu = null;
// Re-render
const element = this.findSectionElement(sectionId);
if (element) {
element.innerHTML = marked.parse(section.currentMarkdown);
}
}
});
setTimeout(() => textarea.focus(), 100);
}
hideCurrentEditor() {
if (this.currentFloatingMenu) {
this.currentFloatingMenu.hide();
this.currentFloatingMenu = null;
}
}
}
// ============= Document Controls =============
class DocumentControls {
constructor() {
this.controlPanel = null;
this.buttons = new Map();
this.eventHandlers = new Map();
this.isVisible = true;
}
create() {
if (this.controlPanel) {
this.destroy();
}
const existingPanel = document.getElementById('markitect-global-controls');
if (existingPanel && existingPanel.parentNode) {
existingPanel.parentNode.removeChild(existingPanel);
}
this.controlPanel = document.createElement('div');
this.controlPanel.id = 'markitect-global-controls';
const title = document.createElement('div');
title.style.cssText = `
font-weight: 600;
margin-bottom: 8px;
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 4px;
`;
title.textContent = 'Document Controls';
const buttonContainer = document.createElement('div');
buttonContainer.id = 'button-container';
buttonContainer.style.cssText = 'display: flex; flex-direction: column; gap: 6px;';
this.controlPanel.appendChild(title);
this.controlPanel.appendChild(buttonContainer);
this.addDefaultButtons();
document.body.appendChild(this.controlPanel);
}
addDefaultButtons() {
this.addButton('save-document', '💾 Save', '#28a745');
this.addButton('reset-all', '🔄 Reset', '#ffc107', '#212529');
this.addButton('show-status', '📊 Status', '#17a2b8');
}
addButton(id, text, backgroundColor, textColor = 'white') {
const buttonContainer = this.controlPanel.querySelector('#button-container');
if (!buttonContainer) return null;
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.style.cssText = `
background: ${backgroundColor};
color: ${textColor};
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
`;
buttonContainer.appendChild(button);
this.buttons.set(id, button);
return button;
}
setEventHandlers(handlers) {
for (const [buttonId, handler] of Object.entries(handlers)) {
const button = this.buttons.get(buttonId);
if (button) {
if (this.eventHandlers.has(buttonId)) {
button.removeEventListener('click', this.eventHandlers.get(buttonId));
}
button.addEventListener('click', handler);
this.eventHandlers.set(buttonId, handler);
}
}
}
destroy() {
if (this.controlPanel && this.controlPanel.parentNode) {
this.controlPanel.parentNode.removeChild(this.controlPanel);
}
this.controlPanel = null;
this.buttons.clear();
this.eventHandlers.clear();
}
}
// ============= Main TestDriveJSUI Class =============
class TestDriveJSUI {
constructor(options = {}) {
if (!options.container) {
throw new Error('TestDriveJSUI: container option is required');
}
this.config = {
container: options.container,
markdown: options.markdown || '# Welcome\n\nStart editing...',
mode: options.mode || 'edit',
...options
};
this.container = null;
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = null;
this.isInitialized = false;
this.listeners = new Map();
this.init();
}
init() {
if (this.isInitialized) return;
this.container = typeof this.config.container === 'string'
? document.querySelector(this.config.container)
: this.config.container;
if (!this.container) {
throw new Error('TestDriveJSUI: Could not resolve container');
}
if (this.config.mode === 'edit') {
this.initEditMode();
} else {
this.initViewMode();
}
this.isInitialized = true;
this.emit('initialized', { mode: this.config.mode });
}
initEditMode() {
this.sectionManager = new SectionManager();
this.domRenderer = new DOMRenderer(this.sectionManager, this.container);
this.documentControls = new DocumentControls();
this.documentControls.create();
this.setupControlHandlers();
this.sectionManager.createSectionsFromMarkdown(this.config.markdown);
}
initViewMode() {
this.container.innerHTML = '';
const html = marked.parse(this.config.markdown);
const contentWrapper = document.createElement('div');
contentWrapper.innerHTML = html;
this.container.appendChild(contentWrapper);
}
setupControlHandlers() {
this.documentControls.setEventHandlers({
'save-document': () => {
const markdown = this.getMarkdown();
this.saveToLocalStorage(markdown);
alert('✅ Document saved to localStorage');
this.emit('save', { markdown });
},
'reset-all': () => {
if (confirm('Reset all sections to original content?')) {
this.resetAll();
}
},
'show-status': () => {
const status = this.getStatus();
this.showStatus(status);
}
});
}
getMarkdown() {
if (this.sectionManager) {
return this.sectionManager.getDocumentMarkdown();
}
return this.config.markdown;
}
setMarkdown(markdown) {
this.config.markdown = markdown;
if (this.sectionManager) {
this.sectionManager.sections.clear();
this.sectionManager.createSectionsFromMarkdown(markdown);
}
this.emit('content-changed', { markdown });
}
getStatus() {
if (this.sectionManager) {
const sections = this.sectionManager.getAllSections();
const text = this.getMarkdown();
return {
mode: this.config.mode,
totalSections: sections.length,
editingSections: sections.filter(s => s.isEditing()).length,
modifiedSections: sections.filter(s => s.hasChanges()).length,
wordCount: text.split(/\s+/).filter(w => w.length > 0).length,
characterCount: text.length
};
}
return { mode: this.config.mode };
}
showStatus(status) {
const message = [
`Mode: ${status.mode}`,
`Total Sections: ${status.totalSections || 0}`,
`Editing: ${status.editingSections || 0}`,
`Modified: ${status.modifiedSections || 0}`,
`Words: ${status.wordCount || 0}`,
`Characters: ${status.characterCount || 0}`
].join('\n');
alert(message);
}
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);
}
}
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(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 });
}
resetAll() {
if (this.sectionManager) {
const sections = this.sectionManager.getAllSections();
sections.forEach(section => {
this.sectionManager.resetSection(section.id);
// Re-render the section
const element = this.domRenderer.findSectionElement(section.id);
if (element) {
element.innerHTML = marked.parse(section.currentMarkdown);
}
});
}
this.emit('reset');
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this;
}
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() {
if (this.documentControls) {
this.documentControls.destroy();
}
if (this.container) {
this.container.innerHTML = '';
}
this.sectionManager = null;
this.domRenderer = null;
this.documentControls = null;
this.listeners.clear();
this.isInitialized = false;
this.emit('destroyed');
}
}
// Make available globally
window.TestDriveJSUI = TestDriveJSUI;
</script>
<!-- Application Script -->
<script>
// Sample document content
const defaultMarkdown = `# TestDrive-JSUI Standalone Demo
## Click Any Section to Edit
This is a **fully functional** markdown editor. Click on any section (like this one!) to start editing it.
### Features
- ✅ Section-based editing
- ✅ Save to localStorage
- ✅ Download as markdown
- ✅ Reset functionality
- ✅ All in one HTML file
## How to Use
1. **Edit**: Click any section to open the editor
2. **Save**: Click Accept (✓) to save changes
3. **Cancel**: Click Cancel (✗) to discard
4. **Reset**: Click Reset (↺) to restore original
## Try It!
Go ahead and click this section to edit it. You'll see a floating editor appear with the markdown source.
### Code Example
\`\`\`javascript
const editor = new TestDriveJSUI({
container: '#editor',
markdown: '# Hello World'
});
\`\`\`
## The Controls
Look for the floating panel in the top-right corner with:
- 💾 Save - Saves to browser localStorage
- 🔄 Reset - Restores all sections to original
- 📊 Status - Shows document statistics
---
**Have fun editing!**
`;
let editor;
window.addEventListener('DOMContentLoaded', function() {
editor = new TestDriveJSUI({
container: '#editor-container',
markdown: defaultMarkdown,
mode: 'edit'
});
editor.on('initialized', () => {
console.log('✅ Editor initialized');
updateStatusBar();
});
editor.on('save', () => {
updateStatusBar();
});
updateStatusBar();
setInterval(updateStatusBar, 2000);
});
function saveDocument() {
editor.saveToLocalStorage(editor.getMarkdown());
alert('✅ Saved to localStorage');
}
function loadDocument() {
const loaded = editor.loadFromLocalStorage();
if (loaded) {
alert('📂 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();
editor.showStatus(status);
}
function resetDocument() {
if (confirm('Reset all sections?')) {
editor.resetAll();
alert('🔄 Document reset!');
}
}
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;
}
window.editor = editor;
</script>
</body>
</html>