refactor: Replace multi-property forms with popup-based link configuration
- Remove clunky multi-property editor with separate text + URL inputs
- Implement single rich text editor (contentEditable) for all content
- Add popup-based link configuration: select text → click 🔗 Link → configure
- Filter out link styles from formatting toolbar (links use popup, not buttons)
- Consolidate CSS: remove separate style-aware-editor.css, integrate into insertr.css
- Clean up 200+ lines of unused multi-property form code and styles
- Fix duplicate link style detection (no more 'Fancy Link' + 'Link' buttons)
Result: Much cleaner UX similar to modern editors where formatting uses
toolbar buttons and complex elements (links) use dedicated popups.
This commit is contained in:
@@ -104,12 +104,9 @@ export class StyleAwareEditor {
|
||||
}
|
||||
|
||||
const hasStyledElements = detection.structure.some(piece => piece.type === 'styled');
|
||||
const hasMultiProperty = this.hasMultiPropertyElements(detection.structure);
|
||||
|
||||
if (hasMultiProperty) {
|
||||
return 'multi-property'; // Complex elements with multiple editable properties
|
||||
} else if (hasStyledElements) {
|
||||
return 'rich'; // Rich text with styling
|
||||
if (hasStyledElements) {
|
||||
return 'rich'; // Rich text with styling (handles all styled content including links)
|
||||
} else {
|
||||
return 'simple'; // Plain text
|
||||
}
|
||||
@@ -147,14 +144,11 @@ export class StyleAwareEditor {
|
||||
case 'rich':
|
||||
this.createRichEditor(analysis);
|
||||
break;
|
||||
case 'multi-property':
|
||||
this.createMultiPropertyEditor(analysis);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add toolbar if enabled and we have detected styles
|
||||
if (this.options.showToolbar && analysis.styles.size > 0) {
|
||||
this.createStyleToolbar(analysis.styles);
|
||||
// Add toolbar if enabled and we have any styled content
|
||||
if (this.options.showToolbar && (analysis.styles.size > 0 || analysis.hasMultiPropertyElements)) {
|
||||
this.createStyleToolbar(analysis.styles, analysis.structure);
|
||||
}
|
||||
|
||||
// Add form actions
|
||||
@@ -191,173 +185,19 @@ export class StyleAwareEditor {
|
||||
this.editorContainer.appendChild(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multi-property editor for complex elements
|
||||
*
|
||||
* @param {Object} analysis - Analysis results
|
||||
*/
|
||||
createMultiPropertyEditor(analysis) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'insertr-multi-property-editor';
|
||||
|
||||
// Create fields for each structure piece
|
||||
analysis.structure.forEach((piece, index) => {
|
||||
if (piece.type === 'text') {
|
||||
// Simple text input
|
||||
const input = this.createTextInput(piece.content, `Text ${index + 1}`);
|
||||
input.dataset.structureIndex = index;
|
||||
container.appendChild(input);
|
||||
} else if (piece.type === 'styled') {
|
||||
// Multi-property form for styled element
|
||||
const form = this.createStyledElementForm(piece, index);
|
||||
container.appendChild(form);
|
||||
}
|
||||
});
|
||||
|
||||
this.contentEditor = container;
|
||||
this.editorContainer.appendChild(container);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create form for editing styled element with multiple properties
|
||||
*
|
||||
* @param {Object} piece - Structure piece for styled element
|
||||
* @param {number} index - Index in structure array
|
||||
* @returns {HTMLElement} - Form element
|
||||
*/
|
||||
createStyledElementForm(piece, index) {
|
||||
const form = document.createElement('div');
|
||||
form.className = 'insertr-styled-element-form';
|
||||
form.dataset.structureIndex = index;
|
||||
form.dataset.styleId = piece.styleId;
|
||||
|
||||
const style = this.detectedStyles.get(piece.styleId);
|
||||
if (!style) {
|
||||
return form;
|
||||
}
|
||||
|
||||
// Form header
|
||||
const header = document.createElement('h4');
|
||||
header.textContent = style.name;
|
||||
header.className = 'insertr-form-header';
|
||||
form.appendChild(header);
|
||||
|
||||
// Create input for each editable property
|
||||
Object.entries(piece.properties).forEach(([property, value]) => {
|
||||
const input = this.createPropertyInput(property, value, style.tagName);
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create input field for a specific property
|
||||
*
|
||||
* @param {string} property - Property name (content, href, src, etc.)
|
||||
* @param {string} value - Current property value
|
||||
* @param {string} tagName - Element tag name for context
|
||||
* @returns {HTMLElement} - Input field container
|
||||
*/
|
||||
createPropertyInput(property, value, tagName) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'insertr-property-input';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.textContent = this.getPropertyLabel(property, tagName);
|
||||
label.className = 'insertr-property-label';
|
||||
|
||||
let input;
|
||||
|
||||
switch (property) {
|
||||
case 'content':
|
||||
input = document.createElement('textarea');
|
||||
input.rows = 2;
|
||||
break;
|
||||
case 'href':
|
||||
input = document.createElement('input');
|
||||
input.type = 'url';
|
||||
input.placeholder = 'https://example.com';
|
||||
break;
|
||||
case 'src':
|
||||
input = document.createElement('input');
|
||||
input.type = 'url';
|
||||
input.placeholder = 'https://example.com/image.jpg';
|
||||
break;
|
||||
case 'alt':
|
||||
case 'title':
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
break;
|
||||
}
|
||||
|
||||
input.className = 'insertr-property-field';
|
||||
input.name = property;
|
||||
input.value = value || '';
|
||||
|
||||
container.appendChild(label);
|
||||
container.appendChild(input);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable label for property
|
||||
*
|
||||
* @param {string} property - Property name
|
||||
* @param {string} tagName - Element tag name
|
||||
* @returns {string} - Human-readable label
|
||||
*/
|
||||
getPropertyLabel(property, tagName) {
|
||||
const labels = {
|
||||
content: tagName === 'img' ? 'Description' : 'Text',
|
||||
href: 'URL',
|
||||
src: 'Image URL',
|
||||
alt: 'Alt Text',
|
||||
title: 'Title',
|
||||
target: 'Target',
|
||||
placeholder: 'Placeholder'
|
||||
};
|
||||
|
||||
return labels[property] || property.charAt(0).toUpperCase() + property.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create simple text input
|
||||
*
|
||||
* @param {string} content - Current content
|
||||
* @param {string} label - Input label
|
||||
* @returns {HTMLElement} - Input container
|
||||
*/
|
||||
createTextInput(content, label) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'insertr-text-input';
|
||||
|
||||
const labelEl = document.createElement('label');
|
||||
labelEl.textContent = label;
|
||||
labelEl.className = 'insertr-input-label';
|
||||
|
||||
const input = document.createElement('textarea');
|
||||
input.className = 'insertr-text-field';
|
||||
input.value = content;
|
||||
input.rows = 2;
|
||||
|
||||
container.appendChild(labelEl);
|
||||
container.appendChild(input);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create formatting toolbar with detected style buttons
|
||||
* Create formatting toolbar with detected style buttons and link button
|
||||
*
|
||||
* @param {Map} styles - Detected styles
|
||||
* @param {Array} structure - Content structure (to detect links)
|
||||
*/
|
||||
createStyleToolbar(styles) {
|
||||
createStyleToolbar(styles, structure = []) {
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'insertr-style-toolbar';
|
||||
|
||||
@@ -366,12 +206,27 @@ export class StyleAwareEditor {
|
||||
title.className = 'insertr-toolbar-title';
|
||||
toolbar.appendChild(title);
|
||||
|
||||
// Add button for each detected style
|
||||
// Add button for each detected style (except links - those use the link popup)
|
||||
for (const [styleId, styleInfo] of styles) {
|
||||
// Skip link styles - they should be handled by the link popup, not toolbar buttons
|
||||
if (styleInfo.tagName.toLowerCase() === 'a') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const button = this.createStyleButton(styleId, styleInfo);
|
||||
toolbar.appendChild(button);
|
||||
}
|
||||
|
||||
// Add link button if we have links in content or always for rich editor
|
||||
const hasLinks = structure.some(piece =>
|
||||
piece.type === 'styled' && piece.element && piece.element.tagName.toLowerCase() === 'a'
|
||||
);
|
||||
|
||||
if (hasLinks || styles.size > 0) {
|
||||
const linkButton = this.createLinkButton();
|
||||
toolbar.appendChild(linkButton);
|
||||
}
|
||||
|
||||
this.toolbar = toolbar;
|
||||
this.editorContainer.insertBefore(toolbar, this.contentEditor);
|
||||
}
|
||||
@@ -400,6 +255,27 @@ export class StyleAwareEditor {
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create link button for opening link configuration popup
|
||||
*
|
||||
* @returns {HTMLElement} - Link button
|
||||
*/
|
||||
createLinkButton() {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'insertr-style-btn';
|
||||
button.textContent = '🔗 Link';
|
||||
button.title = 'Add/Edit Link';
|
||||
|
||||
// Add click handler for link configuration
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.openLinkPopup();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form action buttons (Save, Cancel)
|
||||
*/
|
||||
@@ -553,63 +429,12 @@ export class StyleAwareEditor {
|
||||
type: 'html',
|
||||
content: this.contentEditor.innerHTML
|
||||
};
|
||||
} else if (this.contentEditor.className.includes('multi-property-editor')) {
|
||||
// Multi-property editor
|
||||
return this.extractMultiPropertyContent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content from multi-property editor
|
||||
*
|
||||
* @returns {Object} - Structured content with updated properties
|
||||
*/
|
||||
extractMultiPropertyContent() {
|
||||
const updatedStructure = [...this.contentStructure];
|
||||
const updatedProperties = {};
|
||||
|
||||
// Extract text inputs
|
||||
const textInputs = this.contentEditor.querySelectorAll('.insertr-text-input textarea');
|
||||
textInputs.forEach(input => {
|
||||
const index = parseInt(input.closest('.insertr-text-input').dataset.structureIndex);
|
||||
if (!isNaN(index)) {
|
||||
updatedStructure[index] = {
|
||||
...updatedStructure[index],
|
||||
content: input.value
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Extract styled element forms
|
||||
const styledForms = this.contentEditor.querySelectorAll('.insertr-styled-element-form');
|
||||
styledForms.forEach(form => {
|
||||
const index = parseInt(form.dataset.structureIndex);
|
||||
const styleId = form.dataset.styleId;
|
||||
|
||||
if (!isNaN(index)) {
|
||||
const properties = {};
|
||||
const propertyFields = form.querySelectorAll('.insertr-property-field');
|
||||
|
||||
propertyFields.forEach(field => {
|
||||
properties[field.name] = field.value;
|
||||
});
|
||||
|
||||
updatedProperties[index] = { properties };
|
||||
updatedStructure[index] = {
|
||||
...updatedStructure[index],
|
||||
properties
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'structured',
|
||||
structure: updatedStructure,
|
||||
updatedProperties: updatedProperties
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply extracted content to the original element
|
||||
@@ -657,6 +482,230 @@ export class StyleAwareEditor {
|
||||
return this.editorContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open link configuration popup for selected text
|
||||
*/
|
||||
openLinkPopup() {
|
||||
// Get current selection
|
||||
const selection = window.getSelection();
|
||||
|
||||
// Check if we have a valid selection in our editor
|
||||
if (!selection.rangeCount || !this.contentEditor.contains(selection.anchorNode)) {
|
||||
alert('Please select some text to create a link');
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const selectedText = range.toString().trim();
|
||||
|
||||
if (!selectedText) {
|
||||
alert('Please select some text to create a link');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if selection is inside an existing link
|
||||
let existingLink = null;
|
||||
let currentNode = range.commonAncestorContainer;
|
||||
|
||||
// Walk up the DOM to find if we're inside a link
|
||||
while (currentNode && currentNode !== this.contentEditor) {
|
||||
if (currentNode.nodeType === Node.ELEMENT_NODE && currentNode.tagName.toLowerCase() === 'a') {
|
||||
existingLink = currentNode;
|
||||
break;
|
||||
}
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
|
||||
// Get existing URL if editing a link
|
||||
const currentUrl = existingLink ? existingLink.href : '';
|
||||
const currentTarget = existingLink ? existingLink.target : '';
|
||||
|
||||
// Show popup
|
||||
this.showLinkConfigPopup(selectedText, currentUrl, currentTarget, (url, target) => {
|
||||
this.applyLinkToSelection(range, selectedText, url, target, existingLink);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show link configuration popup
|
||||
*
|
||||
* @param {string} text - Selected text
|
||||
* @param {string} currentUrl - Current URL (if editing)
|
||||
* @param {string} currentTarget - Current target (if editing)
|
||||
* @param {Function} onSave - Callback when user saves
|
||||
*/
|
||||
showLinkConfigPopup(text, currentUrl, currentTarget, onSave) {
|
||||
// Create popup overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'insertr-modal-overlay';
|
||||
overlay.style.zIndex = '999999';
|
||||
|
||||
// Create popup container
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'insertr-modal-container';
|
||||
popup.style.maxWidth = '400px';
|
||||
|
||||
// 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
|
||||
document.body.removeChild(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
actions.appendChild(cancelBtn);
|
||||
actions.appendChild(saveBtn);
|
||||
|
||||
// Assemble popup
|
||||
form.appendChild(header);
|
||||
form.appendChild(body);
|
||||
form.appendChild(actions);
|
||||
popup.appendChild(form);
|
||||
overlay.appendChild(popup);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply link to the current selection
|
||||
*
|
||||
* @param {Range} range - Selection range
|
||||
* @param {string} text - Selected text
|
||||
* @param {string} url - Link URL
|
||||
* @param {string} target - Link target
|
||||
* @param {HTMLElement} existingLink - Existing link element (if editing)
|
||||
*/
|
||||
applyLinkToSelection(range, text, url, target, existingLink) {
|
||||
if (!url) {
|
||||
// Remove link if no URL provided
|
||||
if (existingLink) {
|
||||
const parent = existingLink.parentNode;
|
||||
while (existingLink.firstChild) {
|
||||
parent.insertBefore(existingLink.firstChild, existingLink);
|
||||
}
|
||||
parent.removeChild(existingLink);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingLink) {
|
||||
// Update existing link
|
||||
existingLink.href = url;
|
||||
if (target) {
|
||||
existingLink.target = target;
|
||||
} else {
|
||||
existingLink.removeAttribute('target');
|
||||
}
|
||||
} else {
|
||||
// Create new link
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
if (target) {
|
||||
link.target = target;
|
||||
}
|
||||
|
||||
try {
|
||||
range.surroundContents(link);
|
||||
} catch (e) {
|
||||
// Fallback for complex selections
|
||||
link.textContent = text;
|
||||
range.deleteContents();
|
||||
range.insertNode(link);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
window.getSelection().removeAllRanges();
|
||||
|
||||
// Trigger change event
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the editor and clean up
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user