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 {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -71,14 +61,14 @@ export class StyleDetectionEngine {
|
|||||||
} else if (node.nodeType === (typeof Node !== 'undefined' ? Node.ELEMENT_NODE : NODE_TYPES.ELEMENT_NODE)) {
|
} else if (node.nodeType === (typeof Node !== 'undefined' ? Node.ELEMENT_NODE : NODE_TYPES.ELEMENT_NODE)) {
|
||||||
// Element node - analyze for styling and extract editable properties
|
// Element node - analyze for styling and extract editable properties
|
||||||
const styleInfo = this.analyzeElement(node);
|
const styleInfo = this.analyzeElement(node);
|
||||||
|
|
||||||
if (styleInfo) {
|
if (styleInfo) {
|
||||||
// Styled element - add to both style map and structure
|
// Styled element - add to both style map and structure
|
||||||
styleMap.set(styleInfo.id, styleInfo);
|
styleMap.set(styleInfo.id, styleInfo);
|
||||||
|
|
||||||
// Extract all editable properties for this element
|
// Extract all editable properties for this element
|
||||||
const editableProperties = this.extractEditableProperties(node);
|
const editableProperties = this.extractEditableProperties(node);
|
||||||
|
|
||||||
structure.push({
|
structure.push({
|
||||||
type: 'styled',
|
type: 'styled',
|
||||||
styleId: styleInfo.id,
|
styleId: styleInfo.id,
|
||||||
@@ -88,7 +78,7 @@ export class StyleDetectionEngine {
|
|||||||
} else {
|
} else {
|
||||||
// Unstyled element - treat as text
|
// Unstyled element - treat as text
|
||||||
structure.push({
|
structure.push({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: node.textContent
|
content: node.textContent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,10 +96,10 @@ export class StyleDetectionEngine {
|
|||||||
extractEditableProperties(element) {
|
extractEditableProperties(element) {
|
||||||
const tagName = element.tagName.toLowerCase();
|
const tagName = element.tagName.toLowerCase();
|
||||||
const properties = {};
|
const properties = {};
|
||||||
|
|
||||||
// Always include text content as the primary editable property
|
// Always include text content as the primary editable property
|
||||||
properties.content = element.textContent;
|
properties.content = element.textContent;
|
||||||
|
|
||||||
// Add element-specific editable properties
|
// Add element-specific editable properties
|
||||||
switch (tagName) {
|
switch (tagName) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -119,13 +109,13 @@ export class StyleDetectionEngine {
|
|||||||
properties.target = element.getAttribute('target');
|
properties.target = element.getAttribute('target');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'img':
|
case 'img':
|
||||||
properties.src = element.src || '';
|
properties.src = element.src || '';
|
||||||
properties.alt = element.alt || '';
|
properties.alt = element.alt || '';
|
||||||
properties.title = element.title || '';
|
properties.title = element.title || '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'button':
|
case 'button':
|
||||||
properties.content = element.textContent;
|
properties.content = element.textContent;
|
||||||
if (element.type) {
|
if (element.type) {
|
||||||
@@ -135,7 +125,7 @@ export class StyleDetectionEngine {
|
|||||||
properties.disabled = element.disabled;
|
properties.disabled = element.disabled;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'input':
|
case 'input':
|
||||||
properties.value = element.value || '';
|
properties.value = element.value || '';
|
||||||
properties.placeholder = element.placeholder || '';
|
properties.placeholder = element.placeholder || '';
|
||||||
@@ -143,12 +133,12 @@ export class StyleDetectionEngine {
|
|||||||
properties.type = element.type;
|
properties.type = element.type;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// For other elements, content is the main editable property
|
// For other elements, content is the main editable property
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,16 +152,16 @@ export class StyleDetectionEngine {
|
|||||||
const tagName = element.tagName.toLowerCase();
|
const tagName = element.tagName.toLowerCase();
|
||||||
const classes = Array.from(element.classList);
|
const classes = Array.from(element.classList);
|
||||||
const attributes = this.extractElementAttributes(element);
|
const attributes = this.extractElementAttributes(element);
|
||||||
|
|
||||||
// Skip elements without styling (no classes or special attributes)
|
// Skip elements without styling (no classes or special attributes)
|
||||||
if (classes.length === 0 && !this.hasSignificantAttributes(attributes)) {
|
if (classes.length === 0 && !this.hasSignificantAttributes(attributes)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique style ID and human-readable name
|
// Generate unique style ID and human-readable name
|
||||||
const styleId = this.generateStyleId(tagName, classes, attributes);
|
const styleId = this.generateStyleId(tagName, classes, attributes);
|
||||||
const styleName = this.generateStyleName(tagName, classes, attributes);
|
const styleName = this.generateStyleName(tagName, classes, attributes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: styleId,
|
id: styleId,
|
||||||
name: styleName,
|
name: styleName,
|
||||||
@@ -193,18 +183,18 @@ export class StyleDetectionEngine {
|
|||||||
extractElementAttributes(element) {
|
extractElementAttributes(element) {
|
||||||
const attributes = {};
|
const attributes = {};
|
||||||
const skipAttributes = new Set(['class', 'id']); // These are handled separately
|
const skipAttributes = new Set(['class', 'id']); // These are handled separately
|
||||||
|
|
||||||
for (const attr of element.attributes) {
|
for (const attr of element.attributes) {
|
||||||
if (!skipAttributes.has(attr.name)) {
|
if (!skipAttributes.has(attr.name)) {
|
||||||
attributes[attr.name] = attr.value;
|
attributes[attr.name] = attr.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include ID if present (it's significant for styling)
|
// Include ID if present (it's significant for styling)
|
||||||
if (element.id) {
|
if (element.id) {
|
||||||
attributes.id = element.id;
|
attributes.id = element.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +206,8 @@ export class StyleDetectionEngine {
|
|||||||
*/
|
*/
|
||||||
hasSignificantAttributes(attributes) {
|
hasSignificantAttributes(attributes) {
|
||||||
// Consider data-*, aria-*, href, rel, target, etc. as significant
|
// Consider data-*, aria-*, href, rel, target, etc. as significant
|
||||||
return Object.keys(attributes).some(key =>
|
return Object.keys(attributes).some(key =>
|
||||||
key.startsWith('data-') ||
|
key.startsWith('data-') ||
|
||||||
key.startsWith('aria-') ||
|
key.startsWith('aria-') ||
|
||||||
key === 'href' ||
|
key === 'href' ||
|
||||||
key === 'rel' ||
|
key === 'rel' ||
|
||||||
@@ -255,17 +245,17 @@ export class StyleDetectionEngine {
|
|||||||
if (this.styleNameMappings.has(key)) {
|
if (this.styleNameMappings.has(key)) {
|
||||||
return this.styleNameMappings.get(key);
|
return this.styleNameMappings.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate name from class names
|
// Generate name from class names
|
||||||
if (classes.length > 0) {
|
if (classes.length > 0) {
|
||||||
return this.classesToDisplayName(classes);
|
return this.classesToDisplayName(classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate name from tag + attributes
|
// Generate name from tag + attributes
|
||||||
if (attributes.id) {
|
if (attributes.id) {
|
||||||
return this.tagToDisplayName(tagName) + ' (' + attributes.id + ')';
|
return this.tagToDisplayName(tagName) + ' (' + attributes.id + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to tag name
|
// Fallback to tag name
|
||||||
return this.tagToDisplayName(tagName);
|
return this.tagToDisplayName(tagName);
|
||||||
}
|
}
|
||||||
@@ -276,7 +266,7 @@ export class StyleDetectionEngine {
|
|||||||
*/
|
*/
|
||||||
initializeStyleMappings() {
|
initializeStyleMappings() {
|
||||||
const mappings = new Map();
|
const mappings = new Map();
|
||||||
|
|
||||||
// From demo examples in simple/index.html
|
// From demo examples in simple/index.html
|
||||||
mappings.set('strong.emph', 'Emphasis');
|
mappings.set('strong.emph', 'Emphasis');
|
||||||
mappings.set('strong.brand', 'Brand');
|
mappings.set('strong.brand', 'Brand');
|
||||||
@@ -289,14 +279,14 @@ export class StyleDetectionEngine {
|
|||||||
mappings.set('blockquote.testimonial', 'Testimonial');
|
mappings.set('blockquote.testimonial', 'Testimonial');
|
||||||
mappings.set('i.icon-home', 'Home Icon');
|
mappings.set('i.icon-home', 'Home Icon');
|
||||||
mappings.set('i.icon-info', 'Info Icon');
|
mappings.set('i.icon-info', 'Info Icon');
|
||||||
|
|
||||||
// Common patterns
|
// Common patterns
|
||||||
mappings.set('strong.highlight', 'Highlight Bold');
|
mappings.set('strong.highlight', 'Highlight Bold');
|
||||||
mappings.set('span.brand', 'Brand Style');
|
mappings.set('span.brand', 'Brand Style');
|
||||||
mappings.set('a.button', 'Button Link');
|
mappings.set('a.button', 'Button Link');
|
||||||
mappings.set('span.tag', 'Tag');
|
mappings.set('span.tag', 'Tag');
|
||||||
mappings.set('span.badge', 'Badge');
|
mappings.set('span.badge', 'Badge');
|
||||||
|
|
||||||
return mappings;
|
return mappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +314,7 @@ export class StyleDetectionEngine {
|
|||||||
tagToDisplayName(tagName) {
|
tagToDisplayName(tagName) {
|
||||||
const tagMappings = {
|
const tagMappings = {
|
||||||
'strong': 'Bold',
|
'strong': 'Bold',
|
||||||
'em': 'Italic',
|
'em': 'Italic',
|
||||||
'span': 'Style',
|
'span': 'Style',
|
||||||
'a': 'Link',
|
'a': 'Link',
|
||||||
'button': 'Button',
|
'button': 'Button',
|
||||||
@@ -332,7 +322,7 @@ export class StyleDetectionEngine {
|
|||||||
'code': 'Code',
|
'code': 'Code',
|
||||||
'blockquote': 'Quote'
|
'blockquote': 'Quote'
|
||||||
};
|
};
|
||||||
|
|
||||||
return tagMappings[tagName] || tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
return tagMappings[tagName] || tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,19 +336,19 @@ export class StyleDetectionEngine {
|
|||||||
extractTemplate(element) {
|
extractTemplate(element) {
|
||||||
const tagName = element.tagName.toLowerCase();
|
const tagName = element.tagName.toLowerCase();
|
||||||
const clone = element.cloneNode(false); // Clone without children
|
const clone = element.cloneNode(false); // Clone without children
|
||||||
|
|
||||||
// Create template with placeholders for different element types
|
// Create template with placeholders for different element types
|
||||||
const template = {
|
const template = {
|
||||||
tagName: tagName,
|
tagName: tagName,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
editableProperties: []
|
editableProperties: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Copy all attributes
|
// Copy all attributes
|
||||||
for (const attr of element.attributes) {
|
for (const attr of element.attributes) {
|
||||||
template.attributes[attr.name] = attr.value;
|
template.attributes[attr.name] = attr.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define editable properties and their placeholders
|
// Define editable properties and their placeholders
|
||||||
switch (tagName) {
|
switch (tagName) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -369,7 +359,7 @@ export class StyleDetectionEngine {
|
|||||||
}
|
}
|
||||||
clone.textContent = '{{CONTENT}}';
|
clone.textContent = '{{CONTENT}}';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'img':
|
case 'img':
|
||||||
template.editableProperties = ['src', 'alt', 'title'];
|
template.editableProperties = ['src', 'alt', 'title'];
|
||||||
template.attributes.src = '{{SRC}}';
|
template.attributes.src = '{{SRC}}';
|
||||||
@@ -378,12 +368,12 @@ export class StyleDetectionEngine {
|
|||||||
template.attributes.title = '{{TITLE}}';
|
template.attributes.title = '{{TITLE}}';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'button':
|
case 'button':
|
||||||
template.editableProperties = ['content'];
|
template.editableProperties = ['content'];
|
||||||
clone.textContent = '{{CONTENT}}';
|
clone.textContent = '{{CONTENT}}';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'input':
|
case 'input':
|
||||||
template.editableProperties = ['value', 'placeholder'];
|
template.editableProperties = ['value', 'placeholder'];
|
||||||
if (template.attributes.value !== undefined) {
|
if (template.attributes.value !== undefined) {
|
||||||
@@ -393,17 +383,17 @@ export class StyleDetectionEngine {
|
|||||||
template.attributes.placeholder = '{{PLACEHOLDER}}';
|
template.attributes.placeholder = '{{PLACEHOLDER}}';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Default: only content is editable
|
// Default: only content is editable
|
||||||
template.editableProperties = ['content'];
|
template.editableProperties = ['content'];
|
||||||
clone.textContent = '{{CONTENT}}';
|
clone.textContent = '{{CONTENT}}';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store both the structured template and the HTML template for backward compatibility
|
// Store both the structured template and the HTML template for backward compatibility
|
||||||
template.html = clone.outerHTML;
|
template.html = clone.outerHTML;
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,10 +407,10 @@ export class StyleDetectionEngine {
|
|||||||
*/
|
*/
|
||||||
createElementFromTemplate(styleInfo, properties) {
|
createElementFromTemplate(styleInfo, properties) {
|
||||||
const element = document.createElement(styleInfo.tagName);
|
const element = document.createElement(styleInfo.tagName);
|
||||||
|
|
||||||
// Apply classes
|
// Apply classes
|
||||||
styleInfo.classes.forEach(cls => element.classList.add(cls));
|
styleInfo.classes.forEach(cls => element.classList.add(cls));
|
||||||
|
|
||||||
// Apply base attributes (non-editable ones)
|
// Apply base attributes (non-editable ones)
|
||||||
Object.entries(styleInfo.attributes).forEach(([key, value]) => {
|
Object.entries(styleInfo.attributes).forEach(([key, value]) => {
|
||||||
// Skip attributes that will be set from properties
|
// Skip attributes that will be set from properties
|
||||||
@@ -428,7 +418,7 @@ export class StyleDetectionEngine {
|
|||||||
element.setAttribute(key, value);
|
element.setAttribute(key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle properties - support both object and string (backward compatibility)
|
// Handle properties - support both object and string (backward compatibility)
|
||||||
if (typeof properties === 'string') {
|
if (typeof properties === 'string') {
|
||||||
// Legacy support: treat as text content
|
// Legacy support: treat as text content
|
||||||
@@ -437,7 +427,7 @@ export class StyleDetectionEngine {
|
|||||||
// New multi-property support
|
// New multi-property support
|
||||||
this.applyPropertiesToElement(element, properties, styleInfo.tagName);
|
this.applyPropertiesToElement(element, properties, styleInfo.tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +445,7 @@ export class StyleDetectionEngine {
|
|||||||
'input': ['value', 'placeholder'],
|
'input': ['value', 'placeholder'],
|
||||||
'button': []
|
'button': []
|
||||||
};
|
};
|
||||||
|
|
||||||
return (editableAttributes[tagName] || []).includes(attributeName);
|
return (editableAttributes[tagName] || []).includes(attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +472,7 @@ export class StyleDetectionEngine {
|
|||||||
element.target = properties.target;
|
element.target = properties.target;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'img':
|
case 'img':
|
||||||
if (properties.src !== undefined) {
|
if (properties.src !== undefined) {
|
||||||
element.src = properties.src;
|
element.src = properties.src;
|
||||||
@@ -494,7 +484,7 @@ export class StyleDetectionEngine {
|
|||||||
element.title = properties.title;
|
element.title = properties.title;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'button':
|
case 'button':
|
||||||
if (properties.content !== undefined) {
|
if (properties.content !== undefined) {
|
||||||
element.textContent = properties.content;
|
element.textContent = properties.content;
|
||||||
@@ -506,7 +496,7 @@ export class StyleDetectionEngine {
|
|||||||
element.disabled = properties.disabled;
|
element.disabled = properties.disabled;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'input':
|
case 'input':
|
||||||
if (properties.value !== undefined) {
|
if (properties.value !== undefined) {
|
||||||
element.value = properties.value;
|
element.value = properties.value;
|
||||||
@@ -518,7 +508,7 @@ export class StyleDetectionEngine {
|
|||||||
element.type = properties.type;
|
element.type = properties.type;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Default: set text content
|
// Default: set text content
|
||||||
if (properties.content !== undefined) {
|
if (properties.content !== undefined) {
|
||||||
@@ -561,7 +551,7 @@ export class StyleDetectionEngine {
|
|||||||
*/
|
*/
|
||||||
reconstructHTML(structure, styles, updatedProperties = {}) {
|
reconstructHTML(structure, styles, updatedProperties = {}) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
structure.forEach((piece, index) => {
|
structure.forEach((piece, index) => {
|
||||||
if (piece.type === 'text') {
|
if (piece.type === 'text') {
|
||||||
// Check if any styles are applied to this text piece
|
// Check if any styles are applied to this text piece
|
||||||
@@ -578,7 +568,7 @@ export class StyleDetectionEngine {
|
|||||||
} else if (piece.type === 'styled') {
|
} else if (piece.type === 'styled') {
|
||||||
// Use updated properties or original properties
|
// Use updated properties or original properties
|
||||||
const properties = updatedProperties[index]?.properties || piece.properties;
|
const properties = updatedProperties[index]?.properties || piece.properties;
|
||||||
|
|
||||||
if (styles.has(piece.styleId)) {
|
if (styles.has(piece.styleId)) {
|
||||||
const styleInfo = styles.get(piece.styleId);
|
const styleInfo = styles.get(piece.styleId);
|
||||||
const styledElement = this.createElementFromTemplate(styleInfo, properties);
|
const styledElement = this.createElementFromTemplate(styleInfo, properties);
|
||||||
@@ -590,10 +580,88 @@ 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
|
||||||
*
|
*
|
||||||
@@ -624,12 +692,12 @@ export class StyleDetectionEngine {
|
|||||||
// Simple approach: if text length matches, assume same structure
|
// Simple approach: if text length matches, assume same structure
|
||||||
// More sophisticated approaches could use diff algorithms
|
// More sophisticated approaches could use diff algorithms
|
||||||
const originalText = this.extractTextFromStructure(originalStructure);
|
const originalText = this.extractTextFromStructure(originalStructure);
|
||||||
|
|
||||||
if (newText === originalText) {
|
if (newText === originalText) {
|
||||||
// No changes - return original structure
|
// No changes - return original structure
|
||||||
return originalStructure;
|
return originalStructure;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, create simple text structure
|
// For now, create simple text structure
|
||||||
// TODO: Implement smarter text-to-structure mapping
|
// TODO: Implement smarter text-to-structure mapping
|
||||||
return [{
|
return [{
|
||||||
@@ -640,4 +708,4 @@ export class StyleDetectionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const styleDetectionEngine = new StyleDetectionEngine();
|
export const styleDetectionEngine = new StyleDetectionEngine();
|
||||||
|
|||||||
Reference in New Issue
Block a user