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.
This commit is contained in:
2025-09-04 18:42:30 +02:00
parent 1d81c636cb
commit 6fef293df3
8 changed files with 454 additions and 49 deletions

View File

@@ -100,7 +100,7 @@
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p> <p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p> <p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
</div> </div>
</footer> </footer>

View File

@@ -76,7 +76,7 @@
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p> <p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p> <p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
</div> </div>
</footer> </footer>

View File

@@ -639,7 +639,7 @@ var Insertr = (function () {
constructor(options = {}) { constructor(options = {}) {
this.options = { this.options = {
mockAuth: options.mockAuth !== false, // Enable mock auth by default mockAuth: options.mockAuth !== false, // Enable mock auth by default
autoCreateControls: options.autoCreateControls !== false, hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
...options ...options
}; };
@@ -648,31 +648,191 @@ var Insertr = (function () {
isAuthenticated: false, isAuthenticated: false,
editMode: false, editMode: false,
currentUser: null, currentUser: null,
activeEditor: null activeEditor: null,
isInitialized: false,
isAuthenticating: false
}; };
this.statusIndicator = null; this.statusIndicator = null;
} }
/** /**
* Initialize authentication system * Initialize gate system (called on page load)
*/ */
init() { init() {
console.log('🔐 Initializing Insertr Authentication'); console.log('🔧 Insertr: Scanning for editor gates');
if (this.options.autoCreateControls) { this.setupEditorGates();
this.createAuthControls();
} }
/**
* 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.setupAuthenticationControls();
this.createStatusIndicator(); this.createStatusIndicator();
this.updateBodyClasses(); this.updateBodyClasses();
console.log('📱 Auth controls ready - Look for buttons in top-right corner'); // 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');
} }
/** /**
* Create authentication control buttons if they don't exist * 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() { createAuthControls() {
// Check if controls already exist // Check if controls already exist
@@ -853,6 +1013,32 @@ var Insertr = (function () {
return this.state.currentUser; 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 * Add styles for authentication controls
*/ */
@@ -860,11 +1046,12 @@ var Insertr = (function () {
const styles = ` const styles = `
.insertr-auth-controls { .insertr-auth-controls {
position: fixed; position: fixed;
top: 20px; bottom: 20px;
right: 20px; right: 20px;
z-index: 9999; z-index: 9999;
display: flex; display: flex;
gap: 10px; flex-direction: column;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
@@ -914,6 +1101,7 @@ var Insertr = (function () {
padding: 8px 12px; padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 200px;
} }
.insertr-status-content { .insertr-status-content {
@@ -1034,12 +1222,17 @@ var Insertr = (function () {
return this; return this;
}, },
// Start the editor and authentication // Start the system - only creates the minimal trigger
start() { start() {
if (this.auth) { if (this.auth) {
this.auth.init(); this.auth.init(); // Creates footer trigger only
} }
if (this.editor) { // 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(); this.editor.start();
} }
}, },
@@ -1071,10 +1264,20 @@ var Insertr = (function () {
version: '1.0.0' version: '1.0.0'
}; };
// Auto-initialize in development mode // Auto-initialize in development mode with proper DOM ready handling
if (document.querySelector('[data-insertr-enhanced]')) { function autoInitialize() {
if (document.querySelector('[data-insertr-enhanced="true"]')) {
window.Insertr.init(); 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; var index = window.Insertr;

File diff suppressed because one or more lines are too long

View File

@@ -69,7 +69,7 @@ func (e *Enhancer) EnhanceFile(inputPath, outputPath string) error {
} }
// Inject editor assets for development // Inject editor assets for development
libraryScript := GetLibraryScript(true) // Use minified for better performance libraryScript := GetLibraryScript(false) // Use non-minified for development debugging
e.injector.InjectEditorAssets(doc, true, libraryScript) e.injector.InjectEditorAssets(doc, true, libraryScript)
// Write enhanced HTML // Write enhanced HTML

View File

@@ -2,6 +2,7 @@ package content
import ( import (
"fmt" "fmt"
"strings"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -145,21 +146,19 @@ func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, librar
} }
// Add inline script with embedded library // Add inline script with embedded library
script := &html.Node{ // Note: Using html.TextNode for scripts can cause issues with HTML entity encoding
Type: html.ElementNode, // Instead, we'll insert the script tag as raw HTML
Data: "script", scriptHTML := fmt.Sprintf(`<script type="text/javascript">
Attr: []html.Attribute{ %s
{Key: "type", Val: "text/javascript"}, </script>`, libraryScript)
},
}
// Add the library content as text node // Parse the script HTML and append to head
textNode := &html.Node{ scriptNodes, err := html.ParseFragment(strings.NewReader(scriptHTML), head)
Type: html.TextNode, if err == nil && len(scriptNodes) > 0 {
Data: libraryScript, for _, node := range scriptNodes {
head.AppendChild(node)
}
} }
script.AppendChild(textNode)
head.AppendChild(script)
} }
// findHeadElement finds the <head> element in the document // findHeadElement finds the <head> element in the document

View File

@@ -6,7 +6,7 @@ export class InsertrAuth {
constructor(options = {}) { constructor(options = {}) {
this.options = { this.options = {
mockAuth: options.mockAuth !== false, // Enable mock auth by default mockAuth: options.mockAuth !== false, // Enable mock auth by default
autoCreateControls: options.autoCreateControls !== false, hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
...options ...options
}; };
@@ -15,31 +15,191 @@ export class InsertrAuth {
isAuthenticated: false, isAuthenticated: false,
editMode: false, editMode: false,
currentUser: null, currentUser: null,
activeEditor: null activeEditor: null,
isInitialized: false,
isAuthenticating: false
}; };
this.statusIndicator = null; this.statusIndicator = null;
} }
/** /**
* Initialize authentication system * Initialize gate system (called on page load)
*/ */
init() { init() {
console.log('🔐 Initializing Insertr Authentication'); console.log('🔧 Insertr: Scanning for editor gates');
if (this.options.autoCreateControls) { this.setupEditorGates();
this.createAuthControls();
} }
/**
* 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.setupAuthenticationControls();
this.createStatusIndicator(); this.createStatusIndicator();
this.updateBodyClasses(); this.updateBodyClasses();
console.log('📱 Auth controls ready - Look for buttons in top-right corner'); // 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');
} }
/** /**
* Create authentication control buttons if they don't exist * 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() { createAuthControls() {
// Check if controls already exist // Check if controls already exist
@@ -220,6 +380,32 @@ export class InsertrAuth {
return this.state.currentUser; 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 * Add styles for authentication controls
*/ */
@@ -227,11 +413,12 @@ export class InsertrAuth {
const styles = ` const styles = `
.insertr-auth-controls { .insertr-auth-controls {
position: fixed; position: fixed;
top: 20px; bottom: 20px;
right: 20px; right: 20px;
z-index: 9999; z-index: 9999;
display: flex; display: flex;
gap: 10px; flex-direction: column;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
@@ -281,6 +468,7 @@ export class InsertrAuth {
padding: 8px 12px; padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 200px;
} }
.insertr-status-content { .insertr-status-content {

View File

@@ -33,12 +33,17 @@ window.Insertr = {
return this; return this;
}, },
// Start the editor and authentication // Start the system - only creates the minimal trigger
start() { start() {
if (this.auth) { if (this.auth) {
this.auth.init(); this.auth.init(); // Creates footer trigger only
} }
if (this.editor) { // 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(); this.editor.start();
} }
}, },
@@ -70,9 +75,19 @@ window.Insertr = {
version: '1.0.0' version: '1.0.0'
}; };
// Auto-initialize in development mode // Auto-initialize in development mode with proper DOM ready handling
if (document.querySelector('[data-insertr-enhanced]')) { function autoInitialize() {
if (document.querySelector('[data-insertr-enhanced="true"]')) {
window.Insertr.init(); window.Insertr.init();
}
}
// Run auto-initialization when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInitialize);
} else {
// DOM is already ready
autoInitialize();
} }
export default window.Insertr; export default window.Insertr;