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>
|