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">
<div class="container">
<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>
</footer>

View File

@@ -76,7 +76,7 @@
<footer class="footer">
<div class="container">
<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>
</footer>

View File

@@ -639,7 +639,7 @@ var Insertr = (function () {
constructor(options = {}) {
this.options = {
mockAuth: options.mockAuth !== false, // Enable mock auth by default
autoCreateControls: options.autoCreateControls !== false,
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
...options
};
@@ -648,31 +648,191 @@ var Insertr = (function () {
isAuthenticated: false,
editMode: false,
currentUser: null,
activeEditor: null
activeEditor: null,
isInitialized: false,
isAuthenticating: false
};
this.statusIndicator = null;
}
/**
* Initialize authentication system
* Initialize gate system (called on page load)
*/
init() {
console.log('🔐 Initializing Insertr Authentication');
console.log('🔧 Insertr: Scanning for editor gates');
if (this.options.autoCreateControls) {
this.createAuthControls();
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();
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() {
// Check if controls already exist
@@ -853,6 +1013,32 @@ var Insertr = (function () {
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
*/
@@ -860,11 +1046,12 @@ var Insertr = (function () {
const styles = `
.insertr-auth-controls {
position: fixed;
top: 20px;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
gap: 10px;
flex-direction: column;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
@@ -914,6 +1101,7 @@ var Insertr = (function () {
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 {
@@ -1034,12 +1222,17 @@ var Insertr = (function () {
return this;
},
// Start the editor and authentication
// Start the system - only creates the minimal trigger
start() {
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();
}
},
@@ -1071,10 +1264,20 @@ var Insertr = (function () {
version: '1.0.0'
};
// Auto-initialize in development mode
if (document.querySelector('[data-insertr-enhanced]')) {
// 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;

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
libraryScript := GetLibraryScript(true) // Use minified for better performance
libraryScript := GetLibraryScript(false) // Use non-minified for development debugging
e.injector.InjectEditorAssets(doc, true, libraryScript)
// Write enhanced HTML

View File

@@ -2,6 +2,7 @@ package content
import (
"fmt"
"strings"
"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
script := &html.Node{
Type: html.ElementNode,
Data: "script",
Attr: []html.Attribute{
{Key: "type", Val: "text/javascript"},
},
}
// Note: Using html.TextNode for scripts can cause issues with HTML entity encoding
// Instead, we'll insert the script tag as raw HTML
scriptHTML := fmt.Sprintf(`<script type="text/javascript">
%s
</script>`, libraryScript)
// Add the library content as text node
textNode := &html.Node{
Type: html.TextNode,
Data: libraryScript,
// Parse the script HTML and append to head
scriptNodes, err := html.ParseFragment(strings.NewReader(scriptHTML), head)
if err == nil && len(scriptNodes) > 0 {
for _, node := range scriptNodes {
head.AppendChild(node)
}
}
script.AppendChild(textNode)
head.AppendChild(script)
}
// findHeadElement finds the <head> element in the document

View File

@@ -6,7 +6,7 @@ export class InsertrAuth {
constructor(options = {}) {
this.options = {
mockAuth: options.mockAuth !== false, // Enable mock auth by default
autoCreateControls: options.autoCreateControls !== false,
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
...options
};
@@ -15,31 +15,191 @@ export class InsertrAuth {
isAuthenticated: false,
editMode: false,
currentUser: null,
activeEditor: null
activeEditor: null,
isInitialized: false,
isAuthenticating: false
};
this.statusIndicator = null;
}
/**
* Initialize authentication system
* Initialize gate system (called on page load)
*/
init() {
console.log('🔐 Initializing Insertr Authentication');
console.log('🔧 Insertr: Scanning for editor gates');
if (this.options.autoCreateControls) {
this.createAuthControls();
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();
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() {
// Check if controls already exist
@@ -220,6 +380,32 @@ export class InsertrAuth {
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
*/
@@ -227,11 +413,12 @@ export class InsertrAuth {
const styles = `
.insertr-auth-controls {
position: fixed;
top: 20px;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
gap: 10px;
flex-direction: column;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
@@ -281,6 +468,7 @@ export class InsertrAuth {
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 {

View File

@@ -33,12 +33,17 @@ window.Insertr = {
return this;
},
// Start the editor and authentication
// Start the system - only creates the minimal trigger
start() {
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();
}
},
@@ -70,9 +75,19 @@ window.Insertr = {
version: '1.0.0'
};
// Auto-initialize in development mode
if (document.querySelector('[data-insertr-enhanced]')) {
// 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();
}
export default window.Insertr;