Files
insertr/insertr-cli/pkg/content/assets/insertr.js
Joakim 6fef293df3 feat: implement flexible editor gate system
- Replace automatic auth controls with developer-placed .insertr-gate elements
- Add OAuth-ready authentication flow with mock implementation
- Support any HTML element as gate with custom styling
- Implement proper gate restoration after authentication
- Move auth controls to bottom-right corner for better UX
- Add editor gates to demo pages (footer link and styled button)
- Maintain gates visible by default with hideGatesAfterAuth option
- Prevent duplicate authentication attempts with loading states

This enables small business owners to access editor via discrete
footer links or custom-styled elements placed anywhere by developers.
2025-09-04 18:42:30 +02:00

1287 lines
43 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var Insertr = (function () {
'use strict';
/**
* InsertrCore - Core functionality for content management
*/
class InsertrCore {
constructor(options = {}) {
this.options = {
apiEndpoint: options.apiEndpoint || '/api/content',
siteId: options.siteId || 'default',
...options
};
}
// Find all enhanced elements on the page
findEnhancedElements() {
return document.querySelectorAll('[data-insertr-enhanced="true"]');
}
// Get element metadata
getElementMetadata(element) {
return {
contentId: element.getAttribute('data-content-id'),
contentType: element.getAttribute('data-content-type'),
element: element
};
}
// Get all elements with their metadata
getAllElements() {
const elements = this.findEnhancedElements();
return Array.from(elements).map(el => this.getElementMetadata(el));
}
}
/**
* InsertrFormRenderer - Professional modal editing forms
* Ported from prototype with modern ES6+ architecture
*/
class InsertrFormRenderer {
constructor() {
this.currentOverlay = null;
this.setupStyles();
}
/**
* Create and show edit form for content element
* @param {Object} meta - Element metadata {element, contentId, contentType}
* @param {string} currentContent - Current content value
* @param {Function} onSave - Save callback
* @param {Function} onCancel - Cancel callback
*/
showEditForm(meta, currentContent, onSave, onCancel) {
// Close any existing form
this.closeForm();
const { element, contentId, contentType } = meta;
const config = this.getFieldConfig(element, contentType);
// Create form
const form = this.createEditForm(contentId, config, currentContent);
// Create overlay with backdrop
const overlay = this.createOverlay(form);
// Position form
this.positionForm(element, overlay);
// Setup event handlers
this.setupFormHandlers(form, overlay, { onSave, onCancel });
// Show form
document.body.appendChild(overlay);
this.currentOverlay = overlay;
// Focus first input
const firstInput = form.querySelector('input, textarea');
if (firstInput) {
setTimeout(() => firstInput.focus(), 100);
}
return overlay;
}
/**
* Close current form
*/
closeForm() {
if (this.currentOverlay) {
this.currentOverlay.remove();
this.currentOverlay = null;
}
}
/**
* Generate field configuration based on element
*/
getFieldConfig(element, contentType) {
const tagName = element.tagName.toLowerCase();
const classList = Array.from(element.classList);
// Default configurations based on element type
const configs = {
h1: { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' },
h2: { type: 'text', label: 'Subheading', maxLength: 80, placeholder: 'Enter subheading...' },
h3: { type: 'text', label: 'Section Title', maxLength: 100, placeholder: 'Enter title...' },
h4: { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
h5: { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
h6: { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
p: { type: 'textarea', label: 'Paragraph', rows: 3, placeholder: 'Enter paragraph text...' },
a: { type: 'link', label: 'Link', placeholder: 'Enter link text...', includeUrl: true },
span: { type: 'text', label: 'Text', placeholder: 'Enter text...' },
button: { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' },
};
let config = configs[tagName] || { type: 'text', label: 'Text', placeholder: 'Enter text...' };
// CSS class enhancements
if (classList.includes('lead')) {
config = { ...config, label: 'Lead Paragraph', rows: 4, placeholder: 'Enter lead paragraph...' };
}
// Override with contentType from CLI if specified
if (contentType === 'markdown') {
config = { ...config, type: 'markdown', label: 'Markdown Content', rows: 8 };
}
return config;
}
/**
* Create form HTML structure
*/
createEditForm(contentId, config, currentContent) {
const form = document.createElement('div');
form.className = 'insertr-edit-form';
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (config.type === 'markdown') {
formHTML += this.createMarkdownField(config, currentContent);
} else if (config.type === 'link' && config.includeUrl) {
formHTML += this.createLinkField(config, currentContent);
} else if (config.type === 'textarea') {
formHTML += this.createTextareaField(config, currentContent);
} else {
formHTML += this.createTextField(config, currentContent);
}
// Form buttons
formHTML += `
<div class="insertr-form-actions">
<button type="button" class="insertr-btn-save">Save</button>
<button type="button" class="insertr-btn-cancel">Cancel</button>
</div>
`;
form.innerHTML = formHTML;
return form;
}
/**
* Create markdown field with preview
*/
createMarkdownField(config, currentContent) {
return `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
rows="${config.rows || 8}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
<div class="insertr-form-help">
Supports Markdown formatting (bold, italic, links, etc.)
</div>
</div>
`;
}
/**
* Create link field (text + URL)
*/
createLinkField(config, currentContent) {
const linkText = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
const linkUrl = typeof currentContent === 'object' ? currentContent.url || '' : '';
return `
<div class="insertr-form-group">
<label class="insertr-form-label">Link Text:</label>
<input type="text" class="insertr-form-input" name="text"
value="${this.escapeHtml(linkText)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 200}">
</div>
<div class="insertr-form-group">
<label class="insertr-form-label">Link URL:</label>
<input type="url" class="insertr-form-input" name="url"
value="${this.escapeHtml(linkUrl)}"
placeholder="https://example.com">
</div>
`;
}
/**
* Create textarea field
*/
createTextareaField(config, currentContent) {
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
return `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea" name="content"
rows="${config.rows || 3}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 1000}">${this.escapeHtml(content)}</textarea>
</div>
`;
}
/**
* Create text input field
*/
createTextField(config, currentContent) {
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
return `
<div class="insertr-form-group">
<input type="text" class="insertr-form-input" name="content"
value="${this.escapeHtml(content)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 200}">
</div>
`;
}
/**
* Create overlay with backdrop
*/
createOverlay(form) {
const overlay = document.createElement('div');
overlay.className = 'insertr-form-overlay';
overlay.appendChild(form);
return overlay;
}
/**
* Position form relative to element
*/
positionForm(element, overlay) {
const rect = element.getBoundingClientRect();
const form = overlay.querySelector('.insertr-edit-form');
// Calculate optimal width (responsive)
const viewportWidth = window.innerWidth;
let formWidth;
if (viewportWidth < 768) {
formWidth = Math.min(viewportWidth - 40, 350);
} else {
formWidth = Math.min(Math.max(rect.width, 300), 500);
}
form.style.width = `${formWidth}px`;
// Position below element with some spacing
const top = rect.bottom + window.scrollY + 10;
const left = Math.max(20, rect.left + window.scrollX);
overlay.style.position = 'absolute';
overlay.style.top = `${top}px`;
overlay.style.left = `${left}px`;
overlay.style.zIndex = '10000';
}
/**
* Setup form event handlers
*/
setupFormHandlers(form, overlay, { onSave, onCancel }) {
const saveBtn = form.querySelector('.insertr-btn-save');
const cancelBtn = form.querySelector('.insertr-btn-cancel');
if (saveBtn) {
saveBtn.addEventListener('click', () => {
const formData = this.extractFormData(form);
onSave(formData);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
onCancel();
this.closeForm();
});
}
// ESC key to cancel
const keyHandler = (e) => {
if (e.key === 'Escape') {
onCancel();
this.closeForm();
document.removeEventListener('keydown', keyHandler);
}
};
document.addEventListener('keydown', keyHandler);
// Click outside to cancel
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
onCancel();
this.closeForm();
}
});
}
/**
* Extract form data
*/
extractFormData(form) {
const data = {};
// Handle different field types
const textInput = form.querySelector('input[name="text"]');
const urlInput = form.querySelector('input[name="url"]');
const contentInput = form.querySelector('input[name="content"], textarea[name="content"]');
if (textInput && urlInput) {
// Link field
data.text = textInput.value;
data.url = urlInput.value;
} else if (contentInput) {
// Text or textarea field
data.text = contentInput.value;
}
return data;
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
if (typeof text !== 'string') return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Setup form styles
*/
setupStyles() {
const styles = `
.insertr-form-overlay {
position: absolute;
z-index: 10000;
}
.insertr-edit-form {
background: white;
border: 2px solid #007cba;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
width: 100%;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.insertr-form-header {
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.insertr-form-group {
margin-bottom: 1rem;
}
.insertr-form-group:last-child {
margin-bottom: 0;
}
.insertr-form-label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.insertr-form-input,
.insertr-form-textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-family: inherit;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.insertr-form-input:focus,
.insertr-form-textarea:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1);
}
.insertr-form-textarea {
min-height: 120px;
resize: vertical;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.insertr-markdown-editor {
min-height: 200px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.9rem;
line-height: 1.5;
background-color: #f8fafc;
}
.insertr-form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.insertr-btn-save {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.875rem;
}
.insertr-btn-save:hover {
background: #059669;
}
.insertr-btn-cancel {
background: #6b7280;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.875rem;
}
.insertr-btn-cancel:hover {
background: #4b5563;
}
.insertr-form-help {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
`;
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = styles;
document.head.appendChild(styleSheet);
}
}
/**
* InsertrEditor - Visual editing functionality
*/
class InsertrEditor {
constructor(core, auth, options = {}) {
this.core = core;
this.auth = auth;
this.options = options;
this.isActive = false;
this.formRenderer = new InsertrFormRenderer();
}
start() {
if (this.isActive) return;
console.log('🚀 Starting Insertr Editor');
this.isActive = true;
// Add editor styles
this.addEditorStyles();
// Initialize all enhanced elements
const elements = this.core.getAllElements();
console.log(`📝 Found ${elements.length} editable elements`);
elements.forEach(meta => this.initializeElement(meta));
}
initializeElement(meta) {
const { element, contentId, contentType } = meta;
// Add visual indicators
element.style.cursor = 'pointer';
element.style.position = 'relative';
// Add interaction handlers
this.addHoverEffects(element);
this.addClickHandler(element, meta);
}
addHoverEffects(element) {
element.addEventListener('mouseenter', () => {
element.classList.add('insertr-editing-hover');
});
element.addEventListener('mouseleave', () => {
element.classList.remove('insertr-editing-hover');
});
}
addClickHandler(element, meta) {
element.addEventListener('click', (e) => {
// Only allow editing if authenticated and in edit mode
if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) {
return; // Let normal click behavior happen
}
e.preventDefault();
this.openEditor(meta);
});
}
openEditor(meta) {
const { element } = meta;
const currentContent = this.extractCurrentContent(element);
// Show professional form instead of prompt
this.formRenderer.showEditForm(
meta,
currentContent,
(formData) => this.handleSave(meta, formData),
() => this.handleCancel(meta)
);
}
extractCurrentContent(element) {
// For links, extract both text and URL
if (element.tagName.toLowerCase() === 'a') {
return {
text: element.textContent.trim(),
url: element.getAttribute('href') || ''
};
}
// For other elements, just return text content
return element.textContent.trim();
}
handleSave(meta, formData) {
console.log('💾 Saving content:', meta.contentId, formData);
// Update element content based on type
this.updateElementContent(meta.element, formData);
// Close form
this.formRenderer.closeForm();
// TODO: Save to backend API
console.log(`✅ Content saved:`, meta.contentId, formData);
}
handleCancel(meta) {
console.log('❌ Edit cancelled:', meta.contentId);
}
updateElementContent(element, formData) {
if (element.tagName.toLowerCase() === 'a') {
// Update link element
if (formData.text !== undefined) {
element.textContent = formData.text;
}
if (formData.url !== undefined) {
element.setAttribute('href', formData.url);
}
} else {
// Update text content
element.textContent = formData.text || '';
}
}
// Legacy method - now handled by handleSave and updateElementContent
addEditorStyles() {
const styles = `
.insertr-editing-hover {
outline: 2px dashed #007cba !important;
outline-offset: 2px !important;
background-color: rgba(0, 124, 186, 0.05) !important;
}
[data-insertr-enhanced="true"]:hover::after {
content: "✏️ " attr(data-content-type);
position: absolute;
top: -25px;
left: 0;
background: #007cba;
color: white;
padding: 2px 6px;
font-size: 11px;
border-radius: 3px;
white-space: nowrap;
z-index: 1000;
font-family: monospace;
}
`;
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = styles;
document.head.appendChild(styleSheet);
}
}
/**
* InsertrAuth - Authentication and state management
* Handles user authentication, edit mode, and visual state changes
*/
class InsertrAuth {
constructor(options = {}) {
this.options = {
mockAuth: options.mockAuth !== false, // Enable mock auth by default
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
...options
};
// Authentication state
this.state = {
isAuthenticated: false,
editMode: false,
currentUser: null,
activeEditor: null,
isInitialized: false,
isAuthenticating: false
};
this.statusIndicator = null;
}
/**
* Initialize gate system (called on page load)
*/
init() {
console.log('🔧 Insertr: Scanning for editor gates');
this.setupEditorGates();
}
/**
* Initialize full editing system (called after successful OAuth)
*/
initializeFullSystem() {
if (this.state.isInitialized) {
return; // Already initialized
}
console.log('🔐 Initializing Insertr Editing System');
this.createAuthControls();
this.setupAuthenticationControls();
this.createStatusIndicator();
this.updateBodyClasses();
// Auto-enable edit mode after OAuth
this.state.editMode = true;
this.state.isInitialized = true;
// Start the editor system
if (window.Insertr && window.Insertr.startEditor) {
window.Insertr.startEditor();
}
this.updateButtonStates();
this.updateStatusIndicator();
console.log('📱 Editing system active - Controls in bottom-right corner');
console.log('✏️ Edit mode enabled - Click elements to edit');
}
/**
* Setup editor gate click handlers for any .insertr-gate elements
*/
setupEditorGates() {
const gates = document.querySelectorAll('.insertr-gate');
if (gates.length === 0) {
console.log(' No .insertr-gate elements found - editor access disabled');
return;
}
console.log(`🚪 Found ${gates.length} editor gate(s)`);
// Add gate styles
this.addGateStyles();
gates.forEach((gate, index) => {
// Store original text for later restoration
if (!gate.hasAttribute('data-original-text')) {
gate.setAttribute('data-original-text', gate.textContent);
}
gate.addEventListener('click', (e) => {
e.preventDefault();
this.handleGateClick(gate, index);
});
// Add subtle styling to indicate it's clickable
gate.style.cursor = 'pointer';
});
}
/**
* Handle click on an editor gate element
*/
async handleGateClick(gateElement, gateIndex) {
// Prevent multiple simultaneous authentication attempts
if (this.state.isAuthenticating) {
console.log('⏳ Authentication already in progress...');
return;
}
console.log(`🚀 Editor gate activated (gate ${gateIndex + 1})`);
this.state.isAuthenticating = true;
// Store original text and show loading state
const originalText = gateElement.textContent;
gateElement.setAttribute('data-original-text', originalText);
gateElement.textContent = '⏳ Signing in...';
gateElement.style.pointerEvents = 'none';
try {
// Perform OAuth authentication
await this.performOAuthFlow();
// Initialize full editing system
this.initializeFullSystem();
// Conditionally hide gates based on options
if (this.options.hideGatesAfterAuth) {
this.hideAllGates();
} else {
this.updateGateState();
}
} catch (error) {
console.error('❌ Authentication failed:', error);
// Restore clicked gate to original state
const originalText = gateElement.getAttribute('data-original-text');
if (originalText) {
gateElement.textContent = originalText;
}
gateElement.style.pointerEvents = '';
} finally {
this.state.isAuthenticating = false;
}
}
/**
* Perform OAuth authentication flow
*/
async performOAuthFlow() {
// In development, simulate OAuth flow
if (this.options.mockAuth) {
console.log('🔐 Mock OAuth: Simulating authentication...');
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Set authenticated state
this.state.isAuthenticated = true;
this.state.currentUser = {
name: 'Site Owner',
email: 'owner@example.com',
role: 'admin'
};
console.log('✅ Mock OAuth: Authentication successful');
return;
}
// TODO: In production, implement real OAuth flow
// This would redirect to OAuth provider, handle callback, etc.
throw new Error('Production OAuth not implemented yet');
}
/**
* Hide all editor gates after successful authentication (optional)
*/
hideAllGates() {
document.body.classList.add('insertr-hide-gates');
console.log('🚪 Editor gates hidden (hideGatesAfterAuth enabled)');
}
/**
* Update gate state after authentication (restore normal appearance)
*/
updateGateState() {
const gates = document.querySelectorAll('.insertr-gate');
gates.forEach(gate => {
// Restore original text if it was saved
const originalText = gate.getAttribute('data-original-text');
if (originalText) {
gate.textContent = originalText;
}
// Restore interactive state
gate.style.pointerEvents = '';
gate.style.opacity = '';
});
console.log('🚪 Editor gates restored to original state');
}
/**
* Create authentication control buttons (bottom-right positioned)
*/
createAuthControls() {
// Check if controls already exist
if (document.getElementById('insertr-auth-controls')) {
return;
}
const controlsHtml = `
<div id="insertr-auth-controls" class="insertr-auth-controls">
<button id="insertr-auth-toggle" class="insertr-auth-btn">Login as Client</button>
<button id="insertr-edit-toggle" class="insertr-auth-btn" style="display: none;">Edit Mode: Off</button>
</div>
`;
// Add controls to page
document.body.insertAdjacentHTML('beforeend', controlsHtml);
// Add styles for controls
this.addControlStyles();
}
/**
* Setup event listeners for authentication controls
*/
setupAuthenticationControls() {
const authToggle = document.getElementById('insertr-auth-toggle');
const editToggle = document.getElementById('insertr-edit-toggle');
if (authToggle) {
authToggle.addEventListener('click', () => this.toggleAuthentication());
}
if (editToggle) {
editToggle.addEventListener('click', () => this.toggleEditMode());
}
}
/**
* Toggle authentication state
*/
toggleAuthentication() {
this.state.isAuthenticated = !this.state.isAuthenticated;
this.state.currentUser = this.state.isAuthenticated ? {
name: 'Demo User',
email: 'demo@example.com',
role: 'editor'
} : null;
// Reset edit mode when logging out
if (!this.state.isAuthenticated) {
this.state.editMode = false;
}
this.updateBodyClasses();
this.updateButtonStates();
this.updateStatusIndicator();
console.log(this.state.isAuthenticated
? '✅ Authenticated as Demo User'
: '❌ Logged out');
}
/**
* Toggle edit mode (only when authenticated)
*/
toggleEditMode() {
if (!this.state.isAuthenticated) {
console.warn('❌ Cannot enable edit mode - not authenticated');
return;
}
this.state.editMode = !this.state.editMode;
// Cancel any active editing when turning off edit mode
if (!this.state.editMode && this.state.activeEditor) {
// This would be handled by the main editor
this.state.activeEditor = null;
}
this.updateBodyClasses();
this.updateButtonStates();
this.updateStatusIndicator();
console.log(this.state.editMode
? '✏️ Edit mode ON - Click elements to edit'
: '👀 Edit mode OFF - Read-only view');
}
/**
* Update body CSS classes based on authentication state
*/
updateBodyClasses() {
document.body.classList.toggle('insertr-authenticated', this.state.isAuthenticated);
document.body.classList.toggle('insertr-edit-mode', this.state.editMode);
}
/**
* Update button text and visibility
*/
updateButtonStates() {
const authBtn = document.getElementById('insertr-auth-toggle');
const editBtn = document.getElementById('insertr-edit-toggle');
if (authBtn) {
authBtn.textContent = this.state.isAuthenticated ? 'Logout' : 'Login as Client';
authBtn.className = `insertr-auth-btn ${this.state.isAuthenticated ? 'insertr-authenticated' : ''}`;
}
if (editBtn) {
editBtn.style.display = this.state.isAuthenticated ? 'inline-block' : 'none';
editBtn.textContent = `Edit Mode: ${this.state.editMode ? 'On' : 'Off'}`;
editBtn.className = `insertr-auth-btn ${this.state.editMode ? 'insertr-edit-active' : ''}`;
}
}
/**
* Create status indicator
*/
createStatusIndicator() {
// Check if already exists
if (document.getElementById('insertr-status')) {
return;
}
const statusHtml = `
<div id="insertr-status" class="insertr-status">
<div class="insertr-status-content">
<span class="insertr-status-text">Visitor Mode</span>
<span class="insertr-status-dot"></span>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', statusHtml);
this.statusIndicator = document.getElementById('insertr-status');
this.updateStatusIndicator();
}
/**
* Update status indicator text and style
*/
updateStatusIndicator() {
const statusText = document.querySelector('.insertr-status-text');
const statusDot = document.querySelector('.insertr-status-dot');
if (!statusText || !statusDot) return;
if (!this.state.isAuthenticated) {
statusText.textContent = 'Visitor Mode';
statusDot.className = 'insertr-status-dot insertr-status-visitor';
} else if (this.state.editMode) {
statusText.textContent = 'Editing';
statusDot.className = 'insertr-status-dot insertr-status-editing';
} else {
statusText.textContent = 'Authenticated';
statusDot.className = 'insertr-status-dot insertr-status-authenticated';
}
}
/**
* Check if user is authenticated
*/
isAuthenticated() {
return this.state.isAuthenticated;
}
/**
* Check if edit mode is enabled
*/
isEditMode() {
return this.state.editMode;
}
/**
* Get current user info
*/
getCurrentUser() {
return this.state.currentUser;
}
/**
* Add minimal styles for editor gates
*/
addGateStyles() {
const styles = `
.insertr-gate {
transition: opacity 0.2s ease;
user-select: none;
}
.insertr-gate:hover {
opacity: 0.7;
}
/* Optional: Hide gates when authenticated (only if hideGatesAfterAuth option is true) */
body.insertr-hide-gates .insertr-gate {
display: none !important;
}
`;
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = styles;
document.head.appendChild(styleSheet);
}
/**
* Add styles for authentication controls
*/
addControlStyles() {
const styles = `
.insertr-auth-controls {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.insertr-auth-btn {
background: #4f46e5;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.insertr-auth-btn:hover {
background: #4338ca;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.insertr-auth-btn.insertr-authenticated {
background: #059669;
}
.insertr-auth-btn.insertr-authenticated:hover {
background: #047857;
}
.insertr-auth-btn.insertr-edit-active {
background: #dc2626;
}
.insertr-auth-btn.insertr-edit-active:hover {
background: #b91c1c;
}
.insertr-status {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 9999;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 200px;
}
.insertr-status-content {
display: flex;
align-items: center;
gap: 8px;
}
.insertr-status-text {
font-size: 12px;
font-weight: 500;
color: #374151;
}
.insertr-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #9ca3af;
}
.insertr-status-dot.insertr-status-visitor {
background: #9ca3af;
}
.insertr-status-dot.insertr-status-authenticated {
background: #059669;
}
.insertr-status-dot.insertr-status-editing {
background: #dc2626;
animation: insertr-pulse 2s infinite;
}
@keyframes insertr-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Hide editing interface when not in edit mode */
body:not(.insertr-edit-mode) [data-insertr-enhanced]:hover::after {
display: none !important;
}
/* Only show editing features when in edit mode */
.insertr-authenticated.insertr-edit-mode [data-insertr-enhanced] {
cursor: pointer;
}
.insertr-authenticated.insertr-edit-mode [data-insertr-enhanced]:hover {
outline: 2px dashed #007cba !important;
outline-offset: 2px !important;
background-color: rgba(0, 124, 186, 0.05) !important;
}
`;
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = styles;
document.head.appendChild(styleSheet);
}
/**
* OAuth integration placeholder
* In production, this would handle real OAuth flows
*/
async authenticateWithOAuth(provider = 'google') {
// Mock OAuth flow for now
console.log(`🔐 Mock OAuth login with ${provider}`);
// Simulate OAuth callback
setTimeout(() => {
this.state.isAuthenticated = true;
this.state.currentUser = {
name: 'OAuth User',
email: 'user@example.com',
provider: provider,
role: 'editor'
};
this.updateBodyClasses();
this.updateButtonStates();
this.updateStatusIndicator();
console.log('✅ OAuth authentication successful');
}, 1000);
}
}
/**
* Insertr - The Tailwind of CMS
* Main library entry point
*/
// Create global Insertr instance
window.Insertr = {
// Core functionality
core: null,
editor: null,
auth: null,
// Initialize the library
init(options = {}) {
console.log('🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)');
this.core = new InsertrCore(options);
this.auth = new InsertrAuth(options);
this.editor = new InsertrEditor(this.core, this.auth, options);
// Auto-initialize if DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.start());
} else {
this.start();
}
return this;
},
// Start the system - only creates the minimal trigger
start() {
if (this.auth) {
this.auth.init(); // Creates footer trigger only
}
// Note: Editor is NOT started here, only when trigger is clicked
},
// Start the full editor system (called when trigger is activated)
startEditor() {
if (this.editor && !this.editor.isActive) {
this.editor.start();
}
},
// Public API methods
login() {
return this.auth ? this.auth.toggleAuthentication() : null;
},
logout() {
if (this.auth && this.auth.isAuthenticated()) {
this.auth.toggleAuthentication();
}
},
toggleEditMode() {
return this.auth ? this.auth.toggleEditMode() : null;
},
isAuthenticated() {
return this.auth ? this.auth.isAuthenticated() : false;
},
isEditMode() {
return this.auth ? this.auth.isEditMode() : false;
},
// Version info
version: '1.0.0'
};
// Auto-initialize in development mode with proper DOM ready handling
function autoInitialize() {
if (document.querySelector('[data-insertr-enhanced="true"]')) {
window.Insertr.init();
}
}
// Run auto-initialization when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInitialize);
} else {
// DOM is already ready
autoInitialize();
}
var index = window.Insertr;
return index;
})();