Implement professional smart formatting with toggle logic and whitespace preservation
This commit is contained in:
@@ -250,8 +250,9 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
if siteID == "" {
|
if siteID == "" {
|
||||||
siteID = req.SiteID // fallback to request body
|
siteID = req.SiteID // fallback to request body
|
||||||
}
|
}
|
||||||
if siteID == "" {
|
if siteID == "" || siteID == "__MISSING_SITE_ID__" {
|
||||||
siteID = "default" // final fallback
|
http.Error(w, "site_id parameter is required and must be configured", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate content ID using the unified engine
|
// Generate content ID using the unified engine
|
||||||
|
|||||||
@@ -442,6 +442,76 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
|||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active/applied formatting state - shows current selection has this formatting */
|
||||||
|
.insertr-style-btn.insertr-style-active {
|
||||||
|
background: var(--insertr-primary);
|
||||||
|
border-color: var(--insertr-primary);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-style-active .insertr-preview-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-style-active:hover {
|
||||||
|
background: var(--insertr-primary-hover);
|
||||||
|
border-color: var(--insertr-primary-hover);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state for default style buttons */
|
||||||
|
.insertr-style-btn.insertr-default-style.insertr-style-active {
|
||||||
|
background: var(--insertr-info);
|
||||||
|
border-color: var(--insertr-info);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-default-style.insertr-style-active .insertr-preview-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default formatting style buttons */
|
||||||
|
.insertr-style-btn.insertr-default-style {
|
||||||
|
border-color: var(--insertr-info);
|
||||||
|
background: rgba(23, 162, 184, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-default-style:hover {
|
||||||
|
border-color: var(--insertr-info);
|
||||||
|
background: rgba(23, 162, 184, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-default-style:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 2px rgba(23, 162, 184, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default preview content styling */
|
||||||
|
.insertr-default-preview {
|
||||||
|
font-size: var(--insertr-font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small indicator for default styles */
|
||||||
|
.insertr-style-btn.insertr-default-style::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--insertr-info);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
/* Editor components */
|
/* Editor components */
|
||||||
.insertr-simple-editor,
|
.insertr-simple-editor,
|
||||||
.insertr-rich-editor,
|
.insertr-rich-editor,
|
||||||
@@ -479,7 +549,126 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =================================================================
|
||||||
|
MULTI-PROPERTY FORM COMPONENTS
|
||||||
|
Professional form styling for direct editors (links, buttons, images)
|
||||||
|
================================================================= */
|
||||||
|
|
||||||
|
/* Direct editor container */
|
||||||
|
.insertr-direct-editor {
|
||||||
|
background: var(--insertr-bg-primary);
|
||||||
|
border: 1px solid var(--insertr-border-color);
|
||||||
|
border-radius: var(--insertr-border-radius);
|
||||||
|
padding: var(--insertr-spacing-lg);
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: var(--insertr-font-family);
|
||||||
|
color: var(--insertr-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form groups */
|
||||||
|
.insertr-form-group {
|
||||||
|
margin-bottom: var(--insertr-spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form labels */
|
||||||
|
.insertr-form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--insertr-spacing-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--insertr-font-size-sm);
|
||||||
|
color: var(--insertr-text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form inputs and selects */
|
||||||
|
.insertr-form-input,
|
||||||
|
.insertr-form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--insertr-spacing-sm) var(--insertr-spacing-md);
|
||||||
|
border: 1px solid var(--insertr-border-color);
|
||||||
|
border-radius: var(--insertr-border-radius);
|
||||||
|
font-size: var(--insertr-font-size-base);
|
||||||
|
font-family: var(--insertr-font-family);
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--insertr-text-primary);
|
||||||
|
background: var(--insertr-bg-primary);
|
||||||
|
transition: var(--insertr-transition);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus states */
|
||||||
|
.insertr-form-input:focus,
|
||||||
|
.insertr-form-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--insertr-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover states for selects */
|
||||||
|
.insertr-form-select:hover {
|
||||||
|
border-color: var(--insertr-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled states */
|
||||||
|
.insertr-form-input:disabled,
|
||||||
|
.insertr-form-select:disabled {
|
||||||
|
background: var(--insertr-bg-secondary);
|
||||||
|
color: var(--insertr-text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error states */
|
||||||
|
.insertr-form-input.insertr-error,
|
||||||
|
.insertr-form-select.insertr-error {
|
||||||
|
border-color: var(--insertr-danger);
|
||||||
|
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success states */
|
||||||
|
.insertr-form-input.insertr-success,
|
||||||
|
.insertr-form-select.insertr-success {
|
||||||
|
border-color: var(--insertr-success);
|
||||||
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form validation messages */
|
||||||
|
.insertr-form-message {
|
||||||
|
margin-top: var(--insertr-spacing-xs);
|
||||||
|
font-size: var(--insertr-font-size-sm);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-message.insertr-error {
|
||||||
|
color: var(--insertr-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-message.insertr-success {
|
||||||
|
color: var(--insertr-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-form-message.insertr-info {
|
||||||
|
color: var(--insertr-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific editor variants */
|
||||||
|
.insertr-link-editor {
|
||||||
|
/* Link-specific styling if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-button-editor {
|
||||||
|
/* Button-specific styling if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-image-editor {
|
||||||
|
/* Image-specific styling if needed */
|
||||||
|
}
|
||||||
|
|
||||||
/* Form actions */
|
/* Form actions */
|
||||||
.insertr-form-actions {
|
.insertr-form-actions {
|
||||||
|
|||||||
@@ -29,12 +29,11 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async createContent(content, type, htmlMarkup) {
|
async createContent(content, htmlMarkup) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
|
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
|
||||||
html_content: content,
|
html_content: content,
|
||||||
type: type,
|
|
||||||
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ export class ApiClient {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
console.log(`✅ Content created: ${result.id}`);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
|
console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
|
||||||
|
|||||||
@@ -131,10 +131,8 @@ export class InsertrEditor {
|
|||||||
result = await this.apiClient.updateContent(meta.contentId, contentValue);
|
result = await this.apiClient.updateContent(meta.contentId, contentValue);
|
||||||
} else {
|
} else {
|
||||||
// Create new content - server handles ID extraction/generation from markup
|
// Create new content - server handles ID extraction/generation from markup
|
||||||
const contentType = this.determineContentType(meta.element);
|
|
||||||
result = await this.apiClient.createContent(
|
result = await this.apiClient.createContent(
|
||||||
contentValue,
|
contentValue,
|
||||||
contentType,
|
|
||||||
meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
|
meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -142,7 +140,6 @@ export class InsertrEditor {
|
|||||||
if (result) {
|
if (result) {
|
||||||
// Store the backend-generated/confirmed ID in the element
|
// Store the backend-generated/confirmed ID in the element
|
||||||
meta.element.setAttribute('data-content-id', result.id);
|
meta.element.setAttribute('data-content-id', result.id);
|
||||||
meta.element.setAttribute('data-content-type', result.type);
|
|
||||||
console.log(`✅ Content saved: ${result.id}`, contentValue);
|
console.log(`✅ Content saved: ${result.id}`, contentValue);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ Failed to save content to server');
|
console.error('❌ Failed to save content to server');
|
||||||
@@ -157,16 +154,7 @@ export class InsertrEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
determineContentType(element) {
|
|
||||||
const tagName = element.tagName.toLowerCase();
|
|
||||||
|
|
||||||
if (tagName === 'a' || tagName === 'button') {
|
|
||||||
return 'link';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALL text elements use text content type
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel(meta) {
|
handleCancel(meta) {
|
||||||
console.log('❌ Edit cancelled:', meta.contentId);
|
console.log('❌ Edit cancelled:', meta.contentId);
|
||||||
|
|||||||
@@ -9,29 +9,23 @@ export class InsertrCore {
|
|||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all enhanced elements on the page
|
// Find all enhanced elements on the page
|
||||||
// Note: Container expansion is handled at build-time by the backend enhancer
|
|
||||||
// Frontend should only find elements that already have .insertr class
|
|
||||||
findEnhancedElements() {
|
findEnhancedElements() {
|
||||||
return document.querySelectorAll('.insertr');
|
return document.querySelectorAll('.insertr');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: All container expansion logic removed - handled by backend enhancer
|
|
||||||
// Frontend only works with elements that already have .insertr class
|
|
||||||
|
|
||||||
// Get element metadata
|
// Get element metadata
|
||||||
getElementMetadata(element) {
|
getElementMetadata(element) {
|
||||||
const existingId = element.getAttribute('data-content-id');
|
const existingId = element.getAttribute('data-content-id');
|
||||||
|
|
||||||
// HTML-first approach: no content type needed, just HTML markup for ID generation
|
|
||||||
return {
|
return {
|
||||||
contentId: existingId, // null if new content, existing ID if updating
|
contentId: existingId,
|
||||||
element: element,
|
element: element,
|
||||||
htmlMarkup: element.outerHTML // Server will generate ID from this
|
htmlMarkup: element.outerHTML // Server will generate ID from this
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current file path from URL for consistent ID generation
|
// Get current file path from URL for consistent ID generation
|
||||||
getCurrentFilePath() {
|
getCurrentFilePath() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@@ -41,11 +35,10 @@ export class InsertrCore {
|
|||||||
// Remove leading slash: "/about.html" → "about.html"
|
// Remove leading slash: "/about.html" → "about.html"
|
||||||
return path.replace(/^\//, '');
|
return path.replace(/^\//, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all elements with their metadata
|
// Get all elements with their metadata
|
||||||
// Note: Container expansion handled by backend - frontend finds enhanced elements only
|
|
||||||
getAllElements() {
|
getAllElements() {
|
||||||
const elements = this.findEnhancedElements();
|
const elements = this.findEnhancedElements();
|
||||||
return Array.from(elements).map(el => this.getElementMetadata(el));
|
return Array.from(elements).map(el => this.getElementMetadata(el));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ window.Insertr = {
|
|||||||
editor: null,
|
editor: null,
|
||||||
auth: null,
|
auth: null,
|
||||||
apiClient: null,
|
apiClient: null,
|
||||||
|
|
||||||
// UI layer (presentation)
|
// UI layer (presentation)
|
||||||
controlPanel: null,
|
controlPanel: null,
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ window.Insertr = {
|
|||||||
this.auth = new InsertrAuth(options);
|
this.auth = new InsertrAuth(options);
|
||||||
this.apiClient = new ApiClient(options);
|
this.apiClient = new ApiClient(options);
|
||||||
this.editor = new InsertrEditor(this.core, this.auth, this.apiClient, options);
|
this.editor = new InsertrEditor(this.core, this.auth, this.apiClient, options);
|
||||||
|
|
||||||
// Initialize UI layer
|
// Initialize UI layer
|
||||||
this.controlPanel = new InsertrControlPanel(this.auth, this.editor, this.apiClient, options);
|
this.controlPanel = new InsertrControlPanel(this.auth, this.editor, this.apiClient, options);
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ window.Insertr = {
|
|||||||
link.href = cssPath;
|
link.href = cssPath;
|
||||||
link.onload = () => console.log('✅ Insertr CSS loaded');
|
link.onload = () => console.log('✅ Insertr CSS loaded');
|
||||||
link.onerror = () => console.warn('⚠️ Failed to load Insertr CSS from:', cssPath);
|
link.onerror = () => console.warn('⚠️ Failed to load Insertr CSS from:', cssPath);
|
||||||
|
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -83,9 +83,6 @@ window.Insertr = {
|
|||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.auth.init(); // Sets up invisible editor gates only
|
this.auth.init(); // Sets up invisible editor gates only
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Control panel is NOT created here - only after gate activation
|
|
||||||
// Note: Editor is NOT started here - only when authentication succeeds
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Start the full editor system (called when gate is activated)
|
// Start the full editor system (called when gate is activated)
|
||||||
@@ -94,12 +91,12 @@ window.Insertr = {
|
|||||||
if (this.controlPanel && !this.controlPanel.isInitialized) {
|
if (this.controlPanel && !this.controlPanel.isInitialized) {
|
||||||
this.controlPanel.init(); // Creates unified control panel UI
|
this.controlPanel.init(); // Creates unified control panel UI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start editor functionality
|
// Start editor functionality
|
||||||
if (this.editor && !this.editor.isActive) {
|
if (this.editor && !this.editor.isActive) {
|
||||||
this.editor.start();
|
this.editor.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add editing indicators when editor starts
|
// Add editing indicators when editor starts
|
||||||
if (this.controlPanel) {
|
if (this.controlPanel) {
|
||||||
this.controlPanel.addEditingIndicators();
|
this.controlPanel.addEditingIndicators();
|
||||||
@@ -138,7 +135,7 @@ function autoInitialize() {
|
|||||||
// Check for configuration from script data attributes
|
// Check for configuration from script data attributes
|
||||||
const insertrScript = document.querySelector('script[data-insertr-injected]');
|
const insertrScript = document.querySelector('script[data-insertr-injected]');
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
if (insertrScript) {
|
if (insertrScript) {
|
||||||
config.siteId = insertrScript.getAttribute('data-site-id'); // No fallback - let ApiClient handle missing values
|
config.siteId = insertrScript.getAttribute('data-site-id'); // No fallback - let ApiClient handle missing values
|
||||||
config.apiEndpoint = insertrScript.getAttribute('data-api-endpoint') || '/api/content';
|
config.apiEndpoint = insertrScript.getAttribute('data-api-endpoint') || '/api/content';
|
||||||
@@ -146,7 +143,7 @@ function autoInitialize() {
|
|||||||
config.mockAuth = config.authProvider === 'mock'; // Set mockAuth based on provider
|
config.mockAuth = config.authProvider === 'mock'; // Set mockAuth based on provider
|
||||||
config.debug = insertrScript.getAttribute('data-debug') === 'true';
|
config.debug = insertrScript.getAttribute('data-debug') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Insertr.init(config);
|
window.Insertr.init(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,6 +442,36 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
|||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active/applied formatting state - shows current selection has this formatting */
|
||||||
|
.insertr-style-btn.insertr-style-active {
|
||||||
|
background: var(--insertr-primary);
|
||||||
|
border-color: var(--insertr-primary);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-style-active .insertr-preview-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-style-active:hover {
|
||||||
|
background: var(--insertr-primary-hover);
|
||||||
|
border-color: var(--insertr-primary-hover);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state for default style buttons */
|
||||||
|
.insertr-style-btn.insertr-default-style.insertr-style-active {
|
||||||
|
background: var(--insertr-info);
|
||||||
|
border-color: var(--insertr-info);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insertr-style-btn.insertr-default-style.insertr-style-active .insertr-preview-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* Default formatting style buttons */
|
/* Default formatting style buttons */
|
||||||
.insertr-style-btn.insertr-default-style {
|
.insertr-style-btn.insertr-default-style {
|
||||||
border-color: var(--insertr-info);
|
border-color: var(--insertr-info);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user