- Move scripts/ to lib/scripts/ and convert to ESM modules - Consolidate dependencies: add live-server to lib/package.json - Remove root package.json and node_modules split - Preserve CLI integration via existing rebuild-library.sh - Add development quickstart guide for new unified workflow - Clean up outdated file references and duplicate assets
1286 lines
43 KiB
JavaScript
1286 lines
43 KiB
JavaScript
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('.insertr');
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
.insertr: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) .insertr:hover::after {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Only show editing features when in edit mode */
|
||
.insertr-authenticated.insertr-edit-mode .insertr {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.insertr-authenticated.insertr-edit-mode .insertr: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;
|
||
},
|
||
|
||
// TODO: Version info based on package.json?
|
||
};
|
||
|
||
// Auto-initialize in development mode with proper DOM ready handling
|
||
function autoInitialize() {
|
||
if (document.querySelector('.insertr')) {
|
||
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;
|
||
|
||
})();
|