Complete library cleanup and documentation overhaul
## Library Code Cleanup (~1,200+ lines removed) - Remove legacy markdown system (markdown.js, previewer.js) - Delete unused EditContext code from ui/editor.js (~400 lines) - Remove version history UI components from form-renderer.js (~180 lines) - Clean unused CSS styles from insertr.css (~120 lines) - Update package.json dependencies (remove marked, turndown) ## Documentation Updates - README.md: Update from markdown to HTML-first approach - AGENTS.md: Add current architecture guidance and HTML-first principles - TODO.md: Complete rewrite with realistic roadmap and current status - demos/README.md: Update for development demo server usage ## System Reality Alignment - All documentation now reflects current working system - Removed aspirational features in favor of actual capabilities - Clear separation between development and production workflows - Accurate description of style-aware editor with HTML preservation ## Code Cleanup Benefits - Simplified codebase focused on HTML-first approach - Removed markdown conversion complexity - Cleaner build process without unused dependencies - Better alignment between frontend capabilities and documentation Ready for Phase 3a server updates with clean foundation.
This commit is contained in:
@@ -30,8 +30,5 @@
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"rollup": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^16.2.1",
|
||||
"turndown": "^7.2.1"
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ export class InsertrEditor {
|
||||
return 'link';
|
||||
}
|
||||
|
||||
// ALL text elements use markdown for consistent editing experience
|
||||
return 'markdown';
|
||||
// ALL text elements use text content type
|
||||
return 'text';
|
||||
}
|
||||
|
||||
handleCancel(meta) {
|
||||
|
||||
@@ -135,7 +135,7 @@ export class InsertrCore {
|
||||
const tag = element.tagName.toLowerCase();
|
||||
|
||||
if (element.classList.contains('insertr-group')) {
|
||||
return 'markdown';
|
||||
return 'group';
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
@@ -146,9 +146,9 @@ export class InsertrCore {
|
||||
case 'a': case 'button':
|
||||
return 'link';
|
||||
case 'div': case 'section':
|
||||
return 'markdown';
|
||||
return 'text';
|
||||
case 'span':
|
||||
return 'markdown'; // Match backend: spans support inline markdown
|
||||
return 'text';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* - .insertr-gate: Minimal styling for user-defined gates
|
||||
* - .insertr-auth-*: Authentication controls and buttons
|
||||
* - .insertr-form-*: Modal forms and inputs
|
||||
* - .insertr-version-*: Version history modal
|
||||
* - .insertr-style-aware-*: Style-aware editor components
|
||||
*/
|
||||
|
||||
/* =================================================================
|
||||
@@ -546,133 +546,7 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
VERSION HISTORY MODAL
|
||||
================================================================= */
|
||||
|
||||
.insertr-version-modal {
|
||||
padding: var(--insertr-spacing-lg);
|
||||
margin: 0;
|
||||
font-family: var(--insertr-font-family);
|
||||
font-size: var(--insertr-font-size-base);
|
||||
line-height: var(--insertr-line-height);
|
||||
color: var(--insertr-text-primary);
|
||||
background: var(--insertr-bg-primary);
|
||||
}
|
||||
|
||||
.insertr-version-header {
|
||||
margin: 0 0 var(--insertr-spacing-lg) 0;
|
||||
padding: 0 0 var(--insertr-spacing-md) 0;
|
||||
border-bottom: 1px solid var(--insertr-border-color);
|
||||
}
|
||||
|
||||
.insertr-version-title {
|
||||
margin: 0 0 var(--insertr-spacing-sm) 0;
|
||||
padding: 0;
|
||||
font-size: var(--insertr-font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
.insertr-version-help {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: var(--insertr-font-size-sm);
|
||||
color: var(--insertr-text-muted);
|
||||
}
|
||||
|
||||
.insertr-version-list {
|
||||
margin: 0 0 var(--insertr-spacing-lg) 0;
|
||||
padding: 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.insertr-version-item {
|
||||
padding: var(--insertr-spacing-sm);
|
||||
margin: 0 0 var(--insertr-spacing-sm) 0;
|
||||
border: 1px solid var(--insertr-border-color);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
cursor: pointer;
|
||||
transition: var(--insertr-transition);
|
||||
background: var(--insertr-bg-primary);
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
.insertr-version-item:hover {
|
||||
background: var(--insertr-bg-secondary);
|
||||
}
|
||||
|
||||
.insertr-version-item.selected {
|
||||
border-color: var(--insertr-primary);
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.insertr-version-date {
|
||||
font-weight: 500;
|
||||
color: var(--insertr-text-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.insertr-version-preview {
|
||||
margin: var(--insertr-spacing-xs) 0 0 0;
|
||||
padding: 0;
|
||||
font-size: var(--insertr-font-size-sm);
|
||||
color: var(--insertr-text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.insertr-version-actions {
|
||||
display: flex;
|
||||
gap: var(--insertr-spacing-sm);
|
||||
justify-content: flex-end;
|
||||
margin: 0;
|
||||
padding: var(--insertr-spacing-md) 0 0 0;
|
||||
border-top: 1px solid var(--insertr-border-color);
|
||||
}
|
||||
|
||||
.insertr-btn-restore,
|
||||
.insertr-btn-close {
|
||||
background: var(--insertr-primary);
|
||||
color: var(--insertr-text-inverse);
|
||||
border: none;
|
||||
border-radius: var(--insertr-border-radius);
|
||||
padding: var(--insertr-spacing-sm) var(--insertr-spacing-md);
|
||||
margin: 0;
|
||||
font-family: var(--insertr-font-family);
|
||||
font-size: var(--insertr-font-size-base);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--insertr-transition);
|
||||
line-height: var(--insertr-line-height);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.insertr-btn-close {
|
||||
background: var(--insertr-text-secondary);
|
||||
color: var(--insertr-text-inverse);
|
||||
}
|
||||
|
||||
.insertr-btn-close:hover {
|
||||
background: var(--insertr-text-primary);
|
||||
color: var(--insertr-text-inverse);
|
||||
}
|
||||
|
||||
.insertr-btn-restore:hover {
|
||||
background: var(--insertr-primary-hover);
|
||||
color: var(--insertr-text-inverse);
|
||||
}
|
||||
|
||||
.insertr-btn-restore:focus,
|
||||
.insertr-btn-close:focus {
|
||||
outline: 2px solid var(--insertr-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
STATUS AND FEEDBACK MESSAGES
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
* Editor - Handles all content types with style-aware approach
|
||||
*/
|
||||
import { StyleAwareEditor } from './style-aware-editor.js';
|
||||
import { Previewer } from './previewer.js';
|
||||
|
||||
export class Editor {
|
||||
constructor() {
|
||||
this.currentOverlay = null;
|
||||
this.currentStyleEditor = null;
|
||||
this.previewer = new Previewer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,8 +38,7 @@ export class Editor {
|
||||
this.close();
|
||||
},
|
||||
onChange: (content) => {
|
||||
// Optional: trigger live preview
|
||||
this.handlePreviewChange(primaryElement, content);
|
||||
// Optional: trigger change events if needed
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,237 +122,10 @@ export class Editor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle preview changes from style-aware editor
|
||||
* Handle content changes from style-aware editor
|
||||
*/
|
||||
handlePreviewChange(element, content) {
|
||||
// Implement live preview if needed
|
||||
// For now, we'll skip this to avoid complexity
|
||||
}
|
||||
|
||||
/**
|
||||
* Create editing form for any content type (legacy - keeping for compatibility)
|
||||
*/
|
||||
createForm(context, meta) {
|
||||
const config = this.getFieldConfig(context);
|
||||
const currentContent = context.extractContent();
|
||||
|
||||
const form = document.createElement('div');
|
||||
form.className = 'insertr-edit-form';
|
||||
|
||||
// Build form HTML
|
||||
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
|
||||
|
||||
// Markdown textarea (always present)
|
||||
formHTML += this.createMarkdownField(config, currentContent);
|
||||
|
||||
// URL field (for links only)
|
||||
if (config.includeUrl) {
|
||||
formHTML += this.createUrlField(currentContent);
|
||||
}
|
||||
|
||||
// Form actions
|
||||
formHTML += `
|
||||
<div class="insertr-form-actions">
|
||||
<button type="button" class="insertr-btn-save">Save</button>
|
||||
<button type="button" class="insertr-btn-cancel">Cancel</button>
|
||||
<button type="button" class="insertr-btn-history" data-content-id="${meta.contentId}">View History</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
form.innerHTML = formHTML;
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field configuration for any element type (markdown-first)
|
||||
*/
|
||||
getFieldConfig(context) {
|
||||
const elementCount = context.elements.length;
|
||||
const primaryElement = context.primaryElement;
|
||||
const isLink = primaryElement.tagName.toLowerCase() === 'a';
|
||||
|
||||
// Multi-element groups
|
||||
if (elementCount > 1) {
|
||||
return {
|
||||
type: 'markdown',
|
||||
includeUrl: false,
|
||||
label: `Group Content (${elementCount} elements)`,
|
||||
rows: Math.max(8, elementCount * 2),
|
||||
placeholder: 'Edit all content together using markdown...'
|
||||
};
|
||||
}
|
||||
|
||||
// Single elements - all get markdown by default
|
||||
const tag = primaryElement.tagName.toLowerCase();
|
||||
const baseConfig = {
|
||||
type: 'markdown',
|
||||
includeUrl: isLink,
|
||||
placeholder: 'Enter content using markdown...'
|
||||
};
|
||||
|
||||
// Customize by element type
|
||||
switch (tag) {
|
||||
case 'h1':
|
||||
return { ...baseConfig, label: 'Main Headline', rows: 1, placeholder: 'Enter main headline...' };
|
||||
case 'h2':
|
||||
return { ...baseConfig, label: 'Subheading', rows: 1, placeholder: 'Enter subheading...' };
|
||||
case 'h3': case 'h4': case 'h5': case 'h6':
|
||||
return { ...baseConfig, label: 'Heading', rows: 2, placeholder: 'Enter heading (markdown supported)...' };
|
||||
case 'p':
|
||||
return { ...baseConfig, label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' };
|
||||
case 'span':
|
||||
return { ...baseConfig, label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' };
|
||||
case 'button':
|
||||
return { ...baseConfig, label: 'Button Text', rows: 1, placeholder: 'Enter button text...' };
|
||||
case 'a':
|
||||
return { ...baseConfig, label: 'Link', rows: 2, placeholder: 'Enter link text (markdown supported)...' };
|
||||
default:
|
||||
return { ...baseConfig, label: 'Content', rows: 3, placeholder: 'Enter content using markdown...' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create markdown textarea field
|
||||
*/
|
||||
createMarkdownField(config, content) {
|
||||
const textContent = typeof content === 'object' ? content.text || '' : content;
|
||||
|
||||
return `
|
||||
<div class="insertr-form-group">
|
||||
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
|
||||
rows="${config.rows}"
|
||||
placeholder="${config.placeholder}">${this.escapeHtml(textContent)}</textarea>
|
||||
<div class="insertr-form-help">
|
||||
Supports Markdown formatting (bold, italic, links, etc.)
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create URL field for links
|
||||
*/
|
||||
createUrlField(content) {
|
||||
const url = typeof content === 'object' ? content.url || '' : '';
|
||||
|
||||
return `
|
||||
<div class="insertr-form-group">
|
||||
<label class="insertr-form-label">Link URL:</label>
|
||||
<input type="url" class="insertr-form-input" name="url"
|
||||
value="${this.escapeHtml(url)}"
|
||||
placeholder="https://example.com">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers
|
||||
*/
|
||||
setupEventHandlers(form, overlay, context, { onSave, onCancel }) {
|
||||
const textarea = form.querySelector('textarea');
|
||||
const urlInput = form.querySelector('input[name="url"]');
|
||||
const saveBtn = form.querySelector('.insertr-btn-save');
|
||||
const cancelBtn = form.querySelector('.insertr-btn-cancel');
|
||||
const historyBtn = form.querySelector('.insertr-btn-history');
|
||||
|
||||
// Initialize previewer
|
||||
this.previewer.setActiveContext(context);
|
||||
|
||||
// Setup live preview for content changes
|
||||
if (textarea) {
|
||||
textarea.addEventListener('input', () => {
|
||||
const content = this.extractFormData(form);
|
||||
this.previewer.schedulePreview(context, content);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup live preview for URL changes (links only)
|
||||
if (urlInput) {
|
||||
urlInput.addEventListener('input', () => {
|
||||
const content = this.extractFormData(form);
|
||||
this.previewer.schedulePreview(context, content);
|
||||
});
|
||||
}
|
||||
|
||||
// Save handler
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => {
|
||||
const content = this.extractFormData(form);
|
||||
|
||||
// Apply final content to elements
|
||||
context.applyContent(content);
|
||||
|
||||
// Update stored original content to match current state
|
||||
// This makes the saved content the new baseline for future edits
|
||||
context.updateOriginalContent();
|
||||
|
||||
// Clear preview styling (won't restore content since original matches current)
|
||||
this.previewer.clearPreview();
|
||||
|
||||
// Callback with the content
|
||||
onSave(content);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel handler
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
this.previewer.clearPreview();
|
||||
onCancel();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
// History handler
|
||||
if (historyBtn) {
|
||||
historyBtn.addEventListener('click', () => {
|
||||
const contentId = historyBtn.getAttribute('data-content-id');
|
||||
console.log('Version history not implemented yet for:', contentId);
|
||||
// TODO: Implement version history integration
|
||||
});
|
||||
}
|
||||
|
||||
// ESC key handler
|
||||
const keyHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.previewer.clearPreview();
|
||||
onCancel();
|
||||
this.close();
|
||||
document.removeEventListener('keydown', keyHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keyHandler);
|
||||
|
||||
// Click outside handler
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
this.previewer.clearPreview();
|
||||
onCancel();
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract form data consistently
|
||||
*/
|
||||
extractFormData(form) {
|
||||
const textarea = form.querySelector('textarea[name="content"]');
|
||||
const urlInput = form.querySelector('input[name="url"]');
|
||||
|
||||
const content = textarea ? textarea.value : '';
|
||||
|
||||
if (urlInput) {
|
||||
// Link content
|
||||
return {
|
||||
text: content,
|
||||
url: urlInput.value
|
||||
};
|
||||
}
|
||||
|
||||
// Regular content
|
||||
return content;
|
||||
handleContentChange(element, content) {
|
||||
// Optional: implement change events if needed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,10 +215,6 @@ export class Editor {
|
||||
* Close current editor
|
||||
*/
|
||||
close() {
|
||||
if (this.previewer) {
|
||||
this.previewer.clearPreview();
|
||||
}
|
||||
|
||||
if (this.currentStyleEditor) {
|
||||
this.currentStyleEditor.destroy();
|
||||
this.currentStyleEditor = null;
|
||||
@@ -469,129 +235,4 @@ export class Editor {
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EditContext - Represents content elements for editing
|
||||
*/
|
||||
class EditContext {
|
||||
constructor(elements, currentContent) {
|
||||
this.elements = elements;
|
||||
this.primaryElement = elements[0];
|
||||
this.originalContent = null;
|
||||
this.currentContent = currentContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content from elements in markdown format
|
||||
*/
|
||||
extractContent() {
|
||||
if (this.elements.length === 1) {
|
||||
const element = this.elements[0];
|
||||
|
||||
// Handle links specially
|
||||
if (element.tagName.toLowerCase() === 'a') {
|
||||
return {
|
||||
text: markdownConverter.htmlToMarkdown(element.innerHTML),
|
||||
url: element.href
|
||||
};
|
||||
}
|
||||
|
||||
// Single element - convert to markdown
|
||||
return markdownConverter.htmlToMarkdown(element.innerHTML);
|
||||
} else {
|
||||
// Multiple elements - use group extraction
|
||||
return markdownConverter.extractGroupMarkdown(this.elements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply content to elements from markdown/object
|
||||
*/
|
||||
applyContent(content) {
|
||||
if (this.elements.length === 1) {
|
||||
const element = this.elements[0];
|
||||
|
||||
// Handle links specially
|
||||
if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') {
|
||||
element.innerHTML = markdownConverter.markdownToHtml(content.text || '');
|
||||
if (content.url) {
|
||||
element.href = content.url;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single element - convert markdown to HTML
|
||||
const html = markdownConverter.markdownToHtml(content);
|
||||
element.innerHTML = html;
|
||||
} else {
|
||||
// Multiple elements - use group update
|
||||
markdownConverter.updateGroupElements(this.elements, content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store original content for preview restoration
|
||||
*/
|
||||
storeOriginalContent() {
|
||||
this.originalContent = this.elements.map(el => ({
|
||||
innerHTML: el.innerHTML,
|
||||
href: el.href // Store href for links
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original content (for preview cancellation)
|
||||
*/
|
||||
restoreOriginalContent() {
|
||||
if (this.originalContent) {
|
||||
this.elements.forEach((el, index) => {
|
||||
if (this.originalContent[index] !== undefined) {
|
||||
el.innerHTML = this.originalContent[index].innerHTML;
|
||||
if (this.originalContent[index].href) {
|
||||
el.href = this.originalContent[index].href;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update original content to match current element state (after save)
|
||||
* This makes the current content the new baseline for future cancellations
|
||||
*/
|
||||
updateOriginalContent() {
|
||||
this.originalContent = this.elements.map(el => ({
|
||||
innerHTML: el.innerHTML,
|
||||
href: el.href // Store href for links
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply preview styling to all elements
|
||||
*/
|
||||
applyPreviewStyling() {
|
||||
this.elements.forEach(el => {
|
||||
el.classList.add('insertr-preview-active');
|
||||
});
|
||||
|
||||
// Also apply to containers if they're groups
|
||||
if (this.primaryElement.classList.contains('insertr-group')) {
|
||||
this.primaryElement.classList.add('insertr-preview-active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove preview styling from all elements
|
||||
*/
|
||||
removePreviewStyling() {
|
||||
this.elements.forEach(el => {
|
||||
el.classList.remove('insertr-preview-active');
|
||||
});
|
||||
|
||||
// Also remove from containers
|
||||
if (this.primaryElement.classList.contains('insertr-group')) {
|
||||
this.primaryElement.classList.remove('insertr-preview-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* InsertrFormRenderer - Form renderer using markdown-first approach
|
||||
* InsertrFormRenderer - Form renderer for content editing
|
||||
* Thin wrapper around the Editor system
|
||||
*/
|
||||
import { Editor } from './editor.js';
|
||||
@@ -52,190 +52,5 @@ export class InsertrFormRenderer {
|
||||
this.editor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show version history modal (placeholder for future implementation)
|
||||
*/
|
||||
async showVersionHistory(contentId, element, onRestore) {
|
||||
try {
|
||||
// Get version history from API
|
||||
const apiClient = this.getApiClient();
|
||||
if (!apiClient) {
|
||||
console.warn('No API client configured for version history');
|
||||
return;
|
||||
}
|
||||
|
||||
const versions = await apiClient.getContentVersions(contentId);
|
||||
|
||||
// Create version history modal
|
||||
const historyModal = this.createVersionHistoryModal(contentId, versions, onRestore);
|
||||
document.body.appendChild(historyModal);
|
||||
|
||||
// Setup handlers
|
||||
this.setupVersionHistoryHandlers(historyModal, contentId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load version history:', error);
|
||||
this.showVersionHistoryError('Failed to load version history. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create version history modal (simplified placeholder)
|
||||
*/
|
||||
createVersionHistoryModal(contentId, versions, onRestore) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'insertr-version-modal';
|
||||
|
||||
let versionsHTML = '';
|
||||
if (versions && versions.length > 0) {
|
||||
versionsHTML = versions.map((version, index) => `
|
||||
<div class="insertr-version-item" data-version-id="${version.version_id}">
|
||||
<div class="insertr-version-meta">
|
||||
<span class="insertr-version-label">${index === 0 ? 'Previous Version' : `Version ${versions.length - index}`}</span>
|
||||
<span class="insertr-version-date">${this.formatDate(version.created_at)}</span>
|
||||
${version.created_by ? `<span class="insertr-version-user">by ${version.created_by}</span>` : ''}
|
||||
</div>
|
||||
<div class="insertr-version-content">${this.escapeHtml(this.truncateContent(version.value, 100))}</div>
|
||||
<div class="insertr-version-actions">
|
||||
<button type="button" class="insertr-btn-restore" data-version-id="${version.version_id}">Restore</button>
|
||||
<button type="button" class="insertr-btn-view-diff" data-version-id="${version.version_id}">View Full</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
versionsHTML = '<div class="insertr-version-empty">No previous versions found</div>';
|
||||
}
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="insertr-version-backdrop">
|
||||
<div class="insertr-version-content-modal">
|
||||
<div class="insertr-version-header">
|
||||
<h3>Version History</h3>
|
||||
<button type="button" class="insertr-btn-close">×</button>
|
||||
</div>
|
||||
<div class="insertr-version-list">
|
||||
${versionsHTML}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup version history modal handlers
|
||||
*/
|
||||
setupVersionHistoryHandlers(modal, contentId) {
|
||||
const closeBtn = modal.querySelector('.insertr-btn-close');
|
||||
const backdrop = modal.querySelector('.insertr-version-backdrop');
|
||||
|
||||
// Close handlers
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => modal.remove());
|
||||
}
|
||||
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop) {
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Restore handlers
|
||||
const restoreButtons = modal.querySelectorAll('.insertr-btn-restore');
|
||||
restoreButtons.forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const versionId = btn.getAttribute('data-version-id');
|
||||
if (await this.confirmRestore()) {
|
||||
await this.restoreVersion(contentId, versionId);
|
||||
modal.remove();
|
||||
this.closeForm();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// View diff handlers
|
||||
const viewButtons = modal.querySelectorAll('.insertr-btn-view-diff');
|
||||
viewButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const versionId = btn.getAttribute('data-version-id');
|
||||
this.showVersionDetails(versionId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods for version history
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
// Less than 24 hours ago
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
const hours = Math.floor(diff / (60 * 60 * 1000));
|
||||
if (hours < 1) {
|
||||
const minutes = Math.floor(diff / (60 * 1000));
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
|
||||
// Less than 7 days ago
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
// Older - show actual date
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
truncateContent(content, maxLength) {
|
||||
if (content.length <= maxLength) return content;
|
||||
return content.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
async confirmRestore() {
|
||||
return confirm('Are you sure you want to restore this version? This will replace the current content.');
|
||||
}
|
||||
|
||||
async restoreVersion(contentId, versionId) {
|
||||
try {
|
||||
const apiClient = this.getApiClient();
|
||||
await apiClient.rollbackContent(contentId, versionId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to restore version:', error);
|
||||
alert('Failed to restore version. Please try again.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
showVersionDetails(versionId) {
|
||||
// TODO: Implement detailed version view with diff
|
||||
alert(`Version details not implemented yet (Version ID: ${versionId})`);
|
||||
}
|
||||
|
||||
showVersionHistoryError(message) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Helper to get API client
|
||||
getApiClient() {
|
||||
return this.apiClient || window.insertrAPIClient || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
if (typeof text !== 'string') return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* Previewer - Handles live preview for all content types
|
||||
*/
|
||||
import { markdownConverter } from '../utils/markdown.js';
|
||||
|
||||
export class Previewer {
|
||||
constructor() {
|
||||
this.previewTimeout = null;
|
||||
this.activeContext = null;
|
||||
this.resizeObserver = null;
|
||||
this.onHeightChange = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active editing context for preview
|
||||
*/
|
||||
setActiveContext(context) {
|
||||
this.clearPreview();
|
||||
this.activeContext = context;
|
||||
this.startResizeObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a preview update with debouncing
|
||||
*/
|
||||
schedulePreview(context, content) {
|
||||
// Clear existing timeout
|
||||
if (this.previewTimeout) {
|
||||
clearTimeout(this.previewTimeout);
|
||||
}
|
||||
|
||||
// Schedule new preview with 500ms debounce
|
||||
this.previewTimeout = setTimeout(() => {
|
||||
this.updatePreview(context, content);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview with new content
|
||||
*/
|
||||
updatePreview(context, content) {
|
||||
// Store original content if first preview
|
||||
if (!context.originalContent) {
|
||||
context.storeOriginalContent();
|
||||
}
|
||||
|
||||
// Apply preview content to elements
|
||||
this.applyPreviewContent(context, content);
|
||||
context.applyPreviewStyling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply preview content to context elements
|
||||
*/
|
||||
applyPreviewContent(context, content) {
|
||||
if (context.elements.length === 1) {
|
||||
const element = context.elements[0];
|
||||
|
||||
// Handle links specially
|
||||
if (element.tagName.toLowerCase() === 'a') {
|
||||
if (typeof content === 'object') {
|
||||
// Update link text (markdown to HTML)
|
||||
if (content.text !== undefined) {
|
||||
const html = markdownConverter.markdownToHtml(content.text);
|
||||
element.innerHTML = html;
|
||||
}
|
||||
// Update link URL
|
||||
if (content.url !== undefined && content.url.trim()) {
|
||||
element.href = content.url;
|
||||
}
|
||||
} else if (content && content.trim()) {
|
||||
// Just markdown content for link text
|
||||
const html = markdownConverter.markdownToHtml(content);
|
||||
element.innerHTML = html;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular single element
|
||||
if (content && content.trim()) {
|
||||
const html = markdownConverter.markdownToHtml(content);
|
||||
element.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
// Multiple elements - use group update
|
||||
if (content && content.trim()) {
|
||||
markdownConverter.updateGroupElements(context.elements, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all preview state and restore original content
|
||||
*/
|
||||
clearPreview() {
|
||||
if (this.activeContext) {
|
||||
this.activeContext.restoreOriginalContent();
|
||||
this.activeContext.removePreviewStyling();
|
||||
this.activeContext = null;
|
||||
}
|
||||
|
||||
if (this.previewTimeout) {
|
||||
clearTimeout(this.previewTimeout);
|
||||
this.previewTimeout = null;
|
||||
}
|
||||
|
||||
this.stopResizeObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start observing element size changes for modal repositioning
|
||||
*/
|
||||
startResizeObserver() {
|
||||
this.stopResizeObserver();
|
||||
|
||||
if (this.activeContext) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
// Handle height changes for modal repositioning
|
||||
if (this.onHeightChange) {
|
||||
this.onHeightChange(this.activeContext.primaryElement);
|
||||
}
|
||||
});
|
||||
|
||||
// Observe all elements in the context
|
||||
this.activeContext.elements.forEach(el => {
|
||||
this.resizeObserver.observe(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop observing element size changes
|
||||
*/
|
||||
stopResizeObserver() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for height changes (for modal repositioning)
|
||||
*/
|
||||
setHeightChangeCallback(callback) {
|
||||
this.onHeightChange = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique element ID for tracking
|
||||
*/
|
||||
getElementId(element) {
|
||||
if (!element._insertrId) {
|
||||
element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
return element._insertrId;
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* Markdown conversion utilities using Marked and Turndown
|
||||
*/
|
||||
import { marked } from 'marked';
|
||||
import TurndownService from 'turndown';
|
||||
|
||||
/**
|
||||
* MarkdownConverter - Handles bidirectional HTML ↔ Markdown conversion
|
||||
*/
|
||||
export class MarkdownConverter {
|
||||
constructor() {
|
||||
this.initializeMarked();
|
||||
this.initializeTurndown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure marked for HTML output - MINIMAL MODE
|
||||
* Only supports: **bold**, *italic*, and [links](url)
|
||||
* Matches server-side goldmark configuration
|
||||
*/
|
||||
initializeMarked() {
|
||||
marked.setOptions({
|
||||
gfm: false, // Disable GFM to match server minimal mode
|
||||
breaks: true, // Convert \n to <br> (matches server)
|
||||
pedantic: false, // Don't be overly strict
|
||||
sanitize: false, // Allow HTML (we control the input)
|
||||
smartLists: false, // Disable lists (not supported on server)
|
||||
smartypants: false // Don't convert quotes/dashes
|
||||
});
|
||||
|
||||
// Override renderers to restrict to minimal feature set
|
||||
marked.use({
|
||||
renderer: {
|
||||
// Disable headings - treat as plain text
|
||||
heading(text, level) {
|
||||
return text;
|
||||
},
|
||||
// Disable lists - treat as plain text
|
||||
list(body, ordered, start) {
|
||||
return body.replace(/<\/?li>/g, '');
|
||||
},
|
||||
listitem(text) {
|
||||
return text + '\n';
|
||||
},
|
||||
// Disable code blocks - treat as plain text
|
||||
code(code, language) {
|
||||
return code;
|
||||
},
|
||||
blockquote(quote) {
|
||||
return quote; // Disable blockquotes - treat as plain text
|
||||
},
|
||||
// Disable horizontal rules
|
||||
hr() {
|
||||
return '';
|
||||
},
|
||||
// Disable tables
|
||||
table(header, body) {
|
||||
return header + body;
|
||||
},
|
||||
tablecell(content, flags) {
|
||||
return content;
|
||||
},
|
||||
tablerow(content) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure turndown for markdown output - MINIMAL MODE
|
||||
* Only supports: **bold**, *italic*, and [links](url)
|
||||
* Matches server-side goldmark configuration
|
||||
*/
|
||||
initializeTurndown() {
|
||||
this.turndown = new TurndownService({
|
||||
// Minimal configuration - only basic formatting
|
||||
headingStyle: 'atx', // # headers (but will be disabled)
|
||||
hr: '---', // horizontal rule (but will be disabled)
|
||||
bulletListMarker: '-', // bullet list (but will be disabled)
|
||||
codeBlockStyle: 'fenced', // code blocks (but will be disabled)
|
||||
fence: '```', // fence marker (but will be disabled)
|
||||
emDelimiter: '*', // *italic* - matches server
|
||||
strongDelimiter: '**', // **bold** - matches server
|
||||
linkStyle: 'inlined', // [text](url) - matches server
|
||||
linkReferenceStyle: 'full' // full reference links
|
||||
});
|
||||
|
||||
// Add custom rules for better conversion
|
||||
this.addTurndownRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom turndown rules - MINIMAL MODE
|
||||
* Only supports: **bold**, *italic*, and [links](url)
|
||||
* Disables all other formatting to match server
|
||||
*/
|
||||
addTurndownRules() {
|
||||
// Handle paragraph spacing properly - ensure double newlines between paragraphs
|
||||
this.turndown.addRule('paragraph', {
|
||||
filter: 'p',
|
||||
replacement: function (content) {
|
||||
if (!content.trim()) return '';
|
||||
return content.trim() + '\n\n';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bold text in markdown - keep this (supported)
|
||||
this.turndown.addRule('bold', {
|
||||
filter: ['strong', 'b'],
|
||||
replacement: function (content) {
|
||||
if (!content.trim()) return '';
|
||||
return '**' + content + '**';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle italic text in markdown - keep this (supported)
|
||||
this.turndown.addRule('italic', {
|
||||
filter: ['em', 'i'],
|
||||
replacement: function (content) {
|
||||
if (!content.trim()) return '';
|
||||
return '*' + content + '*';
|
||||
}
|
||||
});
|
||||
|
||||
// DISABLE unsupported features - convert to plain text
|
||||
this.turndown.addRule('disableHeadings', {
|
||||
filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||
replacement: function (content) {
|
||||
return content; // Just return text content, no # markup
|
||||
}
|
||||
});
|
||||
|
||||
this.turndown.addRule('disableLists', {
|
||||
filter: ['ul', 'ol', 'li'],
|
||||
replacement: function (content) {
|
||||
return content; // Just return text content, no list markup
|
||||
}
|
||||
});
|
||||
|
||||
this.turndown.addRule('disableCode', {
|
||||
filter: ['pre', 'code'],
|
||||
replacement: function (content) {
|
||||
return content; // Just return text content, no code markup
|
||||
}
|
||||
});
|
||||
|
||||
this.turndown.addRule('disableBlockquotes', {
|
||||
filter: 'blockquote',
|
||||
replacement: function (content) {
|
||||
return content; // Just return text content, no > markup
|
||||
}
|
||||
});
|
||||
|
||||
this.turndown.addRule('disableHR', {
|
||||
filter: 'hr',
|
||||
replacement: function () {
|
||||
return ''; // Remove horizontal rules entirely
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML to Markdown
|
||||
* @param {string} html - HTML string to convert
|
||||
* @returns {string} - Markdown string
|
||||
*/
|
||||
htmlToMarkdown(html) {
|
||||
if (!html || html.trim() === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const markdown = this.turndown.turndown(html);
|
||||
// Clean up and normalize newlines for proper paragraph separation
|
||||
return markdown
|
||||
.replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2
|
||||
.replace(/^\n+|\n+$/g, '') // Remove leading/trailing newlines
|
||||
.trim(); // Remove other whitespace
|
||||
} catch (error) {
|
||||
console.warn('HTML to Markdown conversion failed:', error);
|
||||
// Fallback: extract text content
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
return tempDiv.textContent || tempDiv.innerText || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Markdown to HTML
|
||||
* @param {string} markdown - Markdown string to convert
|
||||
* @returns {string} - HTML string
|
||||
*/
|
||||
markdownToHtml(markdown) {
|
||||
if (!markdown || markdown.trim() === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const html = marked(markdown);
|
||||
return html;
|
||||
} catch (error) {
|
||||
console.warn('Markdown to HTML conversion failed:', error);
|
||||
// Fallback: convert line breaks to paragraphs
|
||||
return markdown
|
||||
.split(/\n\s*\n/)
|
||||
.filter(p => p.trim())
|
||||
.map(p => `<p>${p.trim()}</p>`)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract HTML content from a group of elements
|
||||
* @param {HTMLElement[]} elements - Array of DOM elements
|
||||
* @returns {string} - Combined HTML content
|
||||
*/
|
||||
extractGroupHTML(elements) {
|
||||
const htmlParts = [];
|
||||
|
||||
elements.forEach(element => {
|
||||
// Wrap inner content in paragraph tags to preserve structure
|
||||
const html = element.innerHTML.trim();
|
||||
if (html) {
|
||||
// If element is already a paragraph, use its outer HTML
|
||||
if (element.tagName.toLowerCase() === 'p') {
|
||||
htmlParts.push(element.outerHTML);
|
||||
} else {
|
||||
// Wrap in paragraph tags
|
||||
htmlParts.push(`<p>${html}</p>`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return htmlParts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML content from group elements to markdown
|
||||
* @param {HTMLElement[]} elements - Array of DOM elements
|
||||
* @returns {string} - Markdown representation
|
||||
*/
|
||||
extractGroupMarkdown(elements) {
|
||||
const html = this.extractGroupHTML(elements);
|
||||
const markdown = this.htmlToMarkdown(html);
|
||||
return markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update group elements with markdown content
|
||||
* @param {HTMLElement[]} elements - Array of DOM elements to update
|
||||
* @param {string} markdown - Markdown content to render
|
||||
*/
|
||||
updateGroupElements(elements, markdown) {
|
||||
const html = this.markdownToHtml(markdown);
|
||||
|
||||
// Split HTML into paragraphs
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
const paragraphs = Array.from(tempDiv.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6'));
|
||||
|
||||
// Handle case where we have more/fewer paragraphs than elements
|
||||
const maxCount = Math.max(elements.length, paragraphs.length);
|
||||
|
||||
for (let i = 0; i < maxCount; i++) {
|
||||
if (i < elements.length && i < paragraphs.length) {
|
||||
// Update existing element with corresponding paragraph
|
||||
elements[i].innerHTML = paragraphs[i].innerHTML;
|
||||
} else if (i < elements.length) {
|
||||
// More elements than paragraphs - clear extra elements
|
||||
elements[i].innerHTML = '';
|
||||
} else if (i < paragraphs.length) {
|
||||
// More paragraphs than elements - create new element
|
||||
const newElement = document.createElement('p');
|
||||
newElement.innerHTML = paragraphs[i].innerHTML;
|
||||
|
||||
// Insert after the last existing element
|
||||
const lastElement = elements[elements.length - 1];
|
||||
lastElement.parentNode.insertBefore(newElement, lastElement.nextSibling);
|
||||
elements.push(newElement); // Add to our elements array for future updates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const markdownConverter = new MarkdownConverter();
|
||||
Reference in New Issue
Block a user