feat: unify link editing interfaces with comprehensive polish

Multi-Property Editor Polish:
- Add comprehensive form styling (.insertr-form-group, .insertr-form-input, etc.)
- Professional layout with titles, validation, auto-focus, and help text
- Enhanced link/button/image editors with real-time validation
- Consistent spacing, colors, and visual hierarchy

Smart Default Formatting:
- Add Bold, Italic, Link options when not detected in content
- Intelligent detection respects existing developer styles
- Visual distinction for default vs detected styles with info-colored borders
- Content-aware: only adds to elements that benefit from text formatting

Link Interface Unification:
- Create shared createLinkConfigurationForm() component
- Eliminate code duplication between direct editing and popup creation
- Update createLinkEditor() and showLinkConfigPopup() to use shared component
- Fix link button styling to match other style buttons with preview content

Benefits:
- Consistent professional editing experience across all interfaces
- Reduced maintenance burden through code unification
- Enhanced UX with validation, keyboard shortcuts, and visual feedback
- Maintains CLASSES.md philosophy while improving out-of-box experience
This commit is contained in:
2025-09-21 20:47:22 +02:00
parent b75eda2a87
commit d44bdd41b4
3 changed files with 760 additions and 279 deletions

View File

@@ -442,6 +442,46 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Default formatting style buttons */
.insertr-style-btn.insertr-default-style {
border-color: var(--insertr-info);
background: rgba(23, 162, 184, 0.1);
position: relative;
}
.insertr-style-btn.insertr-default-style:hover {
border-color: var(--insertr-info);
background: rgba(23, 162, 184, 0.2);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3);
}
.insertr-style-btn.insertr-default-style:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(23, 162, 184, 0.2);
}
/* Default preview content styling */
.insertr-default-preview {
font-size: var(--insertr-font-size-sm);
font-weight: 500;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
display: inline-block;
}
/* Small indicator for default styles */
.insertr-style-btn.insertr-default-style::after {
content: '';
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
background: var(--insertr-info);
border-radius: 50%;
opacity: 0.7;
}
/* Editor components */
.insertr-simple-editor,
.insertr-rich-editor,
@@ -479,7 +519,126 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
overflow-y: auto;
}
/* =================================================================
MULTI-PROPERTY FORM COMPONENTS
Professional form styling for direct editors (links, buttons, images)
================================================================= */
/* Direct editor container */
.insertr-direct-editor {
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: var(--insertr-spacing-lg);
min-width: 400px;
max-width: 600px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
}
/* Form groups */
.insertr-form-group {
margin-bottom: var(--insertr-spacing-md);
}
.insertr-form-group:last-child {
margin-bottom: 0;
}
/* Form labels */
.insertr-form-label {
display: block;
margin-bottom: var(--insertr-spacing-xs);
font-weight: 500;
font-size: var(--insertr-font-size-sm);
color: var(--insertr-text-primary);
line-height: 1.4;
}
/* Form inputs and selects */
.insertr-form-input,
.insertr-form-select {
width: 100%;
padding: var(--insertr-spacing-sm) var(--insertr-spacing-md);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
font-size: var(--insertr-font-size-base);
font-family: var(--insertr-font-family);
line-height: 1.4;
color: var(--insertr-text-primary);
background: var(--insertr-bg-primary);
transition: var(--insertr-transition);
box-sizing: border-box;
}
/* Focus states */
.insertr-form-input:focus,
.insertr-form-select:focus {
outline: none;
border-color: var(--insertr-primary);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
/* Hover states for selects */
.insertr-form-select:hover {
border-color: var(--insertr-text-secondary);
}
/* Disabled states */
.insertr-form-input:disabled,
.insertr-form-select:disabled {
background: var(--insertr-bg-secondary);
color: var(--insertr-text-muted);
cursor: not-allowed;
opacity: 0.6;
}
/* Error states */
.insertr-form-input.insertr-error,
.insertr-form-select.insertr-error {
border-color: var(--insertr-danger);
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
}
/* Success states */
.insertr-form-input.insertr-success,
.insertr-form-select.insertr-success {
border-color: var(--insertr-success);
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
}
/* Form validation messages */
.insertr-form-message {
margin-top: var(--insertr-spacing-xs);
font-size: var(--insertr-font-size-sm);
line-height: 1.3;
}
.insertr-form-message.insertr-error {
color: var(--insertr-danger);
}
.insertr-form-message.insertr-success {
color: var(--insertr-success);
}
.insertr-form-message.insertr-info {
color: var(--insertr-info);
}
/* Specific editor variants */
.insertr-link-editor {
/* Link-specific styling if needed */
}
.insertr-button-editor {
/* Button-specific styling if needed */
}
.insertr-image-editor {
/* Image-specific styling if needed */
}
/* Form actions */
.insertr-form-actions {

View File

@@ -144,7 +144,8 @@ export class StyleAwareEditor {
break;
}
// Add toolbar if enabled and we have styled content (rich editor only)
// Add toolbar if enabled and we have any formatting options (rich editor only)
// This includes both detected styles and smart defaults
if (this.options.showToolbar && analysis.strategy === 'rich' && analysis.styles.size > 0) {
this.createStyleToolbar(analysis.styles, analysis.structure);
}
@@ -172,43 +173,215 @@ export class StyleAwareEditor {
}
/**
* Create link editor with URL and text fields
* Create unified link configuration component
* Used by both direct link editing and popup link creation
*
* @param {Object} options - Configuration options
* @param {string} options.text - Link text
* @param {string} options.url - Current URL
* @param {string} options.target - Current target
* @param {string} options.mode - 'direct' or 'popup'
* @param {boolean} options.showTextField - Whether to show text editing
* @param {Function} options.onSave - Save callback
* @param {Function} options.onCancel - Cancel callback
* @param {Function} options.onRemove - Remove callback (optional)
* @returns {HTMLElement} - The link configuration form
*/
createLinkEditor() {
createLinkConfigurationForm(options = {}) {
const {
text = '',
url = '',
target = '',
mode = 'direct',
showTextField = true,
onSave = () => {},
onCancel = () => {},
onRemove = null
} = options;
// Create form container
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-link-editor';
// Text field
// Create title
const title = document.createElement('h3');
title.className = 'insertr-editor-title';
title.textContent = url ? 'Edit Link' : 'Add Link';
title.style.cssText = `
margin: 0 0 ${getComputedStyle(document.documentElement).getPropertyValue('--insertr-spacing-md') || '16px'} 0;
font-size: 18px;
font-weight: 600;
color: var(--insertr-text-primary);
`;
form.appendChild(title);
// Add help text for popup mode
if (mode === 'popup' && text) {
const helpText = document.createElement('p');
helpText.className = 'insertr-form-message insertr-info';
helpText.textContent = `Configure link for: "${text}"`;
helpText.style.marginBottom = 'var(--insertr-spacing-md)';
form.appendChild(helpText);
}
// Text field (only show if requested)
if (showTextField) {
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Link Text</label>
<input type="text" class="insertr-form-input" id="link-text" value="${this.element.textContent}" placeholder="Link text">
<input type="text" class="insertr-form-input" id="link-text" value="${this.escapeHtml(text)}" placeholder="Enter link text" required>
<div class="insertr-form-message insertr-info" style="display: none;" id="link-text-message"></div>
`;
form.appendChild(textGroup);
}
// URL field
// URL field with validation
const urlGroup = document.createElement('div');
urlGroup.className = 'insertr-form-group';
urlGroup.innerHTML = `
<label class="insertr-form-label">URL</label>
<input type="url" class="insertr-form-input" id="link-url" value="${this.element.href || ''}" placeholder="https://example.com">
<input type="url" class="insertr-form-input" id="link-url" value="${this.escapeHtml(url)}" placeholder="https://example.com" required>
<div class="insertr-form-message insertr-info" style="display: none;" id="link-url-message"></div>
`;
form.appendChild(urlGroup);
// Target field
const targetGroup = document.createElement('div');
targetGroup.className = 'insertr-form-group';
targetGroup.innerHTML = `
<label class="insertr-form-label">Target</label>
<label class="insertr-form-label">Open In</label>
<select class="insertr-form-select" id="link-target">
<option value="">Same window</option>
<option value="_blank" ${this.element.target === '_blank' ? 'selected' : ''}>New window</option>
<option value="_blank" ${target === '_blank' ? 'selected' : ''}>New window/tab</option>
</select>
<div class="insertr-form-message insertr-info">Choose how the link opens when clicked</div>
`;
form.appendChild(textGroup);
form.appendChild(urlGroup);
form.appendChild(targetGroup);
// Form actions
const actions = document.createElement('div');
actions.className = 'insertr-form-actions';
// Save button
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'insertr-btn-save';
saveBtn.textContent = url ? 'Update Link' : 'Create Link';
// Cancel button
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'insertr-btn-cancel';
cancelBtn.textContent = 'Cancel';
// Remove button (if editing existing link)
if (url && onRemove) {
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'insertr-btn-cancel';
removeBtn.textContent = 'Remove Link';
removeBtn.style.marginRight = 'auto';
actions.appendChild(removeBtn);
removeBtn.addEventListener('click', (e) => {
e.preventDefault();
onRemove();
});
}
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
form.appendChild(actions);
// Add validation
this.addLinkValidation(form);
// Event handlers
saveBtn.addEventListener('click', (e) => {
e.preventDefault();
const linkText = showTextField ? document.getElementById('link-text')?.value || text : text;
const linkUrl = document.getElementById('link-url').value;
const linkTarget = document.getElementById('link-target').value;
// Basic validation
if (showTextField && !linkText.trim()) {
this.setValidationState(
document.getElementById('link-text'),
document.getElementById('link-text-message'),
'Link text is required',
'error'
);
return;
}
if (!linkUrl.trim()) {
this.setValidationState(
document.getElementById('link-url'),
document.getElementById('link-url-message'),
'URL is required',
'error'
);
return;
}
onSave({
text: linkText,
url: linkUrl,
target: linkTarget
});
});
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
onCancel();
});
// Auto-focus first input
setTimeout(() => {
const firstInput = form.querySelector(showTextField ? '#link-text' : '#link-url');
if (firstInput) {
firstInput.focus();
firstInput.select();
}
}, 100);
return form;
}
/**
* Create link editor with URL and text fields
*/
createLinkEditor() {
const form = this.createLinkConfigurationForm({
text: this.element.textContent,
url: this.element.href || '',
target: this.element.target || '',
mode: 'direct',
showTextField: true,
onSave: (linkData) => {
// Apply the changes to the element
this.element.textContent = linkData.text;
this.element.href = linkData.url;
if (linkData.target) {
this.element.target = linkData.target;
} else {
this.element.removeAttribute('target');
}
// Trigger change event
this.onChange();
// Close editor (will be handled by parent)
this.destroy();
},
onCancel: () => {
this.destroy();
}
});
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
@@ -220,18 +393,40 @@ export class StyleAwareEditor {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-button-editor';
// Create a title for the editor
const title = document.createElement('h3');
title.className = 'insertr-editor-title';
title.textContent = 'Edit Button';
title.style.cssText = `
margin: 0 0 ${getComputedStyle(document.documentElement).getPropertyValue('--insertr-spacing-md') || '16px'} 0;
font-size: 18px;
font-weight: 600;
color: var(--insertr-text-primary);
`;
form.appendChild(title);
// Text field
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Button Text</label>
<input type="text" class="insertr-form-input" id="button-text" value="${this.element.textContent}" placeholder="Button text">
<input type="text" class="insertr-form-input" id="button-text" value="${this.escapeHtml(this.element.textContent)}" placeholder="Enter button text" required>
<div class="insertr-form-message insertr-info">This text will appear on the button</div>
`;
form.appendChild(textGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
// Focus the input
setTimeout(() => {
const textInput = form.querySelector('#button-text');
if (textInput) {
textInput.focus();
textInput.select();
}
}, 100);
}
/**
@@ -241,12 +436,25 @@ export class StyleAwareEditor {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-image-editor';
// Create a title for the editor
const title = document.createElement('h3');
title.className = 'insertr-editor-title';
title.textContent = 'Edit Image';
title.style.cssText = `
margin: 0 0 ${getComputedStyle(document.documentElement).getPropertyValue('--insertr-spacing-md') || '16px'} 0;
font-size: 18px;
font-weight: 600;
color: var(--insertr-text-primary);
`;
form.appendChild(title);
// Source field
const srcGroup = document.createElement('div');
srcGroup.className = 'insertr-form-group';
srcGroup.innerHTML = `
<label class="insertr-form-label">Image URL</label>
<input type="url" class="insertr-form-input" id="image-src" value="${this.element.src || ''}" placeholder="https://example.com/image.jpg">
<input type="url" class="insertr-form-input" id="image-src" value="${this.escapeHtml(this.element.src || '')}" placeholder="https://example.com/image.jpg" required>
<div class="insertr-form-message insertr-info">Enter the full URL to the image file</div>
`;
// Alt text field
@@ -254,14 +462,35 @@ export class StyleAwareEditor {
altGroup.className = 'insertr-form-group';
altGroup.innerHTML = `
<label class="insertr-form-label">Alt Text</label>
<input type="text" class="insertr-form-input" id="image-alt" value="${this.element.alt || ''}" placeholder="Image description">
<input type="text" class="insertr-form-input" id="image-alt" value="${this.escapeHtml(this.element.alt || '')}" placeholder="Describe this image for accessibility" required>
<div class="insertr-form-message insertr-info">Describe the image for screen readers and accessibility</div>
`;
form.appendChild(srcGroup);
form.appendChild(altGroup);
// Add image preview if src exists
if (this.element.src) {
const previewGroup = document.createElement('div');
previewGroup.className = 'insertr-form-group';
previewGroup.innerHTML = `
<label class="insertr-form-label">Current Image</label>
<img src="${this.escapeHtml(this.element.src)}" alt="${this.escapeHtml(this.element.alt || '')}" style="max-width: 200px; max-height: 150px; border: 1px solid var(--insertr-border-color); border-radius: var(--insertr-border-radius);">
`;
form.appendChild(previewGroup);
}
this.contentEditor = form;
this.editorContainer.appendChild(form);
// Focus the first input
setTimeout(() => {
const srcInput = form.querySelector('#image-src');
if (srcInput) {
srcInput.focus();
srcInput.select();
}
}, 100);
}
/**
@@ -280,12 +509,6 @@ export class StyleAwareEditor {
this.editorContainer.appendChild(editor);
}
/**
* Create formatting toolbar with detected style buttons and link button
*
@@ -345,9 +568,26 @@ export class StyleAwareEditor {
previewContent.className = 'insertr-preview-content';
previewContent.textContent = styleInfo.name;
// Apply the original classes to the preview content (not the button)
if (styleInfo.element && styleInfo.classes && styleInfo.classes.length > 0) {
// Add special preview button class to the button
// Apply styling based on whether this is a detected style or default
if (styleInfo.isDefault) {
// Default style - apply semantic styling
button.classList.add('insertr-default-style');
previewContent.classList.add('insertr-default-preview');
// Add semantic styling for default elements
if (styleInfo.tagName === 'strong') {
previewContent.style.fontWeight = 'bold';
} else if (styleInfo.tagName === 'em') {
previewContent.style.fontStyle = 'italic';
} else if (styleInfo.tagName === 'a') {
previewContent.style.textDecoration = 'underline';
previewContent.style.color = '#0066cc';
}
// Add helpful description to title
button.title = `${styleInfo.name}: ${styleInfo.description || 'Default formatting option'}`;
} else if (styleInfo.element && styleInfo.classes && styleInfo.classes.length > 0) {
// Detected style - apply original classes to preview
button.classList.add('insertr-style-preview');
// Add the detected classes to the preview content
@@ -379,9 +619,19 @@ export class StyleAwareEditor {
createLinkButton() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'insertr-style-btn';
button.textContent = '🔗 Link';
button.title = 'Add/Edit Link';
button.className = 'insertr-style-btn insertr-default-style';
button.title = 'Add/Edit Link: Create hyperlinks in your content';
// Create preview content container to match other buttons
const previewContent = document.createElement('span');
previewContent.className = 'insertr-preview-content insertr-default-preview';
previewContent.textContent = 'Link';
// Apply link styling for visual consistency
previewContent.style.textDecoration = 'underline';
previewContent.style.color = '#0066cc';
button.appendChild(previewContent);
// Add click handler for link configuration
button.addEventListener('click', (e) => {
@@ -529,7 +779,6 @@ export class StyleAwareEditor {
/**
* Extract current content from editor
* HTML-first approach: always return HTML content
*
* @returns {Object} - Extracted content
*/
@@ -556,17 +805,10 @@ export class StyleAwareEditor {
extractDirectEditorContent() {
const tagName = this.element.tagName.toLowerCase();
if (tagName === 'a') {
const text = document.getElementById('link-text').value;
const url = document.getElementById('link-url').value;
const target = document.getElementById('link-target').value;
// Note: Link editing is now handled directly in createLinkConfigurationForm
// This method only handles button and image elements
return {
type: 'html',
content: text,
properties: { href: url, target: target }
};
} else if (tagName === 'button') {
if (tagName === 'button') {
const text = document.getElementById('button-text').value;
return {
@@ -691,112 +933,48 @@ export class StyleAwareEditor {
// Create popup container
const popup = document.createElement('div');
popup.className = 'insertr-modal-container';
popup.style.maxWidth = '400px';
popup.style.maxWidth = '600px'; // Wider to accommodate polished form
// Create form
const form = document.createElement('div');
form.className = 'insertr-edit-form';
// Header
const header = document.createElement('div');
header.className = 'insertr-form-header';
header.innerHTML = `
<h3 class="insertr-form-title">${currentUrl ? 'Edit' : 'Add'} Link</h3>
<p class="insertr-form-help">Configure link for: "${text}"</p>
`;
// Form body
const body = document.createElement('div');
body.className = 'insertr-form-body';
// URL input
const urlGroup = document.createElement('div');
urlGroup.className = 'insertr-form-group';
urlGroup.innerHTML = `
<label class="insertr-form-label">URL</label>
<input type="url" class="insertr-form-input" id="link-url" value="${currentUrl}" placeholder="https://example.com" required>
`;
// Target input
const targetGroup = document.createElement('div');
targetGroup.className = 'insertr-form-group';
targetGroup.innerHTML = `
<label class="insertr-form-label">Target</label>
<select class="insertr-form-select" id="link-target">
<option value="">Same window</option>
<option value="_blank" ${currentTarget === '_blank' ? 'selected' : ''}>New window</option>
</select>
`;
body.appendChild(urlGroup);
body.appendChild(targetGroup);
// Actions
const actions = document.createElement('div');
actions.className = 'insertr-form-actions';
const saveBtn = document.createElement('button');
saveBtn.className = 'insertr-btn-save';
saveBtn.textContent = 'Save Link';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'insertr-btn-cancel';
cancelBtn.textContent = 'Cancel';
// Remove link button (if editing existing link)
if (currentUrl) {
const removeBtn = document.createElement('button');
removeBtn.className = 'insertr-btn-cancel';
removeBtn.textContent = 'Remove Link';
removeBtn.style.marginRight = 'auto';
actions.appendChild(removeBtn);
removeBtn.addEventListener('click', () => {
onSave('', ''); // Empty URL removes the link
// Create unified form using shared component
const form = this.createLinkConfigurationForm({
text: text,
url: currentUrl,
target: currentTarget,
mode: 'popup',
showTextField: false, // Don't show text field for popup (text is already selected)
onSave: (linkData) => {
onSave(linkData.url, linkData.target);
document.body.removeChild(overlay);
},
onCancel: () => {
document.body.removeChild(overlay);
},
onRemove: currentUrl ? () => {
// Call onSave with empty string to indicate removal
onSave('', '');
document.body.removeChild(overlay);
} : null
});
}
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
// Assemble popup
form.appendChild(header);
form.appendChild(body);
form.appendChild(actions);
popup.appendChild(form);
overlay.appendChild(popup);
document.body.appendChild(overlay);
// Event handlers
saveBtn.addEventListener('click', () => {
const url = document.getElementById('link-url').value.trim();
const target = document.getElementById('link-target').value;
if (url) {
onSave(url, target);
document.body.removeChild(overlay);
} else {
alert('Please enter a valid URL');
}
});
cancelBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
});
// Show popup
document.body.appendChild(overlay);
// Focus URL input
setTimeout(() => {
document.getElementById('link-url').focus();
}, 100);
// Handle escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
document.body.removeChild(overlay);
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
/**
@@ -854,6 +1032,82 @@ export class StyleAwareEditor {
this.onChange();
}
/**
* Add validation to link editor
*/
addLinkValidation(form) {
const textInput = form.querySelector('#link-text');
const urlInput = form.querySelector('#link-url');
const textMessage = form.querySelector('#link-text-message');
const urlMessage = form.querySelector('#link-url-message');
// URL validation
urlInput.addEventListener('input', () => {
const url = urlInput.value.trim();
if (!url) {
this.setValidationState(urlInput, urlMessage, '', 'info');
return;
}
try {
new URL(url);
this.setValidationState(urlInput, urlMessage, '✓ Valid URL', 'success');
} catch (e) {
if (url.includes('.') && !url.startsWith('http')) {
// Suggest adding protocol
this.setValidationState(urlInput, urlMessage, 'Try adding https:// to the beginning', 'info');
} else {
this.setValidationState(urlInput, urlMessage, 'Please enter a valid URL', 'error');
}
}
});
// Text validation
textInput.addEventListener('input', () => {
const text = textInput.value.trim();
if (!text) {
this.setValidationState(textInput, textMessage, 'Link text is required', 'error');
} else {
this.setValidationState(textInput, textMessage, '', 'info');
}
});
}
/**
* Set validation state for form elements
*/
setValidationState(input, messageElement, message, type) {
// Remove previous states
input.classList.remove('insertr-error', 'insertr-success');
messageElement.classList.remove('insertr-error', 'insertr-success', 'insertr-info');
// Add new state
if (type === 'error') {
input.classList.add('insertr-error');
messageElement.classList.add('insertr-error');
} else if (type === 'success') {
input.classList.add('insertr-success');
messageElement.classList.add('insertr-success');
} else {
messageElement.classList.add('insertr-info');
}
// Set message
messageElement.textContent = message;
messageElement.style.display = message ? 'block' : 'none';
}
/**
* HTML escape utility
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Destroy the editor and clean up
*/

View File

@@ -1,9 +1,6 @@
/**
* StyleDetectionEngine - Analyzes elements for nested styled children with position preservation
*
* Implements the "one layer deep" analysis described in CLASSES.md line 27:
* "Only direct child elements are analyzed and preserved"
*
* Purpose: Extract styled nested elements as formatting options AND preserve their positions
*/
@@ -21,6 +18,7 @@ export class StyleDetectionEngine {
/**
* Analyze element for nested styled elements AND their positions (CLASSES.md line 26-29)
* Returns both detected styles and structured content that preserves positions
* Enhanced with smart default formatting options when not present
*
* @param {HTMLElement} element - The .insertr element to analyze
* @returns {Object} - {styles: Map, structure: Array}
@@ -32,23 +30,15 @@ export class StyleDetectionEngine {
// Parse the element's content while preserving structure
this.parseContentStructure(element, styleMap, contentStructure);
// Add smart default formatting options if not already present
this.addSmartDefaults(styleMap, element);
return {
styles: styleMap,
structure: contentStructure
};
}
/**
* Legacy method for backward compatibility - only returns styles
*
* @param {HTMLElement} element - The .insertr element to analyze
* @returns {Map} - Map of styleId -> styleInfo objects
*/
detectStyles(element) {
const result = this.detectStylesAndStructure(element);
return result.styles;
}
/**
* Parse content structure while collecting style information
* Creates a structure array that preserves text and styled element positions
@@ -594,6 +584,84 @@ export class StyleDetectionEngine {
return html;
}
/**
* Add smart default formatting options when not already present
* Provides essential formatting (bold, italic, link) if developer hasn't defined them
*
* @param {Map} styleMap - Existing detected styles
* @param {HTMLElement} element - The element being analyzed
*/
addSmartDefaults(styleMap, element) {
// Only add defaults for content elements that benefit from text formatting
const contentTags = new Set(['p', 'div', 'section', 'article', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'td', 'th', 'blockquote']);
if (!contentTags.has(element.tagName.toLowerCase())) {
return; // Skip for non-content elements
}
// Check what's already available
const hasStrong = this.hasStyleTag(styleMap, 'strong');
const hasEm = this.hasStyleTag(styleMap, 'em');
const hasB = this.hasStyleTag(styleMap, 'b');
const hasI = this.hasStyleTag(styleMap, 'i');
const hasA = this.hasStyleTag(styleMap, 'a');
// Add Bold if not present (prefer <strong> for semantics)
if (!hasStrong && !hasB) {
styleMap.set('default-strong', {
name: 'Bold',
tagName: 'strong',
classes: [],
attributes: {},
element: null, // Virtual element - no DOM reference
isDefault: true,
description: 'Make text bold for emphasis'
});
}
// Add Italic if not present (prefer <em> for semantics)
if (!hasEm && !hasI) {
styleMap.set('default-em', {
name: 'Italic',
tagName: 'em',
classes: [],
attributes: {},
element: null,
isDefault: true,
description: 'Make text italic for emphasis'
});
}
// Add Link if not present
if (!hasA) {
styleMap.set('default-a', {
name: 'Link',
tagName: 'a',
classes: [],
attributes: { href: '' },
element: null,
isDefault: true,
description: 'Create a hyperlink'
});
}
}
/**
* Check if styleMap already contains a specific tag type
*
* @param {Map} styleMap - Style map to check
* @param {string} tagName - Tag name to look for
* @returns {boolean} - True if tag is already present
*/
hasStyleTag(styleMap, tagName) {
for (const [, styleInfo] of styleMap) {
if (styleInfo.tagName === tagName) {
return true;
}
}
return false;
}
/**
* Extract plain text from structure while preserving order
*