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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user