generated from coulomb/repo-seed
Created full-editor-standalone.html that embeds all JavaScript inline. This file can be copied anywhere and opened directly without needing any other files or dependencies (except marked.js from CDN). Perfect for distribution and testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1201 lines
41 KiB
HTML
1201 lines
41 KiB
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 - 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>
|