refactor: Replace multi-property forms with popup-based link configuration

- Remove clunky multi-property editor with separate text + URL inputs
- Implement single rich text editor (contentEditable) for all content
- Add popup-based link configuration: select text → click 🔗 Link → configure
- Filter out link styles from formatting toolbar (links use popup, not buttons)
- Consolidate CSS: remove separate style-aware-editor.css, integrate into insertr.css
- Clean up 200+ lines of unused multi-property form code and styles
- Fix duplicate link style detection (no more 'Fancy Link' + 'Link' buttons)

Result: Much cleaner UX similar to modern editors where formatting uses
toolbar buttons and complex elements (links) use dedicated popups.
This commit is contained in:
2025-09-19 20:16:33 +02:00
parent bf2927fec2
commit d317e2e1d9
4 changed files with 481 additions and 939 deletions

View File

@@ -321,88 +321,105 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
}
/* =================================================================
EDIT FORM STYLES
STYLE-AWARE EDITOR STYLES
Modern interface for content editing with style preservation
================================================================= */
.insertr-edit-form {
padding: var(--insertr-spacing-lg);
margin: 0;
font-family: var(--insertr-font-family);
font-size: var(--insertr-font-size-base);
line-height: var(--insertr-line-height);
color: var(--insertr-text-primary);
/* Main editor container */
.insertr-style-aware-editor {
background: var(--insertr-bg-primary);
}
.insertr-form-header {
margin: 0 0 var(--insertr-spacing-lg) 0;
padding: 0 0 var(--insertr-spacing-md) 0;
border-bottom: 1px solid var(--insertr-border-color);
}
.insertr-form-title {
margin: 0 0 var(--insertr-spacing-sm) 0;
padding: 0;
font-size: var(--insertr-font-size-lg);
font-weight: 600;
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);
}
.insertr-form-help {
margin: 0;
padding: 0;
/* Style toolbar */
.insertr-style-toolbar {
display: flex;
align-items: center;
gap: var(--insertr-spacing-xs);
padding: var(--insertr-spacing-sm);
background: var(--insertr-bg-secondary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
margin-bottom: var(--insertr-spacing-md);
flex-wrap: wrap;
}
.insertr-toolbar-title {
font-size: var(--insertr-font-size-sm);
font-weight: 500;
color: var(--insertr-text-muted);
margin-right: var(--insertr-spacing-xs);
}
.insertr-form-body {
margin: 0 0 var(--insertr-spacing-lg) 0;
padding: 0;
}
.insertr-form-group {
margin: 0 0 var(--insertr-spacing-md) 0;
padding: 0;
}
.insertr-form-label {
display: block;
margin: 0 0 var(--insertr-spacing-xs) 0;
padding: 0;
.insertr-style-btn {
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
font-size: var(--insertr-font-size-sm);
font-weight: 500;
color: var(--insertr-text-primary);
cursor: pointer;
transition: var(--insertr-transition);
}
.insertr-form-input,
.insertr-form-textarea,
.insertr-form-select {
.insertr-style-btn:hover {
background: var(--insertr-bg-secondary);
border-color: var(--insertr-text-muted);
}
.insertr-style-btn:active {
background: var(--insertr-border-color);
transform: translateY(1px);
}
/* Editor components */
.insertr-simple-editor,
.insertr-rich-editor,
.insertr-fallback-textarea {
width: 100%;
padding: var(--insertr-input-padding);
margin: 0;
border: var(--insertr-input-border);
border-radius: var(--insertr-border-radius);
background: var(--insertr-input-bg);
font-family: var(--insertr-font-family);
padding: var(--insertr-input-padding);
font-size: var(--insertr-font-size-base);
line-height: var(--insertr-line-height);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
background: var(--insertr-input-bg);
margin-bottom: var(--insertr-spacing-md);
transition: var(--insertr-transition);
box-sizing: border-box;
}
.insertr-form-input:focus,
.insertr-form-textarea:focus,
.insertr-form-select:focus {
.insertr-simple-editor:focus,
.insertr-rich-editor:focus,
.insertr-fallback-textarea:focus {
outline: none;
border: var(--insertr-input-border-focus);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.insertr-form-textarea {
min-height: 120px;
.insertr-simple-editor,
.insertr-fallback-textarea {
resize: vertical;
min-height: 120px;
}
.insertr-rich-editor {
min-height: 100px;
overflow-y: auto;
}
/* Form actions */
.insertr-form-actions {
display: flex;
gap: var(--insertr-spacing-sm);
@@ -466,6 +483,19 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
outline-offset: 2px;
}
/* Fallback editor */
.insertr-fallback-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: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
}
/* =================================================================
VERSION HISTORY MODAL
================================================================= */
@@ -694,10 +724,33 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
top: -25px;
}
/* StyleAware Editor responsive adjustments */
.insertr-style-aware-editor,
.insertr-fallback-editor {
min-width: 300px;
max-width: calc(100vw - 2rem);
padding: var(--insertr-spacing-md);
}
.insertr-style-toolbar {
padding: var(--insertr-spacing-xs);
gap: var(--insertr-spacing-xs);
}
.insertr-style-btn {
padding: var(--insertr-spacing-xs);
font-size: var(--insertr-font-size-sm);
}
.insertr-form-actions {
flex-direction: column;
}
.insertr-btn-save,
.insertr-btn-cancel {
width: 100%;
}
.insertr-btn-history {
margin-right: 0;
order: -1;

View File

@@ -321,88 +321,105 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
}
/* =================================================================
EDIT FORM STYLES
STYLE-AWARE EDITOR STYLES
Modern interface for content editing with style preservation
================================================================= */
.insertr-edit-form {
padding: var(--insertr-spacing-lg);
margin: 0;
font-family: var(--insertr-font-family);
font-size: var(--insertr-font-size-base);
line-height: var(--insertr-line-height);
color: var(--insertr-text-primary);
/* Main editor container */
.insertr-style-aware-editor {
background: var(--insertr-bg-primary);
}
.insertr-form-header {
margin: 0 0 var(--insertr-spacing-lg) 0;
padding: 0 0 var(--insertr-spacing-md) 0;
border-bottom: 1px solid var(--insertr-border-color);
}
.insertr-form-title {
margin: 0 0 var(--insertr-spacing-sm) 0;
padding: 0;
font-size: var(--insertr-font-size-lg);
font-weight: 600;
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);
}
.insertr-form-help {
margin: 0;
padding: 0;
/* Style toolbar */
.insertr-style-toolbar {
display: flex;
align-items: center;
gap: var(--insertr-spacing-xs);
padding: var(--insertr-spacing-sm);
background: var(--insertr-bg-secondary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
margin-bottom: var(--insertr-spacing-md);
flex-wrap: wrap;
}
.insertr-toolbar-title {
font-size: var(--insertr-font-size-sm);
font-weight: 500;
color: var(--insertr-text-muted);
margin-right: var(--insertr-spacing-xs);
}
.insertr-form-body {
margin: 0 0 var(--insertr-spacing-lg) 0;
padding: 0;
}
.insertr-form-group {
margin: 0 0 var(--insertr-spacing-md) 0;
padding: 0;
}
.insertr-form-label {
display: block;
margin: 0 0 var(--insertr-spacing-xs) 0;
padding: 0;
.insertr-style-btn {
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
font-size: var(--insertr-font-size-sm);
font-weight: 500;
color: var(--insertr-text-primary);
cursor: pointer;
transition: var(--insertr-transition);
}
.insertr-form-input,
.insertr-form-textarea,
.insertr-form-select {
.insertr-style-btn:hover {
background: var(--insertr-bg-secondary);
border-color: var(--insertr-text-muted);
}
.insertr-style-btn:active {
background: var(--insertr-border-color);
transform: translateY(1px);
}
/* Editor components */
.insertr-simple-editor,
.insertr-rich-editor,
.insertr-fallback-textarea {
width: 100%;
padding: var(--insertr-input-padding);
margin: 0;
border: var(--insertr-input-border);
border-radius: var(--insertr-border-radius);
background: var(--insertr-input-bg);
font-family: var(--insertr-font-family);
padding: var(--insertr-input-padding);
font-size: var(--insertr-font-size-base);
line-height: var(--insertr-line-height);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
background: var(--insertr-input-bg);
margin-bottom: var(--insertr-spacing-md);
transition: var(--insertr-transition);
box-sizing: border-box;
}
.insertr-form-input:focus,
.insertr-form-textarea:focus,
.insertr-form-select:focus {
.insertr-simple-editor:focus,
.insertr-rich-editor:focus,
.insertr-fallback-textarea:focus {
outline: none;
border: var(--insertr-input-border-focus);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.insertr-form-textarea {
min-height: 120px;
.insertr-simple-editor,
.insertr-fallback-textarea {
resize: vertical;
min-height: 120px;
}
.insertr-rich-editor {
min-height: 100px;
overflow-y: auto;
}
/* Form actions */
.insertr-form-actions {
display: flex;
gap: var(--insertr-spacing-sm);
@@ -466,6 +483,19 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
outline-offset: 2px;
}
/* Fallback editor */
.insertr-fallback-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: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
}
/* =================================================================
VERSION HISTORY MODAL
================================================================= */
@@ -694,318 +724,35 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
top: -25px;
}
/* StyleAware Editor responsive adjustments */
.insertr-style-aware-editor,
.insertr-fallback-editor {
min-width: 300px;
max-width: calc(100vw - 2rem);
padding: var(--insertr-spacing-md);
}
.insertr-style-toolbar {
padding: var(--insertr-spacing-xs);
gap: var(--insertr-spacing-xs);
}
.insertr-style-btn {
padding: var(--insertr-spacing-xs);
font-size: var(--insertr-font-size-sm);
}
.insertr-form-actions {
flex-direction: column;
}
.insertr-btn-save,
.insertr-btn-cancel {
width: 100%;
}
.insertr-btn-history {
margin-right: 0;
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;
}
}

View File

@@ -1,307 +0,0 @@
/**
* 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;
}
}

View File

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