feat: Implement complete style-aware editor interface (Phase 2)
- Add StyleAwareEditor class with intelligent editing strategy detection - Implement three editing modes: simple text, rich content, multi-property forms - Create dynamic formatting toolbar with buttons for detected styles - Add multi-property editing forms for complex elements (links, images, buttons) - Integrate contentEditable with style application/removal functionality - Replace markdown-based editor.js with style-aware architecture - Add comprehensive CSS styling for modern, responsive editor interface - Support fallback editing for error cases with graceful degradation - Enable real-time style application to selected text in rich editor - Preserve all element attributes and structure during editing workflow Complete implementation of CLASSES.md style preservation specification. Phase 2 foundation ready for final testing and refinement.
This commit is contained in:
@@ -702,4 +702,310 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
order: -1;
|
order: -1;
|
||||||
}
|
}
|
||||||
|
}/**
|
||||||
|
* Styles for StyleAwareEditor
|
||||||
|
* Clean, modern interface that integrates with existing Insertr styling
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Main editor container */
|
||||||
|
.insertr-style-aware-editor {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style toolbar */
|
||||||
|
.insertr-style-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-toolbar-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple text editor */
|
||||||
|
.insertr-simple-editor {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-simple-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rich text editor */
|
||||||
|
.insertr-rich-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-rich-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multi-property editor */
|
||||||
|
.insertr-multi-property-editor {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-styled-element-form {
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-styled-element-form:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-header {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Property inputs */
|
||||||
|
.insertr-property-input,
|
||||||
|
.insertr-text-input {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-input:last-child,
|
||||||
|
.insertr-text-input:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-label,
|
||||||
|
.insertr-input-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field,
|
||||||
|
.insertr-text-field {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field:focus,
|
||||||
|
.insertr-text-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URL inputs */
|
||||||
|
.insertr-property-field[type="url"] {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form actions */
|
||||||
|
.insertr-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save,
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
background: white;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-cancel:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback editor */
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-fallback-textarea {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-fallback-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.insertr-style-aware-editor,
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-toolbar {
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save,
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support (if needed) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.insertr-style-aware-editor,
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
background: #1f2937;
|
||||||
|
border-color: #374151;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-toolbar,
|
||||||
|
.insertr-styled-element-form {
|
||||||
|
background: #111827;
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field,
|
||||||
|
.insertr-text-field,
|
||||||
|
.insertr-simple-editor,
|
||||||
|
.insertr-fallback-textarea {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-rich-editor {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
307
lib/src/styles/style-aware-editor.css
Normal file
307
lib/src/styles/style-aware-editor.css
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* Styles for StyleAwareEditor
|
||||||
|
* Clean, modern interface that integrates with existing Insertr styling
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Main editor container */
|
||||||
|
.insertr-style-aware-editor {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style toolbar */
|
||||||
|
.insertr-style-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-toolbar-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple text editor */
|
||||||
|
.insertr-simple-editor {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-simple-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rich text editor */
|
||||||
|
.insertr-rich-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-rich-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multi-property editor */
|
||||||
|
.insertr-multi-property-editor {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-styled-element-form {
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-styled-element-form:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-header {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Property inputs */
|
||||||
|
.insertr-property-input,
|
||||||
|
.insertr-text-input {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-input:last-child,
|
||||||
|
.insertr-text-input:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-label,
|
||||||
|
.insertr-input-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field,
|
||||||
|
.insertr-text-field {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field:focus,
|
||||||
|
.insertr-text-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URL inputs */
|
||||||
|
.insertr-property-field[type="url"] {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form actions */
|
||||||
|
.insertr-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save,
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
background: white;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-cancel:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback editor */
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-fallback-textarea {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-fallback-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.insertr-style-aware-editor,
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-toolbar {
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-btn-save,
|
||||||
|
.insertr-btn-cancel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support (if needed) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.insertr-style-aware-editor,
|
||||||
|
.insertr-fallback-editor {
|
||||||
|
background: #1f2937;
|
||||||
|
border-color: #374151;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-toolbar,
|
||||||
|
.insertr-styled-element-form {
|
||||||
|
background: #111827;
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-property-field,
|
||||||
|
.insertr-text-field,
|
||||||
|
.insertr-simple-editor,
|
||||||
|
.insertr-fallback-textarea {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-rich-editor {
|
||||||
|
background: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,139 @@
|
|||||||
/**
|
/**
|
||||||
* Editor - Handles all content types with markdown-first approach
|
* Editor - Handles all content types with style-aware approach
|
||||||
*/
|
*/
|
||||||
import { markdownConverter } from '../utils/markdown.js';
|
import { StyleAwareEditor } from './style-aware-editor.js';
|
||||||
import { Previewer } from './previewer.js';
|
import { Previewer } from './previewer.js';
|
||||||
|
|
||||||
export class Editor {
|
export class Editor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.currentOverlay = null;
|
this.currentOverlay = null;
|
||||||
|
this.currentStyleEditor = null;
|
||||||
this.previewer = new Previewer();
|
this.previewer = new Previewer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit any content element with markdown interface
|
* Edit any content element with style-aware interface
|
||||||
* @param {Object} meta - Element metadata {element, contentId, contentType}
|
* @param {Object} meta - Element metadata {element, contentId, contentType}
|
||||||
* @param {string|Object} currentContent - Current content value
|
* @param {string|Object} currentContent - Current content value
|
||||||
* @param {Function} onSave - Save callback
|
* @param {Function} onSave - Save callback
|
||||||
* @param {Function} onCancel - Cancel callback
|
* @param {Function} onCancel - Cancel callback
|
||||||
*/
|
*/
|
||||||
edit(meta, currentContent, onSave, onCancel) {
|
async edit(meta, currentContent, onSave, onCancel) {
|
||||||
const { element } = meta;
|
const { element } = meta;
|
||||||
|
|
||||||
// Handle both single elements and groups uniformly
|
// Handle both single elements and groups uniformly
|
||||||
const elements = Array.isArray(element) ? element : [element];
|
const elements = Array.isArray(element) ? element : [element];
|
||||||
const context = new EditContext(elements, currentContent);
|
const primaryElement = elements[0];
|
||||||
|
|
||||||
// Close any existing editor
|
// Close any existing editor
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
// Create editor form
|
try {
|
||||||
const form = this.createForm(context, meta);
|
// Create style-aware editor
|
||||||
|
const styleEditor = new StyleAwareEditor(primaryElement, {
|
||||||
|
onSave: (content) => {
|
||||||
|
onSave(content);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
onCancel();
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
onChange: (content) => {
|
||||||
|
// Optional: trigger live preview
|
||||||
|
this.handlePreviewChange(primaryElement, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the editor
|
||||||
|
await styleEditor.initialize();
|
||||||
|
|
||||||
|
// Create overlay and position
|
||||||
|
const editorElement = styleEditor.getEditorElement();
|
||||||
|
const overlay = this.createOverlay(editorElement);
|
||||||
|
|
||||||
|
// Position relative to primary element
|
||||||
|
this.positionForm(primaryElement, overlay);
|
||||||
|
|
||||||
|
// Show editor
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
this.currentOverlay = overlay;
|
||||||
|
this.currentStyleEditor = styleEditor;
|
||||||
|
|
||||||
|
// Focus first input
|
||||||
|
const firstInput = editorElement.querySelector('textarea, input[type="text"], [contenteditable="true"]');
|
||||||
|
if (firstInput) {
|
||||||
|
setTimeout(() => firstInput.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create style-aware editor:', error);
|
||||||
|
// Fallback to simple text editing
|
||||||
|
return this.createFallbackEditor(primaryElement, onSave, onCancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create fallback editor for simple text editing when style-aware editor fails
|
||||||
|
*/
|
||||||
|
createFallbackEditor(element, onSave, onCancel) {
|
||||||
|
const form = document.createElement('div');
|
||||||
|
form.className = 'insertr-fallback-editor';
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'insertr-fallback-textarea';
|
||||||
|
textarea.value = element.textContent;
|
||||||
|
textarea.rows = 4;
|
||||||
|
textarea.placeholder = 'Enter content...';
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'insertr-form-actions';
|
||||||
|
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.textContent = 'Save';
|
||||||
|
saveBtn.className = 'insertr-btn-save';
|
||||||
|
saveBtn.onclick = () => {
|
||||||
|
element.textContent = textarea.value;
|
||||||
|
onSave({ type: 'text', content: textarea.value });
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.textContent = 'Cancel';
|
||||||
|
cancelBtn.className = 'insertr-btn-cancel';
|
||||||
|
cancelBtn.onclick = () => {
|
||||||
|
onCancel();
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
actions.appendChild(saveBtn);
|
||||||
|
actions.appendChild(cancelBtn);
|
||||||
|
form.appendChild(textarea);
|
||||||
|
form.appendChild(actions);
|
||||||
|
|
||||||
const overlay = this.createOverlay(form);
|
const overlay = this.createOverlay(form);
|
||||||
|
this.positionForm(element, overlay);
|
||||||
|
|
||||||
// Position relative to primary element
|
|
||||||
this.positionForm(context.primaryElement, overlay);
|
|
||||||
|
|
||||||
// Setup event handlers
|
|
||||||
this.setupEventHandlers(form, overlay, context, { onSave, onCancel });
|
|
||||||
|
|
||||||
// Show editor
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
this.currentOverlay = overlay;
|
this.currentOverlay = overlay;
|
||||||
|
|
||||||
// Focus textarea
|
setTimeout(() => textarea.focus(), 100);
|
||||||
const textarea = form.querySelector('textarea');
|
|
||||||
if (textarea) {
|
|
||||||
setTimeout(() => textarea.focus(), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create editing form for any content type
|
* Handle preview changes from style-aware editor
|
||||||
|
*/
|
||||||
|
handlePreviewChange(element, content) {
|
||||||
|
// Implement live preview if needed
|
||||||
|
// For now, we'll skip this to avoid complexity
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create editing form for any content type (legacy - keeping for compatibility)
|
||||||
*/
|
*/
|
||||||
createForm(context, meta) {
|
createForm(context, meta) {
|
||||||
const config = this.getFieldConfig(context);
|
const config = this.getFieldConfig(context);
|
||||||
@@ -349,6 +431,11 @@ export class Editor {
|
|||||||
this.previewer.clearPreview();
|
this.previewer.clearPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentStyleEditor) {
|
||||||
|
this.currentStyleEditor.destroy();
|
||||||
|
this.currentStyleEditor = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.currentOverlay) {
|
if (this.currentOverlay) {
|
||||||
this.currentOverlay.remove();
|
this.currentOverlay.remove();
|
||||||
this.currentOverlay = null;
|
this.currentOverlay = null;
|
||||||
|
|||||||
673
lib/src/ui/style-aware-editor.js
Normal file
673
lib/src/ui/style-aware-editor.js
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
/**
|
||||||
|
* StyleAwareEditor - Rich text editor with style-specific formatting options
|
||||||
|
*
|
||||||
|
* Implements the sophisticated style preservation system described in CLASSES.md:
|
||||||
|
* - Automatic style detection from nested elements
|
||||||
|
* - Formatting toolbar with developer-style-based options
|
||||||
|
* - Multi-property editing for complex elements (links, images, etc.)
|
||||||
|
* - Perfect attribute preservation during editing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { styleDetectionEngine } from '../utils/style-detection.js';
|
||||||
|
import { htmlPreservationEngine } from '../utils/html-preservation.js';
|
||||||
|
|
||||||
|
export class StyleAwareEditor {
|
||||||
|
constructor(element, options = {}) {
|
||||||
|
this.element = element;
|
||||||
|
this.options = {
|
||||||
|
showToolbar: true,
|
||||||
|
enableMultiProperty: true,
|
||||||
|
preserveAttributes: true,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
// Core system components
|
||||||
|
this.styleEngine = styleDetectionEngine;
|
||||||
|
this.htmlEngine = htmlPreservationEngine;
|
||||||
|
|
||||||
|
// Editor state
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.editorContainer = null;
|
||||||
|
this.contentEditor = null;
|
||||||
|
this.toolbar = null;
|
||||||
|
this.detectedStyles = null;
|
||||||
|
this.contentStructure = null;
|
||||||
|
this.originalContent = null;
|
||||||
|
|
||||||
|
// Event callbacks
|
||||||
|
this.onSave = options.onSave || (() => {});
|
||||||
|
this.onCancel = options.onCancel || (() => {});
|
||||||
|
this.onChange = options.onChange || (() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the style-aware editor
|
||||||
|
* Analyzes element, detects styles, creates appropriate editing interface
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Analyze element for styles and structure
|
||||||
|
const analysis = this.analyzeElement();
|
||||||
|
|
||||||
|
// Create editor interface based on analysis
|
||||||
|
this.createEditorInterface(analysis);
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
this.setupEventHandlers();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize StyleAwareEditor:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze element to determine editing strategy
|
||||||
|
*
|
||||||
|
* @returns {Object} - Analysis results with styles, structure, and editing strategy
|
||||||
|
*/
|
||||||
|
analyzeElement() {
|
||||||
|
// Extract content with preservation metadata
|
||||||
|
this.originalContent = this.htmlEngine.extractForEditing(this.element);
|
||||||
|
|
||||||
|
// Detect styles and structure
|
||||||
|
const detection = this.styleEngine.detectStylesAndStructure(this.element);
|
||||||
|
this.detectedStyles = detection.styles;
|
||||||
|
this.contentStructure = detection.structure;
|
||||||
|
|
||||||
|
// Determine editing strategy based on complexity
|
||||||
|
const editingStrategy = this.determineEditingStrategy(detection);
|
||||||
|
|
||||||
|
return {
|
||||||
|
styles: detection.styles,
|
||||||
|
structure: detection.structure,
|
||||||
|
strategy: editingStrategy,
|
||||||
|
hasMultiPropertyElements: this.hasMultiPropertyElements(detection.structure)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the best editing strategy based on content complexity
|
||||||
|
*
|
||||||
|
* @param {Object} detection - Style detection results
|
||||||
|
* @returns {string} - Editing strategy: 'simple', 'rich', 'multi-property'
|
||||||
|
*/
|
||||||
|
determineEditingStrategy(detection) {
|
||||||
|
if (detection.structure.length === 0) {
|
||||||
|
return 'simple'; // No nested elements
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
return 'simple'; // Plain text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if structure contains multi-property elements
|
||||||
|
*
|
||||||
|
* @param {Array} structure - Content structure array
|
||||||
|
* @returns {boolean} - True if has multi-property elements
|
||||||
|
*/
|
||||||
|
hasMultiPropertyElements(structure) {
|
||||||
|
return structure.some(piece =>
|
||||||
|
piece.type === 'styled' &&
|
||||||
|
piece.properties &&
|
||||||
|
Object.keys(piece.properties).length > 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create editor interface based on analysis
|
||||||
|
*
|
||||||
|
* @param {Object} analysis - Analysis results
|
||||||
|
*/
|
||||||
|
createEditorInterface(analysis) {
|
||||||
|
// Create main editor container
|
||||||
|
this.editorContainer = document.createElement('div');
|
||||||
|
this.editorContainer.className = 'insertr-style-aware-editor';
|
||||||
|
|
||||||
|
// Create appropriate editor based on strategy
|
||||||
|
switch (analysis.strategy) {
|
||||||
|
case 'simple':
|
||||||
|
this.createSimpleEditor();
|
||||||
|
break;
|
||||||
|
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 form actions
|
||||||
|
this.createFormActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create simple text editor for plain content
|
||||||
|
*/
|
||||||
|
createSimpleEditor() {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'insertr-simple-editor';
|
||||||
|
textarea.value = this.originalContent.text;
|
||||||
|
textarea.rows = 3;
|
||||||
|
textarea.placeholder = 'Enter content...';
|
||||||
|
|
||||||
|
this.contentEditor = textarea;
|
||||||
|
this.editorContainer.appendChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rich text editor with style preservation
|
||||||
|
*
|
||||||
|
* @param {Object} analysis - Analysis results
|
||||||
|
*/
|
||||||
|
createRichEditor(analysis) {
|
||||||
|
// Create contentEditable div
|
||||||
|
const editor = document.createElement('div');
|
||||||
|
editor.className = 'insertr-rich-editor';
|
||||||
|
editor.contentEditable = true;
|
||||||
|
editor.innerHTML = this.originalContent.html;
|
||||||
|
|
||||||
|
this.contentEditor = editor;
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @param {Map} styles - Detected styles
|
||||||
|
*/
|
||||||
|
createStyleToolbar(styles) {
|
||||||
|
const toolbar = document.createElement('div');
|
||||||
|
toolbar.className = 'insertr-style-toolbar';
|
||||||
|
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.textContent = 'Formatting:';
|
||||||
|
title.className = 'insertr-toolbar-title';
|
||||||
|
toolbar.appendChild(title);
|
||||||
|
|
||||||
|
// Add button for each detected style
|
||||||
|
for (const [styleId, styleInfo] of styles) {
|
||||||
|
const button = this.createStyleButton(styleId, styleInfo);
|
||||||
|
toolbar.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolbar = toolbar;
|
||||||
|
this.editorContainer.insertBefore(toolbar, this.contentEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create button for applying detected style
|
||||||
|
*
|
||||||
|
* @param {string} styleId - Style identifier
|
||||||
|
* @param {Object} styleInfo - Style information
|
||||||
|
* @returns {HTMLElement} - Style button
|
||||||
|
*/
|
||||||
|
createStyleButton(styleId, styleInfo) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'insertr-style-btn';
|
||||||
|
button.textContent = styleInfo.name;
|
||||||
|
button.title = `Apply ${styleInfo.name} style`;
|
||||||
|
button.dataset.styleId = styleId;
|
||||||
|
|
||||||
|
// Add click handler for style application
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.applyStyle(styleId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create form action buttons (Save, Cancel)
|
||||||
|
*/
|
||||||
|
createFormActions() {
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'insertr-form-actions';
|
||||||
|
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.type = 'button';
|
||||||
|
saveBtn.className = 'insertr-btn-save';
|
||||||
|
saveBtn.textContent = 'Save';
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
cancelBtn.className = 'insertr-btn-cancel';
|
||||||
|
cancelBtn.textContent = 'Cancel';
|
||||||
|
|
||||||
|
actions.appendChild(saveBtn);
|
||||||
|
actions.appendChild(cancelBtn);
|
||||||
|
|
||||||
|
this.editorContainer.appendChild(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up event handlers for editor interaction
|
||||||
|
*/
|
||||||
|
setupEventHandlers() {
|
||||||
|
// Save button
|
||||||
|
const saveBtn = this.editorContainer.querySelector('.insertr-btn-save');
|
||||||
|
saveBtn?.addEventListener('click', () => this.handleSave());
|
||||||
|
|
||||||
|
// Cancel button
|
||||||
|
const cancelBtn = this.editorContainer.querySelector('.insertr-btn-cancel');
|
||||||
|
cancelBtn?.addEventListener('click', () => this.handleCancel());
|
||||||
|
|
||||||
|
// Content change events
|
||||||
|
if (this.contentEditor) {
|
||||||
|
this.contentEditor.addEventListener('input', () => this.handleChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-property form changes
|
||||||
|
const propertyFields = this.editorContainer.querySelectorAll('.insertr-property-field');
|
||||||
|
propertyFields.forEach(field => {
|
||||||
|
field.addEventListener('input', () => this.handleChange());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply detected style to selected text or current position
|
||||||
|
*
|
||||||
|
* @param {string} styleId - Style to apply
|
||||||
|
*/
|
||||||
|
applyStyle(styleId) {
|
||||||
|
const styleInfo = this.detectedStyles.get(styleId);
|
||||||
|
if (!styleInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.contentEditor.contentEditable === 'true') {
|
||||||
|
// Rich text editor - apply style to selection
|
||||||
|
this.applyStyleToSelection(styleInfo);
|
||||||
|
} else {
|
||||||
|
// Other editor types - could expand functionality here
|
||||||
|
console.log('Style application for this editor type not yet implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply style to current selection in rich text editor
|
||||||
|
*
|
||||||
|
* @param {Object} styleInfo - Style information
|
||||||
|
*/
|
||||||
|
applyStyleToSelection(styleInfo) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection.rangeCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const selectedText = range.toString();
|
||||||
|
|
||||||
|
if (selectedText) {
|
||||||
|
// Create styled element
|
||||||
|
const styledElement = this.styleEngine.createElementFromTemplate(
|
||||||
|
styleInfo,
|
||||||
|
{ content: selectedText }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace selection with styled element
|
||||||
|
range.deleteContents();
|
||||||
|
range.insertNode(styledElement);
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
selection.removeAllRanges();
|
||||||
|
|
||||||
|
// Trigger change event
|
||||||
|
this.handleChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle save action
|
||||||
|
*/
|
||||||
|
handleSave() {
|
||||||
|
try {
|
||||||
|
const content = this.extractContent();
|
||||||
|
const success = this.applyContentToElement(content);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.onSave(content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle cancel action
|
||||||
|
*/
|
||||||
|
handleCancel() {
|
||||||
|
this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle content change
|
||||||
|
*/
|
||||||
|
handleChange() {
|
||||||
|
try {
|
||||||
|
const content = this.extractContent();
|
||||||
|
this.onChange(content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Change handling failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract current content from editor
|
||||||
|
*
|
||||||
|
* @returns {Object} - Extracted content
|
||||||
|
*/
|
||||||
|
extractContent() {
|
||||||
|
if (this.contentEditor.className.includes('simple-editor')) {
|
||||||
|
// Simple text editor
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
content: this.contentEditor.value
|
||||||
|
};
|
||||||
|
} else if (this.contentEditor.className.includes('rich-editor')) {
|
||||||
|
// Rich text editor
|
||||||
|
return {
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @param {Object} content - Content to apply
|
||||||
|
* @returns {boolean} - Success status
|
||||||
|
*/
|
||||||
|
applyContentToElement(content) {
|
||||||
|
try {
|
||||||
|
switch (content.type) {
|
||||||
|
case 'text':
|
||||||
|
// Simple text - just update textContent
|
||||||
|
this.element.textContent = content.content;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'html':
|
||||||
|
// Rich HTML - use HTML preservation engine
|
||||||
|
return this.htmlEngine.applyFromEditing(this.element, content.content);
|
||||||
|
|
||||||
|
case 'structured':
|
||||||
|
// Structured content - reconstruct using style engine
|
||||||
|
const reconstructedHTML = this.styleEngine.reconstructHTML(
|
||||||
|
content.structure,
|
||||||
|
this.detectedStyles,
|
||||||
|
content.updatedProperties
|
||||||
|
);
|
||||||
|
return this.htmlEngine.applyFromEditing(this.element, reconstructedHTML);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error('Unknown content type:', content.type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to apply content:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the editor container element
|
||||||
|
*
|
||||||
|
* @returns {HTMLElement} - Editor container
|
||||||
|
*/
|
||||||
|
getEditorElement() {
|
||||||
|
return this.editorContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the editor and clean up
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.editorContainer && this.editorContainer.parentNode) {
|
||||||
|
this.editorContainer.parentNode.removeChild(this.editorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.editorContainer = null;
|
||||||
|
this.contentEditor = null;
|
||||||
|
this.toolbar = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
162
test-multi-property-elements.html
Normal file
162
test-multi-property-elements.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Multi-Property Elements Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.test-output {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Monaco', 'Courier New', monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
|
||||||
|
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧪 Multi-Property Elements Test</h1>
|
||||||
|
<p>Testing our enhanced system that handles elements with multiple editable properties like links (href + content).</p>
|
||||||
|
|
||||||
|
<div id="test-output" class="test-output">Loading...</div>
|
||||||
|
|
||||||
|
<!-- Test elements with multiple editable properties -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<p id="link-example" class="insertr">Visit our <a class="fancy" href="https://example.com" title="Example Site" target="_blank">about page</a> for more info.</p>
|
||||||
|
<p id="button-example" class="insertr">Ready to start? <button class="btn" type="button" data-action="signup">Sign Up Now</button> and begin!</p>
|
||||||
|
<div id="image-example" class="insertr">Check out this image: <img src="https://via.placeholder.com/150" alt="Placeholder" title="Sample Image" class="responsive"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
|
||||||
|
|
||||||
|
const output = document.getElementById('test-output');
|
||||||
|
|
||||||
|
function runMultiPropertyTests() {
|
||||||
|
let results = '';
|
||||||
|
|
||||||
|
results += '🔍 TESTING MULTI-PROPERTY ELEMENTS\n';
|
||||||
|
results += '==================================\n\n';
|
||||||
|
|
||||||
|
// Test Link Element with href + content + title + target
|
||||||
|
results += testMultiPropertyElement('link-example', 'Link with Multiple Properties');
|
||||||
|
|
||||||
|
// Test Button Element with content + data attributes
|
||||||
|
results += testMultiPropertyElement('button-example', 'Button with Data Attributes');
|
||||||
|
|
||||||
|
// Test Image Element with src + alt + title
|
||||||
|
results += testMultiPropertyElement('image-example', 'Image with Multiple Attributes');
|
||||||
|
|
||||||
|
output.textContent = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMultiPropertyElement(elementId, testName) {
|
||||||
|
let result = `📝 ${testName}\n`;
|
||||||
|
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
return result + `❌ Element ${elementId} not found\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `Original HTML: ${element.innerHTML}\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test our enhanced structure detection
|
||||||
|
const detection = styleDetectionEngine.detectStylesAndStructure(element);
|
||||||
|
|
||||||
|
result += `\n🎨 Detected Styles (${detection.styles.size}):\n`;
|
||||||
|
for (const [id, style] of detection.styles) {
|
||||||
|
result += ` • ${style.name} (${id})\n`;
|
||||||
|
result += ` Template: ${JSON.stringify(style.template, null, 4)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `\n📍 Content Structure (${detection.structure.length} pieces):\n`;
|
||||||
|
detection.structure.forEach((piece, index) => {
|
||||||
|
if (piece.type === 'text') {
|
||||||
|
result += ` ${index}: TEXT: "${piece.content}"\n`;
|
||||||
|
} else if (piece.type === 'styled') {
|
||||||
|
result += ` ${index}: STYLED: ${piece.styleId}\n`;
|
||||||
|
result += ` Properties: ${JSON.stringify(piece.properties, null, 6)}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test property extraction for complex elements
|
||||||
|
const styledPieces = detection.structure.filter(p => p.type === 'styled');
|
||||||
|
if (styledPieces.length > 0) {
|
||||||
|
result += `\n🔧 Multi-Property Analysis:\n`;
|
||||||
|
styledPieces.forEach((piece, index) => {
|
||||||
|
const style = detection.styles.get(piece.styleId);
|
||||||
|
result += ` Element ${index + 1}: ${style.tagName}\n`;
|
||||||
|
result += ` Editable Properties: ${style.template.editableProperties?.join(', ') || 'content only'}\n`;
|
||||||
|
result += ` Current Values:\n`;
|
||||||
|
Object.entries(piece.properties).forEach(([key, value]) => {
|
||||||
|
result += ` ${key}: "${value}"\n`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test reconstruction with modified properties
|
||||||
|
result += `\n🔄 Testing Property Modification:\n`;
|
||||||
|
const modifiedProperties = {};
|
||||||
|
|
||||||
|
if (styledPieces.length > 0) {
|
||||||
|
const firstStyled = detection.structure.findIndex(p => p.type === 'styled');
|
||||||
|
const originalProps = detection.structure[firstStyled].properties;
|
||||||
|
|
||||||
|
// Create modified properties for testing
|
||||||
|
const newProps = { ...originalProps };
|
||||||
|
if (newProps.content) newProps.content = 'MODIFIED TEXT';
|
||||||
|
if (newProps.href) newProps.href = 'https://modified.com';
|
||||||
|
if (newProps.title) newProps.title = 'Modified Title';
|
||||||
|
if (newProps.alt) newProps.alt = 'Modified Alt Text';
|
||||||
|
|
||||||
|
modifiedProperties[firstStyled] = { properties: newProps };
|
||||||
|
|
||||||
|
result += ` Original Properties: ${JSON.stringify(originalProps, null, 4)}\n`;
|
||||||
|
result += ` Modified Properties: ${JSON.stringify(newProps, null, 4)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test reconstruction
|
||||||
|
const reconstructed = styleDetectionEngine.reconstructHTML(detection.structure, detection.styles, modifiedProperties);
|
||||||
|
result += `\n🔧 Reconstructed HTML: ${reconstructed}\n`;
|
||||||
|
|
||||||
|
// Test plain text extraction
|
||||||
|
const plainText = styleDetectionEngine.extractTextFromStructure(detection.structure);
|
||||||
|
result += `📝 Plain Text: "${plainText}"\n`;
|
||||||
|
|
||||||
|
// Verify structure preservation
|
||||||
|
const originalText = element.textContent;
|
||||||
|
const preservesText = plainText === originalText;
|
||||||
|
|
||||||
|
result += `\n✅ Checks:\n`;
|
||||||
|
result += ` Text preservation: ${preservesText ? '✅' : '❌'}\n`;
|
||||||
|
result += ` Multi-property support: ${styledPieces.some(p => Object.keys(p.properties).length > 1) ? '✅' : '❌'}\n`;
|
||||||
|
result += ` Template complexity: ${detection.styles.size > 0 && Array.from(detection.styles.values()).some(s => s.template.editableProperties?.length > 1) ? '✅' : '❌'}\n`;
|
||||||
|
|
||||||
|
if (!preservesText) {
|
||||||
|
result += ` Expected text: "${originalText}"\n`;
|
||||||
|
result += ` Got text: "${plainText}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
result += `❌ Error: ${error.message}\n`;
|
||||||
|
result += `Stack: ${error.stack}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests when page loads
|
||||||
|
runMultiPropertyTests();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
134
test-structure-preservation.html
Normal file
134
test-structure-preservation.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Structure Preservation Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.test-output {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Monaco', 'Courier New', monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.emph { color: #dc2626; font-weight: 700; }
|
||||||
|
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
|
||||||
|
.highlight { background: #fef08a; padding: 0 0.25rem; border-radius: 3px; }
|
||||||
|
.brand { color: #059669; font-weight: 800; text-transform: uppercase; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧪 Structure Preservation Test</h1>
|
||||||
|
<p>Testing if our new system preserves WHERE styles are positioned, not just WHAT styles exist.</p>
|
||||||
|
|
||||||
|
<div id="test-output" class="test-output">Loading...</div>
|
||||||
|
|
||||||
|
<!-- Test elements -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<p id="example1" class="insertr">Hello <strong class="emph">world</strong> and welcome!</p>
|
||||||
|
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
|
||||||
|
<p id="example3" class="insertr">Save up to <strong class="highlight">50%</strong> today only!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
|
||||||
|
|
||||||
|
const output = document.getElementById('test-output');
|
||||||
|
|
||||||
|
function runStructureTests() {
|
||||||
|
let results = '';
|
||||||
|
|
||||||
|
results += '🔍 TESTING STRUCTURE PRESERVATION\n';
|
||||||
|
results += '================================\n\n';
|
||||||
|
|
||||||
|
// Test Example 1: <p>Hello <strong class="emph">world</strong> and welcome!</p>
|
||||||
|
results += testStructurePreservation('example1', 'Example 1: Styled Strong Element');
|
||||||
|
|
||||||
|
// Test Example 2: <p>Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
|
||||||
|
results += testStructurePreservation('example2', 'Example 2: Styled Link Element');
|
||||||
|
|
||||||
|
// Test Example 3: <p>Save up to <strong class="highlight">50%</strong> today only!</p>
|
||||||
|
results += testStructurePreservation('example3', 'Example 3: Highlighted Text');
|
||||||
|
|
||||||
|
output.textContent = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testStructurePreservation(elementId, testName) {
|
||||||
|
let result = `📝 ${testName}\n`;
|
||||||
|
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
return result + `❌ Element ${elementId} not found\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `Original HTML: ${element.innerHTML}\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test our new structure-preserving detection
|
||||||
|
const detection = styleDetectionEngine.detectStylesAndStructure(element);
|
||||||
|
|
||||||
|
result += `\n🎨 Detected Styles (${detection.styles.size}):\n`;
|
||||||
|
for (const [id, style] of detection.styles) {
|
||||||
|
result += ` • ${style.name} (${id})\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `\n📍 Content Structure (${detection.structure.length} pieces):\n`;
|
||||||
|
detection.structure.forEach((piece, index) => {
|
||||||
|
if (piece.type === 'text') {
|
||||||
|
result += ` ${index}: TEXT: "${piece.content}"\n`;
|
||||||
|
} else if (piece.type === 'styled') {
|
||||||
|
result += ` ${index}: STYLED: "${piece.content}" (${piece.styleId})\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test reconstruction
|
||||||
|
const reconstructed = styleDetectionEngine.reconstructHTML(detection.structure, detection.styles);
|
||||||
|
result += `\n🔄 Reconstructed HTML: ${reconstructed}\n`;
|
||||||
|
|
||||||
|
// Test plain text extraction
|
||||||
|
const plainText = styleDetectionEngine.extractTextFromStructure(detection.structure);
|
||||||
|
result += `📝 Plain Text: "${plainText}"\n`;
|
||||||
|
|
||||||
|
// Verify structure preservation
|
||||||
|
const originalText = element.textContent;
|
||||||
|
const preservesText = plainText === originalText;
|
||||||
|
const preservesHTML = reconstructed === element.innerHTML;
|
||||||
|
|
||||||
|
result += `\n✅ Checks:\n`;
|
||||||
|
result += ` Text preservation: ${preservesText ? '✅' : '❌'}\n`;
|
||||||
|
result += ` HTML reconstruction: ${preservesHTML ? '✅' : '❌'}\n`;
|
||||||
|
|
||||||
|
if (preservesText && preservesHTML) {
|
||||||
|
result += ` 🎉 PERFECT STRUCTURE PRESERVATION!\n`;
|
||||||
|
} else {
|
||||||
|
result += ` ⚠️ Structure not perfectly preserved\n`;
|
||||||
|
if (!preservesText) {
|
||||||
|
result += ` Expected text: "${originalText}"\n`;
|
||||||
|
result += ` Got text: "${plainText}"\n`;
|
||||||
|
}
|
||||||
|
if (!preservesHTML) {
|
||||||
|
result += ` Expected HTML: ${element.innerHTML}\n`;
|
||||||
|
result += ` Got HTML: ${reconstructed}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
result += `❌ Error: ${error.message}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests when page loads
|
||||||
|
runStructureTests();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
174
test-style-system.html
Normal file
174
test-style-system.html
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Style Preservation System Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.test-output {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Monaco', 'Courier New', monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.test-controls {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #005a9e;
|
||||||
|
}
|
||||||
|
.emph { color: #dc2626; font-weight: 700; }
|
||||||
|
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
|
||||||
|
.highlight { background: #fef08a; padding: 0 0.25rem; border-radius: 3px; }
|
||||||
|
.brand { color: #059669; font-weight: 800; text-transform: uppercase; }
|
||||||
|
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧪 Style Preservation System Test</h1>
|
||||||
|
<p>This page tests our new StyleDetectionEngine and HTMLPreservationEngine with actual DOM elements.</p>
|
||||||
|
|
||||||
|
<div class="test-controls">
|
||||||
|
<button onclick="runStyleTests()">Test Style Detection</button>
|
||||||
|
<button onclick="runHTMLTests()">Test HTML Preservation</button>
|
||||||
|
<button onclick="runAllTests()">Run All Tests</button>
|
||||||
|
<button onclick="clearOutput()">Clear Output</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="test-output" class="test-output">Click a test button to run tests...</div>
|
||||||
|
|
||||||
|
<!-- Sample elements for testing (hidden) -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<p id="example1" class="insertr">Hello <strong class="emph">world</strong> and welcome!</p>
|
||||||
|
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
|
||||||
|
<p id="example4" class="insertr">Welcome to <strong class="brand">Acme Corp</strong> where we create <span class="highlight">innovative</span> solutions for <em class="emph">modern</em> businesses.</p>
|
||||||
|
<p id="example5" class="insertr">Ready to start? <button class="btn" data-action="signup" data-analytics="cta-main">Sign Up Now</button> and begin your journey!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// Import our style detection system
|
||||||
|
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
|
||||||
|
import { htmlPreservationEngine } from './lib/src/utils/html-preservation.js';
|
||||||
|
import { runStyleDetectionTests, runHTMLPreservationTests, runAllTests as runTestSuite } from './lib/src/utils/test-runner.js';
|
||||||
|
|
||||||
|
// Make functions available globally
|
||||||
|
window.styleDetectionEngine = styleDetectionEngine;
|
||||||
|
window.htmlPreservationEngine = htmlPreservationEngine;
|
||||||
|
|
||||||
|
// Store original console.log
|
||||||
|
const originalLog = console.log;
|
||||||
|
const output = document.getElementById('test-output');
|
||||||
|
|
||||||
|
// Override console.log to display in our output div
|
||||||
|
function captureOutput(callback) {
|
||||||
|
const logs = [];
|
||||||
|
console.log = (...args) => {
|
||||||
|
logs.push(args.join(' '));
|
||||||
|
originalLog(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = callback();
|
||||||
|
output.textContent = logs.join('\n');
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.runStyleTests = () => {
|
||||||
|
captureOutput(() => runStyleDetectionTests());
|
||||||
|
};
|
||||||
|
|
||||||
|
window.runHTMLTests = () => {
|
||||||
|
captureOutput(() => runHTMLPreservationTests());
|
||||||
|
};
|
||||||
|
|
||||||
|
window.runAllTests = () => {
|
||||||
|
captureOutput(() => runTestSuite());
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearOutput = () => {
|
||||||
|
output.textContent = 'Output cleared. Click a test button to run tests...';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manual testing functions
|
||||||
|
window.testStyleDetection = (elementId) => {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.log(`Element ${elementId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🔍 Testing element: ${elementId}`);
|
||||||
|
console.log(`HTML: ${element.outerHTML}`);
|
||||||
|
|
||||||
|
const styles = styleDetectionEngine.detectStyles(element);
|
||||||
|
console.log(`\nDetected ${styles.size} styles:`);
|
||||||
|
|
||||||
|
for (const [id, style] of styles) {
|
||||||
|
console.log(` • ${style.name} (${style.tagName}.${style.classes.join('.')})`);
|
||||||
|
console.log(` Template: ${style.template}`);
|
||||||
|
if (Object.keys(style.attributes).length > 0) {
|
||||||
|
console.log(` Attributes: ${JSON.stringify(style.attributes)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.testHTMLPreservation = (elementId) => {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.log(`Element ${elementId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🔧 Testing HTML preservation: ${elementId}`);
|
||||||
|
console.log(`Original: ${element.outerHTML}`);
|
||||||
|
|
||||||
|
// Extract content
|
||||||
|
const extracted = htmlPreservationEngine.extractForEditing(element);
|
||||||
|
console.log(`\nExtracted HTML: ${extracted.html}`);
|
||||||
|
console.log(`Extracted Text: ${extracted.text}`);
|
||||||
|
console.log(`Container Attributes: ${JSON.stringify(extracted.containerAttributes)}`);
|
||||||
|
console.log(`Has Nested Elements: ${extracted.hasNestedElements}`);
|
||||||
|
|
||||||
|
// Test applying content
|
||||||
|
const testContent = 'Modified <strong class="emph">content</strong> for testing!';
|
||||||
|
const success = htmlPreservationEngine.applyFromEditing(element, testContent);
|
||||||
|
console.log(`\nApplication Success: ${success}`);
|
||||||
|
console.log(`Updated HTML: ${element.outerHTML}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show initial instructions
|
||||||
|
output.textContent = `🧪 Style Preservation System Test Ready!
|
||||||
|
|
||||||
|
Available manual tests:
|
||||||
|
• testStyleDetection('example1') - Test style detection on Example 1
|
||||||
|
• testStyleDetection('example2') - Test style detection on Example 2
|
||||||
|
• testHTMLPreservation('example1') - Test HTML preservation
|
||||||
|
• runStyleTests() - Run all style detection tests
|
||||||
|
• runHTMLTests() - Run all HTML preservation tests
|
||||||
|
• runAllTests() - Run complete test suite
|
||||||
|
|
||||||
|
Click the buttons above or open browser console and run these commands manually.`;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user