MAJOR UX IMPROVEMENT: Replace basic prompt() with professional forms
New Features:
- Professional modal overlays with backdrop and ESC/click-outside cancel
- Dynamic form generation based on content type and HTML element
- Smart field detection: H1-H6→text, P→textarea, A→link with URL
- Mobile-responsive form positioning and widths
- Complete CSS styling with focus states and transitions
- Proper save/cancel event handling
Technical Implementation:
- Created lib/src/ui/form-renderer.js with modern ES6+ modules
- Integrated into core editor.js with form renderer instance
- Support for text, textarea, markdown, and link field types
- XSS protection with HTML escaping
- Responsive design: mobile-first form sizing
- Professional styling matching prototype quality
Before: Basic browser prompt() for all editing
After: Content-aware professional modal forms
This brings the library from proof-of-concept to professional-grade
editing experience, closing the major UX gap with the archived prototype.
Phase 1.2 ✅ COMPLETED - Next: Authentication system (Phase 1.1)
2 lines
13 KiB
JavaScript
2 lines
13 KiB
JavaScript
var Insertr=function(){"use strict";class e{constructor(e={}){this.options={apiEndpoint:e.apiEndpoint||"/api/content",siteId:e.siteId||"default",...e}}findEnhancedElements(){return document.querySelectorAll('[data-insertr-enhanced="true"]')}getElementMetadata(e){return{contentId:e.getAttribute("data-content-id"),contentType:e.getAttribute("data-content-type"),element:e}}getAllElements(){const e=this.findEnhancedElements();return Array.from(e).map(e=>this.getElementMetadata(e))}}class t{constructor(){this.currentOverlay=null,this.setupStyles()}showEditForm(e,t,n,r){this.closeForm();const{element:o,contentId:a,contentType:i}=e,s=this.getFieldConfig(o,i),l=this.createEditForm(a,s,t),d=this.createOverlay(l);this.positionForm(o,d),this.setupFormHandlers(l,d,{onSave:n,onCancel:r}),document.body.appendChild(d),this.currentOverlay=d;const c=l.querySelector("input, textarea");return c&&setTimeout(()=>c.focus(),100),d}closeForm(){this.currentOverlay&&(this.currentOverlay.remove(),this.currentOverlay=null)}getFieldConfig(e,t){const n=e.tagName.toLowerCase(),r=Array.from(e.classList);let o={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 r.includes("lead")&&(o={...o,label:"Lead Paragraph",rows:4,placeholder:"Enter lead paragraph..."}),"markdown"===t&&(o={...o,type:"markdown",label:"Markdown Content",rows:8}),o}createEditForm(e,t,n){const r=document.createElement("div");r.className="insertr-edit-form";let o=`<div class="insertr-form-header">${t.label}</div>`;return"markdown"===t.type?o+=this.createMarkdownField(t,n):"link"===t.type&&t.includeUrl?o+=this.createLinkField(t,n):"textarea"===t.type?o+=this.createTextareaField(t,n):o+=this.createTextField(t,n),o+='\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 ',r.innerHTML=o,r}createMarkdownField(e,t){return`\n <div class="insertr-form-group">\n <textarea class="insertr-form-textarea insertr-markdown-editor" name="content" \n rows="${e.rows||8}"\n placeholder="${e.placeholder}">${this.escapeHtml(t)}</textarea>\n <div class="insertr-form-help">\n Supports Markdown formatting (bold, italic, links, etc.)\n </div>\n </div>\n `}createLinkField(e,t){const n="object"==typeof t?t.text||"":t,r="object"==typeof t&&t.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="${e.placeholder}" \n maxlength="${e.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(r)}" \n placeholder="https://example.com">\n </div>\n `}createTextareaField(e,t){const n="object"==typeof t?t.text||"":t;return`\n <div class="insertr-form-group">\n <textarea class="insertr-form-textarea" name="content" \n rows="${e.rows||3}"\n placeholder="${e.placeholder}"\n maxlength="${e.maxLength||1e3}">${this.escapeHtml(n)}</textarea>\n </div>\n `}createTextField(e,t){const n="object"==typeof t?t.text||"":t;return`\n <div class="insertr-form-group">\n <input type="text" class="insertr-form-input" name="content" \n value="${this.escapeHtml(n)}" \n placeholder="${e.placeholder}" \n maxlength="${e.maxLength||200}">\n </div>\n `}createOverlay(e){const t=document.createElement("div");return t.className="insertr-form-overlay",t.appendChild(e),t}positionForm(e,t){const n=e.getBoundingClientRect(),r=t.querySelector(".insertr-edit-form"),o=window.innerWidth;let a;a=o<768?Math.min(o-40,350):Math.min(Math.max(n.width,300),500),r.style.width=`${a}px`;const i=n.bottom+window.scrollY+10,s=Math.max(20,n.left+window.scrollX);t.style.position="absolute",t.style.top=`${i}px`,t.style.left=`${s}px`,t.style.zIndex="10000"}setupFormHandlers(e,t,{onSave:n,onCancel:r}){const o=e.querySelector(".insertr-btn-save"),a=e.querySelector(".insertr-btn-cancel");o&&o.addEventListener("click",()=>{const t=this.extractFormData(e);n(t)}),a&&a.addEventListener("click",()=>{r(),this.closeForm()});const i=e=>{"Escape"===e.key&&(r(),this.closeForm(),document.removeEventListener("keydown",i))};document.addEventListener("keydown",i),t.addEventListener("click",e=>{e.target===t&&(r(),this.closeForm())})}extractFormData(e){const t={},n=e.querySelector('input[name="text"]'),r=e.querySelector('input[name="url"]'),o=e.querySelector('input[name="content"], textarea[name="content"]');return n&&r?(t.text=n.value,t.url=r.value):o&&(t.text=o.value),t}escapeHtml(e){if("string"!=typeof e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}setupStyles(){const e=document.createElement("style");e.type="text/css",e.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(e)}}class n{constructor(e,n={}){this.core=e,this.options=n,this.isActive=!1,this.formRenderer=new t}start(){if(this.isActive)return;console.log("🚀 Starting Insertr Editor"),this.isActive=!0,this.addEditorStyles();const e=this.core.getAllElements();console.log(`📝 Found ${e.length} editable elements`),e.forEach(e=>this.initializeElement(e))}initializeElement(e){const{element:t,contentId:n,contentType:r}=e;t.style.cursor="pointer",t.style.position="relative",this.addHoverEffects(t),this.addClickHandler(t,e)}addHoverEffects(e){e.addEventListener("mouseenter",()=>{e.classList.add("insertr-editing-hover")}),e.addEventListener("mouseleave",()=>{e.classList.remove("insertr-editing-hover")})}addClickHandler(e,t){e.addEventListener("click",e=>{e.preventDefault(),this.openEditor(t)})}openEditor(e){const{element:t}=e,n=this.extractCurrentContent(t);this.formRenderer.showEditForm(e,n,t=>this.handleSave(e,t),()=>this.handleCancel(e))}extractCurrentContent(e){return"a"===e.tagName.toLowerCase()?{text:e.textContent.trim(),url:e.getAttribute("href")||""}:e.textContent.trim()}handleSave(e,t){console.log("💾 Saving content:",e.contentId,t),this.updateElementContent(e.element,t),this.formRenderer.closeForm(),console.log("✅ Content saved:",e.contentId,t)}handleCancel(e){console.log("❌ Edit cancelled:",e.contentId)}updateElementContent(e,t){"a"===e.tagName.toLowerCase()?(void 0!==t.text&&(e.textContent=t.text),void 0!==t.url&&e.setAttribute("href",t.url)):e.textContent=t.text||""}addEditorStyles(){const e=document.createElement("style");e.type="text/css",e.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(e)}}return window.Insertr={core:null,editor:null,init(t={}){return console.log("🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)"),this.core=new e(t),this.editor=new n(this.core,t),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.start()):this.start(),this},start(){this.editor&&this.editor.start()},version:"1.0.0"},document.querySelector("[data-insertr-enhanced]")&&window.Insertr.init(),window.Insertr}();
|