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);
|
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 */
|
/* Editor components */
|
||||||
.insertr-simple-editor,
|
.insertr-simple-editor,
|
||||||
.insertr-rich-editor,
|
.insertr-rich-editor,
|
||||||
@@ -479,7 +519,126 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
|||||||
overflow-y: auto;
|
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 */
|
/* Form actions */
|
||||||
.insertr-form-actions {
|
.insertr-form-actions {
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export class StyleAwareEditor {
|
|||||||
this.originalContent = null;
|
this.originalContent = null;
|
||||||
|
|
||||||
// Event callbacks
|
// Event callbacks
|
||||||
this.onSave = options.onSave || (() => {});
|
this.onSave = options.onSave || (() => { });
|
||||||
this.onCancel = options.onCancel || (() => {});
|
this.onCancel = options.onCancel || (() => { });
|
||||||
this.onChange = options.onChange || (() => {});
|
this.onChange = options.onChange || (() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,7 +144,8 @@ export class StyleAwareEditor {
|
|||||||
break;
|
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) {
|
if (this.options.showToolbar && analysis.strategy === 'rich' && analysis.styles.size > 0) {
|
||||||
this.createStyleToolbar(analysis.styles, analysis.structure);
|
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');
|
const form = document.createElement('div');
|
||||||
form.className = 'insertr-direct-editor insertr-link-editor';
|
form.className = 'insertr-direct-editor insertr-link-editor';
|
||||||
|
|
||||||
// Text field
|
// Create title
|
||||||
const textGroup = document.createElement('div');
|
const title = document.createElement('h3');
|
||||||
textGroup.className = 'insertr-form-group';
|
title.className = 'insertr-editor-title';
|
||||||
textGroup.innerHTML = `
|
title.textContent = url ? 'Edit Link' : 'Add Link';
|
||||||
<label class="insertr-form-label">Link Text</label>
|
title.style.cssText = `
|
||||||
<input type="text" class="insertr-form-input" id="link-text" value="${this.element.textContent}" placeholder="Link text">
|
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);
|
||||||
|
|
||||||
// URL field
|
// 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.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 with validation
|
||||||
const urlGroup = document.createElement('div');
|
const urlGroup = document.createElement('div');
|
||||||
urlGroup.className = 'insertr-form-group';
|
urlGroup.className = 'insertr-form-group';
|
||||||
urlGroup.innerHTML = `
|
urlGroup.innerHTML = `
|
||||||
<label class="insertr-form-label">URL</label>
|
<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
|
// Target field
|
||||||
const targetGroup = document.createElement('div');
|
const targetGroup = document.createElement('div');
|
||||||
targetGroup.className = 'insertr-form-group';
|
targetGroup.className = 'insertr-form-group';
|
||||||
targetGroup.innerHTML = `
|
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">
|
<select class="insertr-form-select" id="link-target">
|
||||||
<option value="">Same window</option>
|
<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>
|
</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.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.contentEditor = form;
|
||||||
this.editorContainer.appendChild(form);
|
this.editorContainer.appendChild(form);
|
||||||
}
|
}
|
||||||
@@ -220,18 +393,40 @@ export class StyleAwareEditor {
|
|||||||
const form = document.createElement('div');
|
const form = document.createElement('div');
|
||||||
form.className = 'insertr-direct-editor insertr-button-editor';
|
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
|
// Text field
|
||||||
const textGroup = document.createElement('div');
|
const textGroup = document.createElement('div');
|
||||||
textGroup.className = 'insertr-form-group';
|
textGroup.className = 'insertr-form-group';
|
||||||
textGroup.innerHTML = `
|
textGroup.innerHTML = `
|
||||||
<label class="insertr-form-label">Button Text</label>
|
<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);
|
form.appendChild(textGroup);
|
||||||
|
|
||||||
this.contentEditor = form;
|
this.contentEditor = form;
|
||||||
this.editorContainer.appendChild(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');
|
const form = document.createElement('div');
|
||||||
form.className = 'insertr-direct-editor insertr-image-editor';
|
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
|
// Source field
|
||||||
const srcGroup = document.createElement('div');
|
const srcGroup = document.createElement('div');
|
||||||
srcGroup.className = 'insertr-form-group';
|
srcGroup.className = 'insertr-form-group';
|
||||||
srcGroup.innerHTML = `
|
srcGroup.innerHTML = `
|
||||||
<label class="insertr-form-label">Image URL</label>
|
<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
|
// Alt text field
|
||||||
@@ -254,14 +462,35 @@ export class StyleAwareEditor {
|
|||||||
altGroup.className = 'insertr-form-group';
|
altGroup.className = 'insertr-form-group';
|
||||||
altGroup.innerHTML = `
|
altGroup.innerHTML = `
|
||||||
<label class="insertr-form-label">Alt Text</label>
|
<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(srcGroup);
|
||||||
form.appendChild(altGroup);
|
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.contentEditor = form;
|
||||||
this.editorContainer.appendChild(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);
|
this.editorContainer.appendChild(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create formatting toolbar with detected style buttons and link button
|
* Create formatting toolbar with detected style buttons and link button
|
||||||
*
|
*
|
||||||
@@ -345,9 +568,26 @@ export class StyleAwareEditor {
|
|||||||
previewContent.className = 'insertr-preview-content';
|
previewContent.className = 'insertr-preview-content';
|
||||||
previewContent.textContent = styleInfo.name;
|
previewContent.textContent = styleInfo.name;
|
||||||
|
|
||||||
// Apply the original classes to the preview content (not the button)
|
// Apply styling based on whether this is a detected style or default
|
||||||
if (styleInfo.element && styleInfo.classes && styleInfo.classes.length > 0) {
|
if (styleInfo.isDefault) {
|
||||||
// Add special preview button class to the button
|
// 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');
|
button.classList.add('insertr-style-preview');
|
||||||
|
|
||||||
// Add the detected classes to the preview content
|
// Add the detected classes to the preview content
|
||||||
@@ -379,9 +619,19 @@ export class StyleAwareEditor {
|
|||||||
createLinkButton() {
|
createLinkButton() {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.className = 'insertr-style-btn';
|
button.className = 'insertr-style-btn insertr-default-style';
|
||||||
button.textContent = '🔗 Link';
|
button.title = 'Add/Edit Link: Create hyperlinks in your content';
|
||||||
button.title = 'Add/Edit Link';
|
|
||||||
|
// 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
|
// Add click handler for link configuration
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', (e) => {
|
||||||
@@ -529,7 +779,6 @@ export class StyleAwareEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract current content from editor
|
* Extract current content from editor
|
||||||
* HTML-first approach: always return HTML content
|
|
||||||
*
|
*
|
||||||
* @returns {Object} - Extracted content
|
* @returns {Object} - Extracted content
|
||||||
*/
|
*/
|
||||||
@@ -556,17 +805,10 @@ export class StyleAwareEditor {
|
|||||||
extractDirectEditorContent() {
|
extractDirectEditorContent() {
|
||||||
const tagName = this.element.tagName.toLowerCase();
|
const tagName = this.element.tagName.toLowerCase();
|
||||||
|
|
||||||
if (tagName === 'a') {
|
// Note: Link editing is now handled directly in createLinkConfigurationForm
|
||||||
const text = document.getElementById('link-text').value;
|
// This method only handles button and image elements
|
||||||
const url = document.getElementById('link-url').value;
|
|
||||||
const target = document.getElementById('link-target').value;
|
|
||||||
|
|
||||||
return {
|
if (tagName === 'button') {
|
||||||
type: 'html',
|
|
||||||
content: text,
|
|
||||||
properties: { href: url, target: target }
|
|
||||||
};
|
|
||||||
} else if (tagName === 'button') {
|
|
||||||
const text = document.getElementById('button-text').value;
|
const text = document.getElementById('button-text').value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -691,112 +933,48 @@ export class StyleAwareEditor {
|
|||||||
// Create popup container
|
// Create popup container
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.className = 'insertr-modal-container';
|
popup.className = 'insertr-modal-container';
|
||||||
popup.style.maxWidth = '400px';
|
popup.style.maxWidth = '600px'; // Wider to accommodate polished form
|
||||||
|
|
||||||
// Create form
|
// Create unified form using shared component
|
||||||
const form = document.createElement('div');
|
const form = this.createLinkConfigurationForm({
|
||||||
form.className = 'insertr-edit-form';
|
text: text,
|
||||||
|
url: currentUrl,
|
||||||
// Header
|
target: currentTarget,
|
||||||
const header = document.createElement('div');
|
mode: 'popup',
|
||||||
header.className = 'insertr-form-header';
|
showTextField: false, // Don't show text field for popup (text is already selected)
|
||||||
header.innerHTML = `
|
onSave: (linkData) => {
|
||||||
<h3 class="insertr-form-title">${currentUrl ? 'Edit' : 'Add'} Link</h3>
|
onSave(linkData.url, linkData.target);
|
||||||
<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);
|
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);
|
popup.appendChild(form);
|
||||||
overlay.appendChild(popup);
|
overlay.appendChild(popup);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
// Event handlers
|
// Close on overlay click
|
||||||
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) => {
|
overlay.addEventListener('click', (e) => {
|
||||||
if (e.target === overlay) {
|
if (e.target === overlay) {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show popup
|
// Handle escape key
|
||||||
document.body.appendChild(overlay);
|
const handleEscape = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
// Focus URL input
|
document.body.removeChild(overlay);
|
||||||
setTimeout(() => {
|
document.removeEventListener('keydown', handleEscape);
|
||||||
document.getElementById('link-url').focus();
|
}
|
||||||
}, 100);
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -854,6 +1032,82 @@ export class StyleAwareEditor {
|
|||||||
this.onChange();
|
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
|
* Destroy the editor and clean up
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* StyleDetectionEngine - Analyzes elements for nested styled children with position preservation
|
* 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
|
* 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)
|
* Analyze element for nested styled elements AND their positions (CLASSES.md line 26-29)
|
||||||
* Returns both detected styles and structured content that preserves positions
|
* 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
|
* @param {HTMLElement} element - The .insertr element to analyze
|
||||||
* @returns {Object} - {styles: Map, structure: Array}
|
* @returns {Object} - {styles: Map, structure: Array}
|
||||||
@@ -32,23 +30,15 @@ export class StyleDetectionEngine {
|
|||||||
// Parse the element's content while preserving structure
|
// Parse the element's content while preserving structure
|
||||||
this.parseContentStructure(element, styleMap, contentStructure);
|
this.parseContentStructure(element, styleMap, contentStructure);
|
||||||
|
|
||||||
|
// Add smart default formatting options if not already present
|
||||||
|
this.addSmartDefaults(styleMap, element);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
styles: styleMap,
|
styles: styleMap,
|
||||||
structure: contentStructure
|
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
|
* Parse content structure while collecting style information
|
||||||
* Creates a structure array that preserves text and styled element positions
|
* Creates a structure array that preserves text and styled element positions
|
||||||
@@ -594,6 +584,84 @@ export class StyleDetectionEngine {
|
|||||||
return html;
|
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
|
* Extract plain text from structure while preserving order
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user