testdrive-jsui/js/controls/edit-control.js
tegwick 652cb4e255
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
style: apply eslint --fix (semi, indent 4); no behavior change, Jest 68/68
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 299063@bnt-lap001
Assistant-Session: 53eb1e1c-c13a-4277-9ce2-3702960d93f7
2026-10-02 22:59:33 +02:00

572 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* EditControl - Document Editing Tools and Actions Control
*
* Provides a comprehensive set of document editing tools including text formatting,
* document actions (print, save, export), navigation helpers, and editing modes.
* Designed to enhance the writing and editing experience within the TestDrive-JSUI
* environment.
*
* Features:
* - Document actions (print, save, export to various formats)
* - Text formatting tools (bold, italic, headers)
* - Navigation helpers (scroll to top/bottom, go to line)
* - Word processing features (find/replace, word count)
* - Accessibility tools (font size, contrast adjustment)
* - Markdown formatting shortcuts
*
* Dependencies:
* - ControlBase (base control functionality)
*/
/**
* EditControl - Comprehensive document editing control
*
* This control provides writers and editors with essential tools for document
* creation and modification. It includes both basic text operations and
* advanced features for content management and formatting.
*/
class EditControl extends ControlBase {
constructor() {
super();
// Configure for editing functionality
this.config = {
icon: '✏️',
title: 'Edit',
className: 'edit-control',
defaultContent: 'Document editing tools loading...',
ariaLabel: 'Document Edit Control',
position: 'e' // East positioning
};
// Edit control state
this.editingMode = 'view'; // 'view', 'edit', 'preview'
this.fontSize = 16;
this.lastSaveTime = null;
this.unsavedChanges = false;
this.shortcuts = new Map();
this.initializeShortcuts();
}
/**
* Initialize keyboard shortcuts for editing
*/
initializeShortcuts() {
this.shortcuts.set('Ctrl+S', () => this.saveDocument());
this.shortcuts.set('Ctrl+P', () => this.printDocument());
this.shortcuts.set('Ctrl+F', () => this.showFindDialog());
this.shortcuts.set('Ctrl+B', () => this.toggleBold());
this.shortcuts.set('Ctrl+I', () => this.toggleItalic());
this.shortcuts.set('Escape', () => this.exitEditMode());
}
/**
* Generate the main editing tools HTML
*/
generateEditToolsHTML() {
return this.safeOperation(() => {
return `
<!-- Document Actions -->
<div class="action-section" style="margin-bottom: 1rem;">
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Document Actions</div>
<button onclick="this.closest('.edit-control').editControl.printDocument()"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
🖨️ Print Document
</button>
<button onclick="this.closest('.edit-control').editControl.saveDocument()"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
💾 Save Changes
</button>
<button onclick="this.closest('.edit-control').editControl.exportDocument()"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
📄 Export Document
</button>
<button onclick="this.closest('.edit-control').editControl.resetAll()"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
🔄 Reset All
</button>
</div>
<!-- Navigation Tools -->
<div class="navigation-section" style="margin-bottom: 1rem;">
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Navigation</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
<button onclick="this.closest('.edit-control').editControl.scrollToTop()"
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
⬆️ Top
</button>
<button onclick="this.closest('.edit-control').editControl.scrollToBottom()"
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
⬇️ Bottom
</button>
</div>
<button onclick="this.closest('.edit-control').editControl.showGoToLine()"
style="width: 100%; padding: 0.4rem; margin-top: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🎯 Go to Line
</button>
</div>
<!-- Text Tools -->
<div class="text-section" style="margin-bottom: 1rem;">
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Text Tools</div>
<button onclick="this.closest('.edit-control').editControl.showFindReplace()"
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🔍 Find & Replace
</button>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 0.3rem;">
<button onclick="this.closest('.edit-control').editControl.increaseFontSize()"
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🔍+ Font
</button>
<button onclick="this.closest('.edit-control').editControl.decreaseFontSize()"
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🔍- Font
</button>
</div>
<button onclick="this.closest('.edit-control').editControl.copyLink()"
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #fd7e14; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
📋 Copy Page Link
</button>
</div>
<!-- Markdown Tools -->
<div class="markdown-section" style="margin-bottom: 1rem;">
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Markdown Tools</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('**', '**', 'Bold text')"
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
**B**
</button>
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('*', '*', 'Italic text')"
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
*I*
</button>
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('## ', '', 'Heading')"
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
H2
</button>
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('- ', '', 'List item')"
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
•List
</button>
</div>
</div>
<!-- Status Info -->
<div class="status-section" style="border-top: 1px solid #eee; padding-top: 0.5rem;">
<div style="font-size: 0.7rem; color: #666;">
<div>Mode: <span style="color: #007bff;">${this.editingMode}</span></div>
<div>Font: <span style="color: #007bff;">${this.fontSize}px</span></div>
${this.lastSaveTime ? `<div>Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}</div>` : ''}
${this.unsavedChanges ? '<div style="color: #dc3545;">⚠️ Unsaved changes</div>' : ''}
</div>
</div>
`;
}, '<p>Error generating edit tools</p>', 'generateEditToolsHTML');
}
/**
* Print the document
*/
printDocument() {
return this.safeOperation(() => {
window.print();
// Show feedback
this.showActionFeedback('🖨️ Print dialog opened', '#28a745');
}, null, 'printDocument');
}
/**
* Save document (placeholder - would integrate with actual save system)
*/
saveDocument() {
return this.safeOperation(() => {
// In a real implementation, this would save to a backend
this.lastSaveTime = Date.now();
this.unsavedChanges = false;
// Update display
this.buildContent();
// Show feedback
this.showActionFeedback('💾 Document saved', '#007bff');
}, null, 'saveDocument');
}
/**
* Export document to various formats
*/
exportDocument() {
return this.safeOperation(() => {
const contentArea = document.querySelector('#markitect-content') || document.body;
const htmlContent = contentArea.innerHTML;
const textContent = contentArea.textContent;
// Create export menu
const exportMenu = document.createElement('div');
exportMenu.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 2px solid #007bff;
border-radius: 8px;
padding: 1rem;
z-index: 10000;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
`;
exportMenu.innerHTML = `
<div style="margin-top: 0; font-weight: 600; font-size: 1.1em; color: #333; margin-bottom: 1rem;">Export Document</div>
<button onclick="this.parentElement.exportAsHTML()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
Export as HTML
</button>
<button onclick="this.parentElement.exportAsText()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
Export as Text
</button>
<button onclick="this.parentElement.exportAsMarkdown()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: #6f42c1; color: white; border: none; border-radius: 3px; cursor: pointer;">
Export as Markdown
</button>
<button onclick="document.body.removeChild(this.parentElement)" style="width: 100%; padding: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">
Cancel
</button>
`;
// Add export functions
exportMenu.exportAsHTML = () => {
this.downloadFile(htmlContent, 'document.html', 'text/html');
document.body.removeChild(exportMenu);
};
exportMenu.exportAsText = () => {
this.downloadFile(textContent, 'document.txt', 'text/plain');
document.body.removeChild(exportMenu);
};
exportMenu.exportAsMarkdown = () => {
// Simple HTML to Markdown conversion (basic)
const markdown = htmlContent
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
.replace(/<[^>]*>/g, ''); // Remove remaining HTML tags
this.downloadFile(markdown, 'document.md', 'text/markdown');
document.body.removeChild(exportMenu);
};
document.body.appendChild(exportMenu);
}, null, 'exportDocument');
}
/**
* Download a file with given content
*/
downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Reset all changes and restore document to original state
*/
resetAll() {
return this.safeOperation(() => {
// Show confirmation dialog
const confirmed = window.confirm(
'Reset all changes?\n\nThis will:\n' +
'• Restore document to original state\n' +
'• Clear all unsaved changes\n' +
'• Reset font size and other settings\n\n' +
'This action cannot be undone.'
);
if (!confirmed) {
this.showActionFeedback('🚫 Reset cancelled', '#6c757d');
return;
}
// Reset edit control state
this.fontSize = 16;
this.editingMode = 'view';
this.unsavedChanges = false;
this.lastSaveTime = null;
// Reset font size
this.applyFontSize();
// Clear any highlights
document.querySelectorAll('.edit-highlight').forEach(el => {
el.outerHTML = el.innerHTML;
});
// Try to reset sections if SectionManager is available
if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') {
window.sectionManager.resetAllSections();
}
// Try to reset document controls if available
if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') {
window.documentControls.resetAllChanges();
}
// Clear any debug messages if debug control is available
if (window.debugControl && typeof window.debugControl.clearMessages === 'function') {
window.debugControl.clearMessages();
}
// Reload the page as ultimate fallback
if (window.confirm('Reload page to complete reset?')) {
window.location.reload();
return;
}
// Update the control display
this.buildContent();
// Show feedback
this.showActionFeedback('🔄 All changes reset', '#ffc107', '#212529');
}, null, 'resetAll');
}
/**
* Scroll to top of document
*/
scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showActionFeedback('⬆️ Scrolled to top', '#6c757d');
}
/**
* Scroll to bottom of document
*/
scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
this.showActionFeedback('⬇️ Scrolled to bottom', '#6c757d');
}
/**
* Show go to line dialog
*/
showGoToLine() {
const lineNumber = prompt('Go to line number:');
if (lineNumber && !isNaN(lineNumber)) {
// Simple implementation - scroll to approximate position
const totalHeight = document.body.scrollHeight;
const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight;
window.scrollTo({ top: approximatePosition, behavior: 'smooth' });
this.showActionFeedback(`🎯 Went to line ${lineNumber}`, '#6c757d');
}
}
/**
* Show find and replace dialog
*/
showFindReplace() {
const searchTerm = prompt('Find text:');
if (searchTerm) {
// Simple highlight implementation
this.highlightText(searchTerm);
this.showActionFeedback(`🔍 Highlighted "${searchTerm}"`, '#ffc107', '#000');
}
}
/**
* Highlight text in the document
*/
highlightText(searchTerm) {
return this.safeOperation(() => {
// Remove previous highlights
document.querySelectorAll('.edit-highlight').forEach(el => {
el.outerHTML = el.innerHTML;
});
// Add new highlights
const contentArea = document.querySelector('#markitect-content') || document.body;
const walker = document.createTreeWalker(
contentArea,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
const text = textNode.textContent;
if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
const regex = new RegExp(`(${searchTerm})`, 'gi');
const highlightedHTML = text.replace(regex, '<span class="edit-highlight" style="background-color: yellow; padding: 0.1rem;">$1</span>');
const wrapper = document.createElement('div');
wrapper.innerHTML = highlightedHTML;
while (wrapper.firstChild) {
parent.insertBefore(wrapper.firstChild, textNode);
}
parent.removeChild(textNode);
}
});
}, null, 'highlightText');
}
/**
* Increase font size
*/
increaseFontSize() {
this.fontSize = Math.min(this.fontSize + 2, 24);
this.applyFontSize();
this.buildContent();
}
/**
* Decrease font size
*/
decreaseFontSize() {
this.fontSize = Math.max(this.fontSize - 2, 12);
this.applyFontSize();
this.buildContent();
}
/**
* Apply font size to document
*/
applyFontSize() {
const contentArea = document.querySelector('#markitect-content') || document.body;
contentArea.style.fontSize = `${this.fontSize}px`;
}
/**
* Copy page link to clipboard
*/
copyLink() {
return this.safeOperation(() => {
const url = window.location.href;
if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(() => {
this.showActionFeedback('📋 Link copied to clipboard', '#fd7e14');
});
} else {
// Fallback for older browsers
prompt('Copy this link:', url);
this.showActionFeedback('📋 Link displayed for copying', '#fd7e14');
}
}, null, 'copyLink');
}
/**
* Insert markdown formatting
*/
insertMarkdown(prefix, suffix, placeholder) {
// This would integrate with an actual text editor
// For now, just show what would be inserted
const text = `${prefix}${placeholder}${suffix}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
this.showActionFeedback(`📋 Copied: ${text}`, '#495057');
} else {
prompt('Markdown to copy:', text);
}
}
/**
* Show action feedback message
*/
showActionFeedback(message, backgroundColor, color = 'white') {
const feedback = document.createElement('div');
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${backgroundColor};
color: ${color};
padding: 0.5rem 1rem;
border-radius: 4px;
z-index: 9999;
font-size: 0.8rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
feedback.textContent = message;
document.body.appendChild(feedback);
setTimeout(() => {
if (feedback.parentNode) {
document.body.removeChild(feedback);
}
}, 3000);
}
/**
* Build the control content
* Override of base class method to provide edit-specific functionality
*/
/**
* Generate edit control content (called by base class buildContent)
*/
generateContent() {
return this.safeOperation(() => {
return this.generateEditToolsHTML();
}, 'Error generating edit content', 'generateContent');
}
/**
* Override buildContent to add control reference
*/
buildContent() {
super.buildContent();
// Store reference to this control for onclick handlers
if (this.element) {
this.element.editControl = this;
}
}
/**
* Exit edit mode
*/
exitEditMode() {
this.editingMode = 'view';
this.buildContent();
}
}
// Export for module systems or attach to global for direct usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = EditControl;
}
// Also expose globally: bundlers shim `module`, so the else-branch never ran in dist builds
if (typeof window !== 'undefined') {
window.EditControl = EditControl;
}