Files
insertr/insertr-cli/pkg/content/assets/insertr.min.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

2 lines
24 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";class t{constructor(t={}){this.options={apiEndpoint:t.apiEndpoint||"/api/content",siteId:t.siteId||"default",...t}}findEnhancedElements(){return document.querySelectorAll('[data-insertr-enhanced="true"]')}getElementMetadata(t){return{contentId:t.getAttribute("data-content-id"),contentType:t.getAttribute("data-content-type"),element:t}}getAllElements(){const t=this.findEnhancedElements();return Array.from(t).map(t=>this.getElementMetadata(t))}}class e{constructor(){this.currentOverlay=null,this.setupStyles()}showEditForm(t,e,n,i){this.closeForm();const{element:r,contentId:o,contentType:s}=t,a=this.getFieldConfig(r,s),d=this.createEditForm(o,a,e),l=this.createOverlay(d);this.positionForm(r,l),this.setupFormHandlers(d,l,{onSave:n,onCancel:i}),document.body.appendChild(l),this.currentOverlay=l;const c=d.querySelector("input, textarea");return c&&setTimeout(()=>c.focus(),100),l}closeForm(){this.currentOverlay&&(this.currentOverlay.remove(),this.currentOverlay=null)}getFieldConfig(t,e){const n=t.tagName.toLowerCase(),i=Array.from(t.classList);let r={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:!0},span:{type:"text",label:"Text",placeholder:"Enter text..."},button:{type:"text",label:"Button Text",placeholder:"Enter button text..."}}[n]||{type:"text",label:"Text",placeholder:"Enter text..."};return i.includes("lead")&&(r={...r,label:"Lead Paragraph",rows:4,placeholder:"Enter lead paragraph..."}),"markdown"===e&&(r={...r,type:"markdown",label:"Markdown Content",rows:8}),r}createEditForm(t,e,n){const i=document.createElement("div");i.className="insertr-edit-form";let r=`<div class="insertr-form-header">${e.label}</div>`;return"markdown"===e.type?r+=this.createMarkdownField(e,n):"link"===e.type&&e.includeUrl?r+=this.createLinkField(e,n):"textarea"===e.type?r+=this.createTextareaField(e,n):r+=this.createTextField(e,n),r+='\n <div class="insertr-form-actions">\n <button type="button" class="insertr-btn-save">Save</button>\n <button type="button" class="insertr-btn-cancel">Cancel</button>\n </div>\n ',i.innerHTML=r,i}createMarkdownField(t,e){return`\n <div class="insertr-form-group">\n <textarea class="insertr-form-textarea insertr-markdown-editor" name="content" \n rows="${t.rows||8}"\n placeholder="${t.placeholder}">${this.escapeHtml(e)}</textarea>\n <div class="insertr-form-help">\n Supports Markdown formatting (bold, italic, links, etc.)\n </div>\n </div>\n `}createLinkField(t,e){const n="object"==typeof e?e.text||"":e,i="object"==typeof e&&e.url||"";return`\n <div class="insertr-form-group">\n <label class="insertr-form-label">Link Text:</label>\n <input type="text" class="insertr-form-input" name="text" \n value="${this.escapeHtml(n)}" \n placeholder="${t.placeholder}" \n maxlength="${t.maxLength||200}">\n </div>\n <div class="insertr-form-group">\n <label class="insertr-form-label">Link URL:</label>\n <input type="url" class="insertr-form-input" name="url" \n value="${this.escapeHtml(i)}" \n placeholder="https://example.com">\n </div>\n `}createTextareaField(t,e){const n="object"==typeof e?e.text||"":e;return`\n <div class="insertr-form-group">\n <textarea class="insertr-form-textarea" name="content" \n rows="${t.rows||3}"\n placeholder="${t.placeholder}"\n maxlength="${t.maxLength||1e3}">${this.escapeHtml(n)}</textarea>\n </div>\n `}createTextField(t,e){const n="object"==typeof e?e.text||"":e;return`\n <div class="insertr-form-group">\n <input type="text" class="insertr-form-input" name="content" \n value="${this.escapeHtml(n)}" \n placeholder="${t.placeholder}" \n maxlength="${t.maxLength||200}">\n </div>\n `}createOverlay(t){const e=document.createElement("div");return e.className="insertr-form-overlay",e.appendChild(t),e}positionForm(t,e){const n=t.getBoundingClientRect(),i=e.querySelector(".insertr-edit-form"),r=window.innerWidth;let o;o=r<768?Math.min(r-40,350):Math.min(Math.max(n.width,300),500),i.style.width=`${o}px`;const s=n.bottom+window.scrollY+10,a=Math.max(20,n.left+window.scrollX);e.style.position="absolute",e.style.top=`${s}px`,e.style.left=`${a}px`,e.style.zIndex="10000"}setupFormHandlers(t,e,{onSave:n,onCancel:i}){const r=t.querySelector(".insertr-btn-save"),o=t.querySelector(".insertr-btn-cancel");r&&r.addEventListener("click",()=>{const e=this.extractFormData(t);n(e)}),o&&o.addEventListener("click",()=>{i(),this.closeForm()});const s=t=>{"Escape"===t.key&&(i(),this.closeForm(),document.removeEventListener("keydown",s))};document.addEventListener("keydown",s),e.addEventListener("click",t=>{t.target===e&&(i(),this.closeForm())})}extractFormData(t){const e={},n=t.querySelector('input[name="text"]'),i=t.querySelector('input[name="url"]'),r=t.querySelector('input[name="content"], textarea[name="content"]');return n&&i?(e.text=n.value,e.url=i.value):r&&(e.text=r.value),e}escapeHtml(t){if("string"!=typeof t)return"";const e=document.createElement("div");return e.textContent=t,e.innerHTML}setupStyles(){const t=document.createElement("style");t.type="text/css",t.innerHTML="\n .insertr-form-overlay {\n position: absolute;\n z-index: 10000;\n }\n\n .insertr-edit-form {\n background: white;\n border: 2px solid #007cba;\n border-radius: 8px;\n padding: 1rem;\n box-shadow: 0 8px 25px rgba(0,0,0,0.15);\n width: 100%;\n box-sizing: border-box;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n .insertr-form-header {\n font-weight: 600;\n color: #1f2937;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid #e5e7eb;\n font-size: 0.875rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .insertr-form-group {\n margin-bottom: 1rem;\n }\n\n .insertr-form-group:last-child {\n margin-bottom: 0;\n }\n\n .insertr-form-label {\n display: block;\n font-weight: 600;\n color: #374151;\n margin-bottom: 0.5rem;\n font-size: 0.875rem;\n }\n\n .insertr-form-input, \n .insertr-form-textarea {\n width: 100%;\n padding: 0.75rem;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n font-family: inherit;\n font-size: 1rem;\n transition: border-color 0.2s, box-shadow 0.2s;\n box-sizing: border-box;\n }\n\n .insertr-form-input:focus,\n .insertr-form-textarea:focus {\n outline: none;\n border-color: #007cba;\n box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1);\n }\n\n .insertr-form-textarea {\n min-height: 120px;\n resize: vertical;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n }\n\n .insertr-markdown-editor {\n min-height: 200px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n font-size: 0.9rem;\n line-height: 1.5;\n background-color: #f8fafc;\n }\n\n .insertr-form-actions {\n display: flex;\n gap: 0.5rem;\n justify-content: flex-end;\n margin-top: 1rem;\n padding-top: 1rem;\n border-top: 1px solid #e5e7eb;\n }\n\n .insertr-btn-save {\n background: #10b981;\n color: white;\n border: none;\n padding: 0.5rem 1rem;\n border-radius: 6px;\n font-weight: 500;\n cursor: pointer;\n transition: background-color 0.2s;\n font-size: 0.875rem;\n }\n\n .insertr-btn-save:hover {\n background: #059669;\n }\n\n .insertr-btn-cancel {\n background: #6b7280;\n color: white;\n border: none;\n padding: 0.5rem 1rem;\n border-radius: 6px;\n font-weight: 500;\n cursor: pointer;\n transition: background-color 0.2s;\n font-size: 0.875rem;\n }\n\n .insertr-btn-cancel:hover {\n background: #4b5563;\n }\n\n .insertr-form-help {\n font-size: 0.75rem;\n color: #6b7280;\n margin-top: 0.25rem;\n }\n ",document.head.appendChild(t)}}class n{constructor(t,n,i={}){this.core=t,this.auth=n,this.options=i,this.isActive=!1,this.formRenderer=new e}start(){if(this.isActive)return;console.log("🚀 Starting Insertr Editor"),this.isActive=!0,this.addEditorStyles();const t=this.core.getAllElements();console.log(`📝 Found ${t.length} editable elements`),t.forEach(t=>this.initializeElement(t))}initializeElement(t){const{element:e,contentId:n,contentType:i}=t;e.style.cursor="pointer",e.style.position="relative",this.addHoverEffects(e),this.addClickHandler(e,t)}addHoverEffects(t){t.addEventListener("mouseenter",()=>{t.classList.add("insertr-editing-hover")}),t.addEventListener("mouseleave",()=>{t.classList.remove("insertr-editing-hover")})}addClickHandler(t,e){t.addEventListener("click",t=>{this.auth.isAuthenticated()&&this.auth.isEditMode()&&(t.preventDefault(),this.openEditor(e))})}openEditor(t){const{element:e}=t,n=this.extractCurrentContent(e);this.formRenderer.showEditForm(t,n,e=>this.handleSave(t,e),()=>this.handleCancel(t))}extractCurrentContent(t){return"a"===t.tagName.toLowerCase()?{text:t.textContent.trim(),url:t.getAttribute("href")||""}:t.textContent.trim()}handleSave(t,e){console.log("💾 Saving content:",t.contentId,e),this.updateElementContent(t.element,e),this.formRenderer.closeForm(),console.log("✅ Content saved:",t.contentId,e)}handleCancel(t){console.log("❌ Edit cancelled:",t.contentId)}updateElementContent(t,e){"a"===t.tagName.toLowerCase()?(void 0!==e.text&&(t.textContent=e.text),void 0!==e.url&&t.setAttribute("href",e.url)):t.textContent=e.text||""}addEditorStyles(){const t=document.createElement("style");t.type="text/css",t.innerHTML='\n .insertr-editing-hover {\n outline: 2px dashed #007cba !important;\n outline-offset: 2px !important;\n background-color: rgba(0, 124, 186, 0.05) !important;\n }\n \n [data-insertr-enhanced="true"]:hover::after {\n content: "✏️ " attr(data-content-type);\n position: absolute;\n top: -25px;\n left: 0;\n background: #007cba;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n font-family: monospace;\n }\n ',document.head.appendChild(t)}}class i{constructor(t={}){this.options={mockAuth:!1!==t.mockAuth,hideGatesAfterAuth:!0===t.hideGatesAfterAuth,...t},this.state={isAuthenticated:!1,editMode:!1,currentUser:null,activeEditor:null,isInitialized:!1,isAuthenticating:!1},this.statusIndicator=null}init(){console.log("🔧 Insertr: Scanning for editor gates"),this.setupEditorGates()}initializeFullSystem(){this.state.isInitialized||(console.log("🔐 Initializing Insertr Editing System"),this.createAuthControls(),this.setupAuthenticationControls(),this.createStatusIndicator(),this.updateBodyClasses(),this.state.editMode=!0,this.state.isInitialized=!0,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"))}setupEditorGates(){const t=document.querySelectorAll(".insertr-gate");0!==t.length?(console.log(`🚪 Found ${t.length} editor gate(s)`),this.addGateStyles(),t.forEach((t,e)=>{t.hasAttribute("data-original-text")||t.setAttribute("data-original-text",t.textContent),t.addEventListener("click",n=>{n.preventDefault(),this.handleGateClick(t,e)}),t.style.cursor="pointer"})):console.log(" No .insertr-gate elements found - editor access disabled")}async handleGateClick(t,e){if(this.state.isAuthenticating)return void console.log("⏳ Authentication already in progress...");console.log(`🚀 Editor gate activated (gate ${e+1})`),this.state.isAuthenticating=!0;const n=t.textContent;t.setAttribute("data-original-text",n),t.textContent="⏳ Signing in...",t.style.pointerEvents="none";try{await this.performOAuthFlow(),this.initializeFullSystem(),this.options.hideGatesAfterAuth?this.hideAllGates():this.updateGateState()}catch(e){console.error("❌ Authentication failed:",e);const n=t.getAttribute("data-original-text");n&&(t.textContent=n),t.style.pointerEvents=""}finally{this.state.isAuthenticating=!1}}async performOAuthFlow(){if(this.options.mockAuth)return console.log("🔐 Mock OAuth: Simulating authentication..."),await new Promise(t=>setTimeout(t,1e3)),this.state.isAuthenticated=!0,this.state.currentUser={name:"Site Owner",email:"owner@example.com",role:"admin"},void console.log("✅ Mock OAuth: Authentication successful");throw new Error("Production OAuth not implemented yet")}hideAllGates(){document.body.classList.add("insertr-hide-gates"),console.log("🚪 Editor gates hidden (hideGatesAfterAuth enabled)")}updateGateState(){document.querySelectorAll(".insertr-gate").forEach(t=>{const e=t.getAttribute("data-original-text");e&&(t.textContent=e),t.style.pointerEvents="",t.style.opacity=""}),console.log("🚪 Editor gates restored to original state")}createAuthControls(){if(document.getElementById("insertr-auth-controls"))return;document.body.insertAdjacentHTML("beforeend",'\n <div id="insertr-auth-controls" class="insertr-auth-controls">\n <button id="insertr-auth-toggle" class="insertr-auth-btn">Login as Client</button>\n <button id="insertr-edit-toggle" class="insertr-auth-btn" style="display: none;">Edit Mode: Off</button>\n </div>\n '),this.addControlStyles()}setupAuthenticationControls(){const t=document.getElementById("insertr-auth-toggle"),e=document.getElementById("insertr-edit-toggle");t&&t.addEventListener("click",()=>this.toggleAuthentication()),e&&e.addEventListener("click",()=>this.toggleEditMode())}toggleAuthentication(){this.state.isAuthenticated=!this.state.isAuthenticated,this.state.currentUser=this.state.isAuthenticated?{name:"Demo User",email:"demo@example.com",role:"editor"}:null,this.state.isAuthenticated||(this.state.editMode=!1),this.updateBodyClasses(),this.updateButtonStates(),this.updateStatusIndicator(),console.log(this.state.isAuthenticated?"✅ Authenticated as Demo User":"❌ Logged out")}toggleEditMode(){this.state.isAuthenticated?(this.state.editMode=!this.state.editMode,!this.state.editMode&&this.state.activeEditor&&(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")):console.warn("❌ Cannot enable edit mode - not authenticated")}updateBodyClasses(){document.body.classList.toggle("insertr-authenticated",this.state.isAuthenticated),document.body.classList.toggle("insertr-edit-mode",this.state.editMode)}updateButtonStates(){const t=document.getElementById("insertr-auth-toggle"),e=document.getElementById("insertr-edit-toggle");t&&(t.textContent=this.state.isAuthenticated?"Logout":"Login as Client",t.className="insertr-auth-btn "+(this.state.isAuthenticated?"insertr-authenticated":"")),e&&(e.style.display=this.state.isAuthenticated?"inline-block":"none",e.textContent="Edit Mode: "+(this.state.editMode?"On":"Off"),e.className="insertr-auth-btn "+(this.state.editMode?"insertr-edit-active":""))}createStatusIndicator(){if(document.getElementById("insertr-status"))return;document.body.insertAdjacentHTML("beforeend",'\n <div id="insertr-status" class="insertr-status">\n <div class="insertr-status-content">\n <span class="insertr-status-text">Visitor Mode</span>\n <span class="insertr-status-dot"></span>\n </div>\n </div>\n '),this.statusIndicator=document.getElementById("insertr-status"),this.updateStatusIndicator()}updateStatusIndicator(){const t=document.querySelector(".insertr-status-text"),e=document.querySelector(".insertr-status-dot");t&&e&&(this.state.isAuthenticated?this.state.editMode?(t.textContent="Editing",e.className="insertr-status-dot insertr-status-editing"):(t.textContent="Authenticated",e.className="insertr-status-dot insertr-status-authenticated"):(t.textContent="Visitor Mode",e.className="insertr-status-dot insertr-status-visitor"))}isAuthenticated(){return this.state.isAuthenticated}isEditMode(){return this.state.editMode}getCurrentUser(){return this.state.currentUser}addGateStyles(){const t=document.createElement("style");t.type="text/css",t.innerHTML="\n .insertr-gate {\n transition: opacity 0.2s ease;\n user-select: none;\n }\n\n .insertr-gate:hover {\n opacity: 0.7;\n }\n\n /* Optional: Hide gates when authenticated (only if hideGatesAfterAuth option is true) */\n body.insertr-hide-gates .insertr-gate {\n display: none !important;\n }\n ",document.head.appendChild(t)}addControlStyles(){const t=document.createElement("style");t.type="text/css",t.innerHTML="\n .insertr-auth-controls {\n position: fixed;\n bottom: 20px;\n right: 20px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n .insertr-auth-btn {\n background: #4f46e5;\n color: white;\n border: none;\n padding: 8px 16px;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .insertr-auth-btn:hover {\n background: #4338ca;\n transform: translateY(-1px);\n box-shadow: 0 4px 8px rgba(0,0,0,0.15);\n }\n\n .insertr-auth-btn.insertr-authenticated {\n background: #059669;\n }\n\n .insertr-auth-btn.insertr-authenticated:hover {\n background: #047857;\n }\n\n .insertr-auth-btn.insertr-edit-active {\n background: #dc2626;\n }\n\n .insertr-auth-btn.insertr-edit-active:hover {\n background: #b91c1c;\n }\n\n .insertr-status {\n position: fixed;\n bottom: 20px;\n left: 20px;\n z-index: 9999;\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n padding: 8px 12px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n max-width: 200px;\n }\n\n .insertr-status-content {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .insertr-status-text {\n font-size: 12px;\n font-weight: 500;\n color: #374151;\n }\n\n .insertr-status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #9ca3af;\n }\n\n .insertr-status-dot.insertr-status-visitor {\n background: #9ca3af;\n }\n\n .insertr-status-dot.insertr-status-authenticated {\n background: #059669;\n }\n\n .insertr-status-dot.insertr-status-editing {\n background: #dc2626;\n animation: insertr-pulse 2s infinite;\n }\n\n @keyframes insertr-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.5; }\n }\n\n /* Hide editing interface when not in edit mode */\n body:not(.insertr-edit-mode) [data-insertr-enhanced]:hover::after {\n display: none !important;\n }\n\n /* Only show editing features when in edit mode */\n .insertr-authenticated.insertr-edit-mode [data-insertr-enhanced] {\n cursor: pointer;\n }\n\n .insertr-authenticated.insertr-edit-mode [data-insertr-enhanced]:hover {\n outline: 2px dashed #007cba !important;\n outline-offset: 2px !important;\n background-color: rgba(0, 124, 186, 0.05) !important;\n }\n ",document.head.appendChild(t)}async authenticateWithOAuth(t="google"){console.log(`🔐 Mock OAuth login with ${t}`),setTimeout(()=>{this.state.isAuthenticated=!0,this.state.currentUser={name:"OAuth User",email:"user@example.com",provider:t,role:"editor"},this.updateBodyClasses(),this.updateButtonStates(),this.updateStatusIndicator(),console.log("✅ OAuth authentication successful")},1e3)}}function r(){document.querySelector('[data-insertr-enhanced="true"]')&&window.Insertr.init()}return window.Insertr={core:null,editor:null,auth:null,init(e={}){return console.log("🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)"),this.core=new t(e),this.auth=new i(e),this.editor=new n(this.core,this.auth,e),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.start()):this.start(),this},start(){this.auth&&this.auth.init()},startEditor(){this.editor&&!this.editor.isActive&&this.editor.start()},login(){return this.auth?this.auth.toggleAuthentication():null},logout(){this.auth&&this.auth.isAuthenticated()&&this.auth.toggleAuthentication()},toggleEditMode(){return this.auth?this.auth.toggleEditMode():null},isAuthenticated(){return!!this.auth&&this.auth.isAuthenticated()},isEditMode(){return!!this.auth&&this.auth.isEditMode()},version:"1.0.0"},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",r):r(),window.Insertr}();