diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index eaef139..46b8912 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -33,8 +33,7 @@ The install code is self-contained, so strict CSP pages do not block it. 1. **Click the bookmarklet** on any webpage to open the Webbender panel 2. **Toggle features** using the checkboxes: - - Edit Text: Make webpage content editable - - Remove Elements: Click elements to remove them + - Start Immersive Edit: Opens a movable floating action sheet with icon tools for select, pan, text, shapes, image insertion, duplicate/delete, color picker, options, undo/redo, save, and close 3. **Apply fonts**: Select from preset fonts or type a custom font name 4. **Change theme**: Click a theme button to apply colors 5. **Test dialogs**: Use dialog buttons to test alert/confirm/prompt boxes diff --git a/site/bookmarklet.js b/site/bookmarklet.js index 9db046d..c6e55d0 100644 --- a/site/bookmarklet.js +++ b/site/bookmarklet.js @@ -1 +1 @@ -function wbUI(){return{create(e,t={}){const n=document.createElement(e);return void 0!==t.textContent&&(n.textContent=t.textContent),t.attrs&&Object.entries(t.attrs).forEach(([e,t])=>{n.setAttribute(e,t)}),t.style&&Object.assign(n.style,t.style),n},append:(e,t)=>(t.forEach(t=>e.appendChild(t)),e),button(e,t,n={}){const o=this.create("button",{textContent:e,style:t});return n.click&&(o.onclick=n.click),n.mouseover&&(o.onmouseover=n.mouseover),n.mouseout&&(o.onmouseout=n.mouseout),o}}}function wbGetStyleElement(e){let t=document.getElementById(e);return t||(t=document.createElement("style"),t.id=e,document.head.appendChild(t)),t}function wbInitState(){const e="webbender-ui",t="webbender-settings",n=document.getElementById(e);if(n)return n.remove(),null;const o={editMode:!1,moveMode:!1,removeMode:!1,customFont:"",theme:"default"};try{const e=localStorage.getItem(t);e&&Object.assign(o,JSON.parse(e))}catch(e){console.warn("Failed to load webbender settings:",e)}return{ID:e,STORAGE_KEY:t,VERSION:"1.0.2",BUILD_DATE:"2026-05-20T14:10:23.296Z",VERSION_URL:"https://webbender.web.app/version.json",settings:o,saveSettings:function(){try{localStorage.setItem(t,JSON.stringify(o))}catch(e){console.warn("Failed to save webbender settings:",e)}}}}function wbCreateContainer(e,t){return e.create("div",{attrs:{id:t},style:{position:"fixed",top:"20px",right:"20px",width:"300px",backgroundColor:"#18181b",color:"#f4f4f5",padding:"16px",borderRadius:"12px",boxShadow:"0 10px 25px -5px rgba(0, 0, 0, 0.5)",zIndex:"2147483647",fontFamily:"-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",fontSize:"13px",userSelect:"none",boxSizing:"border-box",border:"1px solid #27272a",display:"flex",flexDirection:"column",gap:"14px"}})}function wbCreateHeader(e,t){const n=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",borderBottom:"1px solid #27272a",paddingBottom:"8px"}}),o=e.create("span",{textContent:"Webbender",style:{fontWeight:"700",fontSize:"14px"}}),r=e.button("✕",{cursor:"pointer",color:"#71717a",fontWeight:"bold",background:"none",border:"none",fontSize:"16px",padding:"0 4px",transition:"color 0.2s"},{mouseover:()=>{r.style.color="#f4f4f5"},mouseout:()=>{r.style.color="#71717a"},click:()=>{window._webbenderRemoveMode&&window._webbenderToggleRemove(!1),window._webbenderMoveMode&&window._webbenderToggleMove(!1),t.remove()}});return e.append(n,[o,r]),{header:n}}function wbCreateUpdateBanner(e){const t=e.create("div",{style:{display:"none",flexDirection:"column",gap:"4px",padding:"8px 10px",borderRadius:"8px",background:"rgba(234, 179, 8, 0.12)",border:"1px solid rgba(234, 179, 8, 0.3)",fontSize:"12px",lineHeight:"1.5"}}),n=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}}),o=e.create("span",{style:{color:"#fde047"}}),r=e.button("✕",{background:"none",border:"none",color:"#71717a",cursor:"pointer",padding:"0",fontSize:"12px"},{click:()=>t.style.display="none"}),a=e.create("a",{textContent:"Re-install from webbender.web.app →",attrs:{href:"https://webbender.web.app",target:"_blank",rel:"noreferrer"},style:{color:"#93c5fd",fontSize:"11px",display:"block"}});return e.append(n,[o,r]),e.append(t,[n,a]),{updateBanner:t,updateText:o}}function wbCreateEditRemoveSection(e,t,n){const{settings:o,saveSettings:r}=n;function a(e){return!e||e===t||t.contains(e)||"HTML"===e.tagName||"BODY"===e.tagName}function i(e,t){e&&void 0===e.dataset.webbenderOutlineBackup&&(e.dataset.webbenderOutlineBackup=e.style.outline||"",e.style.outline=t)}function d(e){e&&void 0!==e.dataset.webbenderOutlineBackup&&(e.style.outline=e.dataset.webbenderOutlineBackup,delete e.dataset.webbenderOutlineBackup)}const s=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}}),c=e.create("label",{textContent:"Edit Text (Design Mode)",style:{cursor:"pointer",display:"flex",alignItems:"center",gap:"8px",flex:"1"}}),l=e.create("input",{attrs:{type:"checkbox"},style:{cursor:"pointer",width:"16px",height:"16px"}});function p(e){e&&window._webbenderRemoveMode&&window._webbenderToggleRemove(!1),e&&window._webbenderMoveMode&&window._webbenderToggleMove(!1),document.designMode=e?"on":"off",document.body.contentEditable=e?"true":"false",l.checked=e,o.editMode=e,r()}l.checked="on"===document.designMode,l.onchange=e=>p(e.target.checked),c.appendChild(l),s.appendChild(c);const u=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}}),m=e.create("label",{textContent:"Grab & Move",style:{cursor:"pointer",display:"flex",alignItems:"center",gap:"8px",flex:"1"}}),b=e.create("input",{attrs:{type:"checkbox"},style:{cursor:"pointer",width:"16px",height:"16px"}}),g=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}}),f=e.create("label",{textContent:"Remove Elements",style:{cursor:"pointer",display:"flex",alignItems:"center",gap:"8px",flex:"1"}}),w=e.create("input",{attrs:{type:"checkbox"},style:{cursor:"pointer",width:"16px",height:"16px"}});let v=null,x=null;function y(e){v!==e&&(d(v),v=e,v&&i(v,"2px solid #22c55e"))}window._webbenderMoveMode=!1,window._webbenderRemoveMode=!1;const h=e=>{x||a(e.target)||y(e.target)},S=e=>{x||e.target!==v||y(null)},C=e=>{if(0!==e.button||a(e.target))return;const t=e.target.getBoundingClientRect(),n=parseFloat(e.target.dataset.webbenderMoveX||"0"),o=parseFloat(e.target.dataset.webbenderMoveY||"0");void 0===e.target.dataset.webbenderBaseTransform&&(e.target.dataset.webbenderBaseTransform=e.target.style.transform||""),x={target:e.target,startX:e.clientX,startY:e.clientY,moveX:n,moveY:o,minDeltaX:1-t.right,maxDeltaX:window.innerWidth-t.left-1,minDeltaY:1-t.bottom,maxDeltaY:window.innerHeight-t.top-1},y(e.target),e.preventDefault(),e.stopPropagation()},k=e=>{if(!x)return;const t=Math.min(Math.max(e.clientX-x.startX,x.minDeltaX),x.maxDeltaX),n=Math.min(Math.max(e.clientY-x.startY,x.minDeltaY),x.maxDeltaY);!function(e,t,n){const o=e.dataset.webbenderBaseTransform||"",r=`translate(${t}px, ${n}px)`;e.style.transform=o?`${r} ${o}`:r,e.dataset.webbenderMoveX=String(t),e.dataset.webbenderMoveY=String(n)}(x.target,x.moveX+t,x.moveY+n),e.preventDefault(),e.stopPropagation()},M=e=>{x&&(y(x.target),x=null,e.preventDefault(),e.stopPropagation())},E=e=>{a(e.target)||(e.preventDefault(),e.stopPropagation())},T=e=>{a(e.target)||i(e.target,"2px solid #ef4444")},R=e=>{a(e.target)||d(e.target)},I=e=>{a(e.target)||(e.preventDefault(),e.stopPropagation(),d(e.target),e.target.remove())};return window._webbenderToggleMove=function(e){e&&"on"===document.designMode&&p(!1),e&&window._webbenderRemoveMode&&window._webbenderToggleRemove(!1),window._webbenderMoveMode=e,b.checked=e,o.moveMode=e,r(),e?(document.addEventListener("mouseover",h),document.addEventListener("mouseout",S),document.addEventListener("mousedown",C,!0),document.addEventListener("mousemove",k,!0),document.addEventListener("mouseup",M,!0),document.addEventListener("click",E,!0)):(document.removeEventListener("mouseover",h),document.removeEventListener("mouseout",S),document.removeEventListener("mousedown",C,!0),document.removeEventListener("mousemove",k,!0),document.removeEventListener("mouseup",M,!0),document.removeEventListener("click",E,!0),y(null),x=null)},window._webbenderToggleRemove=function(e){e&&"on"===document.designMode&&p(!1),e&&window._webbenderMoveMode&&window._webbenderToggleMove(!1),window._webbenderRemoveMode=e,w.checked=e,o.removeMode=e,r(),e?(document.addEventListener("mouseover",T),document.addEventListener("mouseout",R),document.addEventListener("click",I,!0)):(document.removeEventListener("mouseover",T),document.removeEventListener("mouseout",R),document.removeEventListener("click",I,!0))},b.onchange=e=>window._webbenderToggleMove(e.target.checked),m.appendChild(b),u.appendChild(m),w.onchange=e=>window._webbenderToggleRemove(e.target.checked),f.appendChild(w),g.appendChild(f),{editSection:s,moveSection:u,removeSection:g,setEditMode:p}}function wbGetThemeCss(e,t){return`* { background: ${e} !important; color: ${t} !important; border-color: rgba(128, 128, 128, 0.2) !important; background-image: none !important; } html, body { background: ${e} !important; background-image: none !important; }`}function wbCreateFontThemeSection(e,t){const{settings:n,saveSettings:o}=t,r=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),a=e.create("span",{textContent:"Typography",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),i=e.create("select",{style:{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"13px",cursor:"pointer"}});[["Default",""],["Sans-Serif","-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif"],["Serif","Georgia, Times New Roman, serif"],["Monospace","Courier New, monospace"],["Comic Sans","Comic Sans MS, Arial, sans-serif"]].forEach(([t,n])=>{const o=e.create("option",{textContent:t});o.value=n,i.appendChild(o)});const d=e.create("input",{attrs:{type:"text",placeholder:"Custom font..."},style:{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"12px"}});function s(e){wbGetStyleElement("webbender-font-style").textContent=e?`* { font-family: "${e}" !important; }`:""}i.onchange=e=>{d.value="",s(e.target.value),n.customFont=e.target.value,o()},d.oninput=e=>{i.value="",s(e.target.value),n.customFont=e.target.value,o()},e.append(r,[a,i,d]);const c=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),l=e.create("span",{textContent:"Color Theme",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),p=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(2, 1fr)",gap:"6px"}}),u=[{name:"Default",bg:"",fg:""},{name:"Dark",bg:"#121212",fg:"#e4e4e7"},{name:"Light",bg:"#ffffff",fg:"#18181b"},{name:"Sepia",bg:"#f4ecd8",fg:"#433422"}];return u.forEach(t=>{const r=e.button(t.name,{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"11px",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>r.style.background="#3f3f46",mouseout:()=>r.style.background="#27272a",click:()=>{const e=wbGetStyleElement("webbender-theme-style");t.bg?(e.textContent=wbGetThemeCss(t.bg,t.fg),n.theme=t.name.toLowerCase()):(e.textContent="",n.theme="default"),o()}});p.appendChild(r)}),e.append(c,[l,p]),{fontSection:r,themeSection:c,fontSelect:i,customFontInput:d,applyFont:s,themes:u}}function wbCreateDialogsActions(e,t,n){const{settings:o,saveSettings:r}=t,{setEditMode:a,fontSelect:i,customFontInput:d,container:s}=n,c=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),l=e.create("span",{textContent:"Dialogs",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),p=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"6px"}}),u={background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"11px",cursor:"pointer",transition:"all 0.2s"};function m(t,n){return e.button(t,{...u},{mouseover:e=>e.target.style.background="#3f3f46",mouseout:e=>e.target.style.background="#27272a",click:n})}function b(e){if(!s)return void e();const t=s.style.display;s.style.display="none",s.offsetHeight;try{e()}finally{s.style.display=t||"flex"}}const g=m("Alert",()=>{const e=prompt("Alert message:","This is a test alert.");null!==e&&b(()=>alert(e))}),f=m("Confirm",()=>{const e=prompt("Confirm message:","Are you sure?");null!==e&&b(()=>confirm(e))}),w=m("Prompt",()=>{const e=prompt("Prompt question:","Your question?");null!==e&&b(()=>prompt(e,""))});e.append(p,[g,f,w]),e.append(c,[l,p]);const v=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(2, 1fr)",gap:"6px",marginTop:"6px"}}),x=e.button("Reset",{background:"#dc2626",color:"#fff",border:"none",borderRadius:"6px",padding:"8px",fontSize:"11px",fontWeight:"bold",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>x.style.background="#991b1b",mouseout:()=>x.style.background="#dc2626",click:()=>{a(!1),window._webbenderToggleRemove(!1),window._webbenderToggleMove(!1);const e=document.getElementById("webbender-font-style");e&&(e.textContent="");const t=document.getElementById("webbender-theme-style");t&&(t.textContent=""),i.value="",d.value="",o.editMode=!1,o.moveMode=!1,o.removeMode=!1,o.customFont="",o.theme="default",r()}}),y=e.button("Check Updates",{background:"#2563eb",color:"#fff",border:"none",borderRadius:"6px",padding:"8px",fontSize:"11px",fontWeight:"bold",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>y.style.background="#1d4ed8",mouseout:()=>y.style.background="#2563eb",click:()=>{"function"==typeof window._webbenderCheckUpdates&&window._webbenderCheckUpdates()}});return e.append(v,[x,y]),{dialogSection:c,actionRow:v}}function wbRestoreAndAssemble(e,t){const{settings:n,VERSION:o,BUILD_DATE:r,VERSION_URL:a,container:i,header:d,updateBanner:s,updateText:c,editSection:l,moveSection:p,removeSection:u,fontSection:m,themeSection:b,dialogSection:g,actionRow:f,themes:w,setEditMode:v,applyFont:x,customFontInput:y}=t;if(n.theme&&"default"!==n.theme){const e=w.find(e=>e.name.toLowerCase()===n.theme);if(e&&e.bg){wbGetStyleElement("webbender-theme-style").textContent=wbGetThemeCss(e.bg,e.fg)}}n.editMode&&v(!0),n.moveMode&&window._webbenderToggleMove(!0),n.removeMode&&window._webbenderToggleRemove(!0),n.customFont&&(y.value=n.customFont,x(n.customFont));function h(e){const t=e.version===o;c.textContent=t?`Update available on main (newer build for v${o})`:`Update available: v${e.version} (you have v${o})`,s.style.display="flex"}function S(){try{fetch(a,{cache:"no-store"}).then(e=>e.json()).then(e=>{e&&e.version&&(!function(e,t){const n=e.split(".").map(Number),o=t.split(".").map(Number);for(let e=0;e<3;e++){if((n[e]||0)>(o[e]||0))return!0;if((n[e]||0)<(o[e]||0))return!1}return!1}(e.version,o)?e.version===o&&function(e,t){const n=Date.parse(e),o=Date.parse(t);return!(!Number.isFinite(n)||!Number.isFinite(o))&&n>o}(e.buildDate,r)&&h(e):h(e))}).catch(()=>{})}catch(e){}}wbUI().append(i,[d,l,p,u,m,b,g,f,s]),document.body.appendChild(i),window._webbenderCheckUpdates=S,S()}!function(){const e=wbInitState();if(!e)return;const t=wbUI(),n=wbCreateContainer(t,e.ID),{header:o}=wbCreateHeader(t,n),{updateBanner:r,updateText:a}=wbCreateUpdateBanner(t),{editSection:i,moveSection:d,removeSection:s,setEditMode:c}=wbCreateEditRemoveSection(t,n,e),{fontSection:l,themeSection:p,fontSelect:u,customFontInput:m,applyFont:b,themes:g}=wbCreateFontThemeSection(t,e),{dialogSection:f,actionRow:w}=wbCreateDialogsActions(t,e,{setEditMode:c,fontSelect:u,customFontInput:m,container:n});wbRestoreAndAssemble(e,{...e,container:n,header:o,updateBanner:r,updateText:a,editSection:i,moveSection:d,removeSection:s,fontSection:l,themeSection:p,dialogSection:f,actionRow:w,themes:g,setEditMode:c,applyFont:b,customFontInput:m})}(); \ No newline at end of file +function wbUI(){return{create(e,t={}){const n=document.createElement(e);return void 0!==t.textContent&&(n.textContent=t.textContent),t.attrs&&Object.entries(t.attrs).forEach(([e,t])=>{n.setAttribute(e,t)}),t.style&&Object.assign(n.style,t.style),n},append:(e,t)=>(t.forEach(t=>e.appendChild(t)),e),button(e,t,n={}){const o=this.create("button",{textContent:e,style:t});return n.click&&(o.onclick=n.click),n.mouseover&&(o.onmouseover=n.mouseover),n.mouseout&&(o.onmouseout=n.mouseout),o}}}function wbGetStyleElement(e){let t=document.getElementById(e);return t||(t=document.createElement("style"),t.id=e,document.head.appendChild(t)),t}function wbInitState(){const e="webbender-ui",t="webbender-settings",n=document.getElementById(e);if(n)return n.remove(),null;const o={editMode:!1,moveMode:!1,removeMode:!1,customFont:"",theme:"default"};try{const e=localStorage.getItem(t);e&&Object.assign(o,JSON.parse(e))}catch(e){console.warn("Failed to load webbender settings:",e)}return{ID:e,STORAGE_KEY:t,VERSION:"1.0.2",BUILD_DATE:"2026-05-22T09:24:09.245Z",VERSION_URL:"https://webbender.web.app/version.json",settings:o,saveSettings:function(){try{localStorage.setItem(t,JSON.stringify(o))}catch(e){console.warn("Failed to save webbender settings:",e)}}}}function wbCreateContainer(e,t){return e.create("div",{attrs:{id:t},style:{position:"fixed",top:"20px",right:"20px",width:"300px",backgroundColor:"#18181b",color:"#f4f4f5",padding:"16px",borderRadius:"12px",boxShadow:"0 10px 25px -5px rgba(0, 0, 0, 0.5)",zIndex:"2147483647",fontFamily:"-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",fontSize:"13px",userSelect:"none",boxSizing:"border-box",border:"1px solid #27272a",display:"flex",flexDirection:"column",gap:"14px"}})}function wbCreateHeader(e,t){const n=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",borderBottom:"1px solid #27272a",paddingBottom:"8px"}}),o=e.create("span",{textContent:"Webbender",style:{fontWeight:"700",fontSize:"14px"}}),r=e.button("✕",{cursor:"pointer",color:"#71717a",fontWeight:"bold",background:"none",border:"none",fontSize:"16px",padding:"0 4px",transition:"color 0.2s"},{mouseover:()=>{r.style.color="#f4f4f5"},mouseout:()=>{r.style.color="#71717a"},click:()=>{if(window._webbenderRemoveMode&&window._webbenderToggleRemove(!1),window._webbenderMoveMode&&window._webbenderToggleMove(!1),"function"==typeof window._webbenderCloseImmersiveSheet){if(!window._webbenderCloseImmersiveSheet())return}t.remove()}});return e.append(n,[o,r]),{header:n}}function wbCreateUpdateBanner(e){const t=e.create("div",{style:{display:"none",flexDirection:"column",gap:"4px",padding:"8px 10px",borderRadius:"8px",background:"rgba(234, 179, 8, 0.12)",border:"1px solid rgba(234, 179, 8, 0.3)",fontSize:"12px",lineHeight:"1.5"}}),n=e.create("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}}),o=e.create("span",{style:{color:"#fde047"}}),r=e.button("✕",{background:"none",border:"none",color:"#71717a",cursor:"pointer",padding:"0",fontSize:"12px"},{click:()=>t.style.display="none"}),a=e.create("a",{textContent:"Re-install from webbender.web.app →",attrs:{href:"https://webbender.web.app",target:"_blank",rel:"noreferrer"},style:{color:"#93c5fd",fontSize:"11px",display:"block"}});return e.append(n,[o,r]),e.append(t,[n,a]),{updateBanner:t,updateText:o}}function wbCreateEditRemoveSection(e,t,n){const{settings:o,saveSettings:r}=n,a=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"8px"}}),i=e.create("span",{textContent:"Immersive Edit",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),s=e.button("Start Immersive Edit",{background:"#22c55e",color:"#052e16",border:"none",borderRadius:"8px",padding:"8px",fontSize:"12px",fontWeight:"700",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>s.style.background="#4ade80",mouseout:()=>s.style.background="#22c55e",click:()=>function(){if(u)return;u=e.create("div",{attrs:{id:"webbender-immersive-sheet"},style:{position:"fixed",left:"50%",bottom:"16px",transform:"translateX(-50%)",width:"min(720px, calc(100vw - 24px))",background:"#0f172a",border:"1px solid #334155",borderRadius:"12px",padding:"10px",zIndex:"2147483646",color:"#f8fafc",boxShadow:"0 10px 35px rgba(0,0,0,0.5)",display:"flex",flexDirection:"column",gap:"8px"}});const t=e.create("div",{textContent:"Immersive Actions",style:{cursor:"move",textAlign:"center",fontSize:"11px",color:"#94a3b8",borderBottom:"1px solid #1e293b",paddingBottom:"6px"}}),n=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(12, 34px)",gap:"6px",justifyContent:"center"}});b=e.create("div",{style:{border:"1px solid #334155",borderRadius:"8px",padding:"8px",minHeight:"44px",fontSize:"11px",color:"#cbd5e1"}});const o=N("🖱️","Select",()=>T("select")),r=N("✋","Pan",()=>T("pan")),a=N("T","Text",()=>{const t=e.create("div",{textContent:"Editable text",attrs:{contenteditable:"true"},style:{padding:"4px",border:"1px dashed #60a5fa",minHeight:"24px"}}),n=m;M(()=>R(n,t),()=>t.remove()),C(t),T("select")}),i=N("⬜","Shapes",()=>{const t=e.create("div",{style:{width:"120px",height:"80px",background:"#38bdf8",borderRadius:"8px",border:"1px solid #0ea5e9"}}),n=m;M(()=>R(n,t),()=>t.remove()),C(t)}),d=N("🖼️","Image",()=>{const t=prompt("Image URL:","https://");if(!t)return;const n=e.create("img",{attrs:{src:t,alt:"Immersive inserted image"},style:{maxWidth:"320px",borderRadius:"8px",border:"1px solid #334155"}}),o=m;M(()=>R(o,n),()=>n.remove()),C(n)}),l=N("⧉","Duplicate",()=>{if(!m)return;f=m.cloneNode(!0);const e=f.cloneNode(!0),t=m;M(()=>R(t,e),()=>e.remove()),C(e)}),c=N("🗑️","Delete",()=>{if(!m||!m.parentNode)return;const e=m,t=e.parentNode,n=e.nextSibling;M(()=>e.remove(),()=>t.insertBefore(e,n)),C(null)}),p=N("🎨","Color picker",()=>{T("color"),E()}),g=N("⚙️","Options",()=>E()),w=N("↶","Undo",()=>function(){const e=v.pop();if(!e)return;e.undoAction(),y.push(e),k(h.length>0&&v.length>0)}()),x=N("↷","Redo",()=>function(){const e=y.pop();if(!e)return;e.doAction(),v.push(e),k(!0)}());A=N("✅","Save",()=>{v.length=0,y.length=0,h.length=0,k(!1)});const S=N("❌","Close",()=>window._webbenderCloseImmersiveSheet());S.style.background="#7f1d1d",S.style.color="#fecaca",z.set("select",o),z.set("pan",r),z.set("color",p),e.append(n,[w,x,o,r,a,i,d,l,c,p,g,A,S]),e.append(u,[t,n,b]),document.body.appendChild(u),document.addEventListener("click",D,!0),document.addEventListener("mousedown",_,!0),document.addEventListener("mousemove",B,!0),document.addEventListener("mouseup",F,!0),T("select"),E(),k(!1),function(e,t){let n=!1,o=0,r=0;t.onmousedown=t=>{n=!0,e.style.transform="none",o=t.clientX-e.getBoundingClientRect().left,r=t.clientY-e.getBoundingClientRect().top,t.preventDefault()},document.addEventListener("mousemove",t=>{n&&e===u&&(e.style.left=`${Math.max(8,Math.min(window.innerWidth-e.offsetWidth-8,t.clientX-o))}px`,e.style.top=`${Math.max(8,Math.min(window.innerHeight-e.offsetHeight-8,t.clientY-r))}px`,e.style.bottom="auto")}),document.addEventListener("mouseup",()=>{n=!1})}(u,t),s.textContent="Immersive Edit Running"}()}),d=e.create("span",{textContent:"Paint-style sheet with icon tools appears at the bottom.",style:{color:"#71717a",fontSize:"10px"}});e.append(a,[i,s,d]);const l=e.create("div",{style:{display:"none"}}),c=e.create("div",{style:{display:"none"}}),p=e.create("div",{style:{display:"none"}});o.editMode=!1,o.moveMode=!1,o.removeMode=!1,r();let u=null,b=null,m=null,f=null,g="select",w=!1,x=null;const v=[],y=[],h=[];function S(e){return!e||e===t||t.contains(e)||e===u||u&&u.contains(e)||"HTML"===e.tagName||"BODY"===e.tagName}function C(e){!function(e){e&&void 0!==e.dataset.webbenderOutlineBackup&&(e.style.outline=e.dataset.webbenderOutlineBackup,delete e.dataset.webbenderOutlineBackup)}(m),m=S(e)?null:e,m&&function(e,t){e&&void 0===e.dataset.webbenderOutlineBackup&&(e.dataset.webbenderOutlineBackup=e.style.outline||"",e.style.outline=t)}(m,"2px dashed #60a5fa"),E()}function k(e){w=e,A&&(A.style.background=w?"#22c55e":"#14532d",A.style.color=w?"#052e16":"#86efac")}function M(e,t){e(),v.push({doAction:e,undoAction:t}),h.push({doAction:e,undoAction:t}),y.length=0,k(!0)}function R(e,t){e&&e.parentNode?e.parentNode.insertBefore(t,e.nextSibling):document.body.appendChild(t)}function I(e,t,n){if(!e)return;const o=e.style[t]||"";o!==n&&M(()=>e.style[t]=n,()=>e.style[t]=o)}function E(){if(!b)return;if(b.innerHTML="",!m)return void(b.textContent="Pick an element to edit options.");const t=m.tagName.toLowerCase(),n=e.create("div",{textContent:`Selected: <${t}>`,style:{color:"#a1a1aa",fontSize:"10px"}});b.appendChild(n);const o=e.create("input",{attrs:{type:"color",value:"#2563eb","aria-label":"Color picker"},style:{width:"100%",height:"30px",border:"none",background:"transparent"}});if(o.oninput=e=>{if(!m)return;const t=e.target.value,n="IMG"===m.tagName?"borderColor":"color";I(m,n,t)},b.appendChild(o),"IMG"===m.tagName){const t=e.create("input",{attrs:{type:"range",min:"80",max:"1200",value:String(Math.round(m.getBoundingClientRect().width||300)),"aria-label":"Image width"},style:{width:"100%"}});t.onchange=e=>{const t=m;if(!t)return;const n=t.style.width||"",o=`${e.target.value}px`;M(()=>t.style.width=o,()=>t.style.width=n)},b.appendChild(t)}else{const t=e.button("Rounded +",{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"10px",cursor:"pointer",width:"100%"},{click:()=>{if(!m)return;const e=parseFloat(window.getComputedStyle(m).borderRadius||"0")||0;I(m,"borderRadius",`${e+4}px`)}});b.appendChild(t)}}function T(e){g=e,z.forEach((t,n)=>{t.style.background=n===e?"#3f3f46":"#27272a"})}function D(e){S(e.target)||"select"===g&&(C(e.target),e.preventDefault(),e.stopPropagation())}function _(e){if("pan"!==g||S(e.target)||0!==e.button)return;C(e.target);const t=m;if(!t)return;const n=e.clientX,o=e.clientY,r=t.style.transform||"",a=parseFloat(t.dataset.webbenderPanX||"0"),i=parseFloat(t.dataset.webbenderPanY||"0");x={target:t,startX:n,startY:o,baseX:a,baseY:i,startTransform:r},e.preventDefault(),e.stopPropagation()}function B(e){if(!x)return;const t=e.clientX-x.startX,n=e.clientY-x.startY,o=x.baseX+t,r=x.baseY+n;x.target.dataset.webbenderPanX=String(o),x.target.dataset.webbenderPanY=String(r),x.target.style.transform=`translate(${o}px, ${r}px)`,e.preventDefault(),e.stopPropagation()}function F(e){if(!x)return;const t=x;x=null;const n=t.target.style.transform||"",o=t.target.dataset.webbenderPanX||"0",r=t.target.dataset.webbenderPanY||"0";n!==t.startTransform&&M(()=>{t.target.style.transform=n,t.target.dataset.webbenderPanX=o,t.target.dataset.webbenderPanY=r},()=>{t.target.style.transform=t.startTransform,t.target.dataset.webbenderPanX=String(t.baseX),t.target.dataset.webbenderPanY=String(t.baseY)}),e.preventDefault(),e.stopPropagation()}window._webbenderMoveMode=!1,window._webbenderRemoveMode=!1,window._webbenderDrawMode=!1,window._webbenderToggleMove=()=>{window._webbenderMoveMode=!1,o.moveMode=!1,r()},window._webbenderToggleRemove=()=>{window._webbenderRemoveMode=!1,o.removeMode=!1,r()},window._webbenderToggleDraw=()=>{window._webbenderDrawMode=!1};const z=new Map;let A=null;function N(t,n,o){const r=e.create("button",{textContent:t,attrs:{title:n,"aria-label":n},style:{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",width:"34px",height:"34px",fontSize:"16px",cursor:"pointer"}});return r.onclick=o,r}return window._webbenderCloseImmersiveSheet=function(){if(!u)return!0;if(w){if(!confirm("Delete unsaved immersive edits?"))return!1;!function(){for(let e=v.length-1;e>=0;e--)v[e].undoAction();v.length=0,y.length=0,h.length=0,k(!1)}()}return u&&(document.removeEventListener("click",D,!0),document.removeEventListener("mousedown",_,!0),document.removeEventListener("mousemove",B,!0),document.removeEventListener("mouseup",F,!0),C(null),u.remove(),u=null,x=null,s.textContent="Start Immersive Edit"),!0},{editSection:l,moveSection:c,removeSection:p,immersiveSection:a,setEditMode:function(e){document.designMode=e?"on":"off",document.body.contentEditable=e?"true":"false",o.editMode=e,r()}}}function wbGetThemeCss(e,t){return`* { background: ${e} !important; color: ${t} !important; border-color: rgba(128, 128, 128, 0.2) !important; background-image: none !important; } html, body { background: ${e} !important; background-image: none !important; }`}function wbCreateFontThemeSection(e,t){const{settings:n,saveSettings:o}=t,r=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),a=e.create("span",{textContent:"Typography",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),i=e.create("select",{style:{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"13px",cursor:"pointer"}});[["Default",""],["Sans-Serif","-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif"],["Serif","Georgia, Times New Roman, serif"],["Monospace","Courier New, monospace"],["Comic Sans","Comic Sans MS, Arial, sans-serif"]].forEach(([t,n])=>{const o=e.create("option",{textContent:t});o.value=n,i.appendChild(o)});const s=e.create("input",{attrs:{type:"text",placeholder:"Custom font..."},style:{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"12px"}});function d(e){wbGetStyleElement("webbender-font-style").textContent=e?`* { font-family: "${e}" !important; }`:""}i.onchange=e=>{s.value="",d(e.target.value),n.customFont=e.target.value,o()},s.oninput=e=>{i.value="",d(e.target.value),n.customFont=e.target.value,o()},e.append(r,[a,i,s]);const l=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),c=e.create("span",{textContent:"Color Theme",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),p=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(2, 1fr)",gap:"6px"}}),u=[{name:"Default",bg:"",fg:""},{name:"Dark",bg:"#121212",fg:"#e4e4e7"},{name:"Light",bg:"#ffffff",fg:"#18181b"},{name:"Sepia",bg:"#f4ecd8",fg:"#433422"}];return u.forEach(t=>{const r=e.button(t.name,{background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"11px",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>r.style.background="#3f3f46",mouseout:()=>r.style.background="#27272a",click:()=>{const e=wbGetStyleElement("webbender-theme-style");t.bg?(e.textContent=wbGetThemeCss(t.bg,t.fg),n.theme=t.name.toLowerCase()):(e.textContent="",n.theme="default"),o()}});p.appendChild(r)}),e.append(l,[c,p]),{fontSection:r,themeSection:l,fontSelect:i,customFontInput:s,applyFont:d,themes:u}}function wbCreateDialogsActions(e,t,n){const{settings:o,saveSettings:r}=t,{setEditMode:a,fontSelect:i,customFontInput:s,container:d}=n,l=e.create("div",{style:{display:"flex",flexDirection:"column",gap:"6px"}}),c=e.create("span",{textContent:"Dialogs",style:{color:"#a1a1aa",fontSize:"11px",fontWeight:"600",textTransform:"uppercase"}}),p=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"6px"}}),u={background:"#27272a",color:"#f4f4f5",border:"1px solid #3f3f46",borderRadius:"6px",padding:"6px",fontSize:"11px",cursor:"pointer",transition:"all 0.2s"};function b(t,n){return e.button(t,{...u},{mouseover:e=>e.target.style.background="#3f3f46",mouseout:e=>e.target.style.background="#27272a",click:n})}function m(e){if(!d)return void e();const t=d.style.display;d.style.display="none",d.offsetHeight;try{e()}finally{d.style.display=t||"flex"}}const f=b("Alert",()=>{const e=prompt("Alert message:","This is a test alert.");null!==e&&m(()=>alert(e))}),g=b("Confirm",()=>{const e=prompt("Confirm message:","Are you sure?");null!==e&&m(()=>confirm(e))}),w=b("Prompt",()=>{const e=prompt("Prompt question:","Your question?");null!==e&&m(()=>prompt(e,""))});e.append(p,[f,g,w]),e.append(l,[c,p]);const x=e.create("div",{style:{display:"grid",gridTemplateColumns:"repeat(2, 1fr)",gap:"6px",marginTop:"6px"}}),v=e.button("Reset",{background:"#dc2626",color:"#fff",border:"none",borderRadius:"6px",padding:"8px",fontSize:"11px",fontWeight:"bold",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>v.style.background="#991b1b",mouseout:()=>v.style.background="#dc2626",click:()=>{if("function"==typeof window._webbenderCloseImmersiveSheet&&!window._webbenderCloseImmersiveSheet())return;a(!1),window._webbenderToggleRemove(!1),window._webbenderToggleMove(!1);const e=document.getElementById("webbender-font-style");e&&(e.textContent="");const t=document.getElementById("webbender-theme-style");t&&(t.textContent=""),i.value="",s.value="",o.editMode=!1,o.moveMode=!1,o.removeMode=!1,o.customFont="",o.theme="default",r()}}),y=e.button("Check Updates",{background:"#2563eb",color:"#fff",border:"none",borderRadius:"6px",padding:"8px",fontSize:"11px",fontWeight:"bold",cursor:"pointer",transition:"all 0.2s"},{mouseover:()=>y.style.background="#1d4ed8",mouseout:()=>y.style.background="#2563eb",click:()=>{"function"==typeof window._webbenderCheckUpdates&&window._webbenderCheckUpdates()}});return e.append(x,[v,y]),{dialogSection:l,actionRow:x}}function wbRestoreAndAssemble(e,t){const{settings:n,VERSION:o,BUILD_DATE:r,VERSION_URL:a,container:i,header:s,updateBanner:d,updateText:l,editSection:c,moveSection:p,removeSection:u,immersiveSection:b,fontSection:m,themeSection:f,dialogSection:g,actionRow:w,themes:x,setEditMode:v,applyFont:y,customFontInput:h}=t;if(n.theme&&"default"!==n.theme){const e=x.find(e=>e.name.toLowerCase()===n.theme);if(e&&e.bg){wbGetStyleElement("webbender-theme-style").textContent=wbGetThemeCss(e.bg,e.fg)}}n.editMode&&v(!0),n.moveMode&&window._webbenderToggleMove(!0),n.removeMode&&window._webbenderToggleRemove(!0),n.customFont&&(h.value=n.customFont,y(n.customFont));function S(e){const t=e.version===o;l.textContent=t?`Update available on main (newer build for v${o})`:`Update available: v${e.version} (you have v${o})`,d.style.display="flex"}function C(){try{fetch(a,{cache:"no-store"}).then(e=>e.json()).then(e=>{e&&e.version&&(!function(e,t){const n=e.split(".").map(Number),o=t.split(".").map(Number);for(let e=0;e<3;e++){if((n[e]||0)>(o[e]||0))return!0;if((n[e]||0)<(o[e]||0))return!1}return!1}(e.version,o)?e.version===o&&function(e,t){const n=Date.parse(e),o=Date.parse(t);return!(!Number.isFinite(n)||!Number.isFinite(o))&&n>o}(e.buildDate,r)&&S(e):S(e))}).catch(()=>{})}catch(e){}}wbUI().append(i,[s,c,p,u,b,m,f,g,w,d]),document.body.appendChild(i),window._webbenderCheckUpdates=C,C()}!function(){const e=wbInitState();if(!e)return;const t=wbUI(),n=wbCreateContainer(t,e.ID),{header:o}=wbCreateHeader(t,n),{updateBanner:r,updateText:a}=wbCreateUpdateBanner(t),{editSection:i,moveSection:s,removeSection:d,immersiveSection:l,setEditMode:c}=wbCreateEditRemoveSection(t,n,e),{fontSection:p,themeSection:u,fontSelect:b,customFontInput:m,applyFont:f,themes:g}=wbCreateFontThemeSection(t,e),{dialogSection:w,actionRow:x}=wbCreateDialogsActions(t,e,{setEditMode:c,fontSelect:b,customFontInput:m,container:n});wbRestoreAndAssemble(e,{...e,container:n,header:o,updateBanner:r,updateText:a,editSection:i,moveSection:s,removeSection:d,immersiveSection:l,fontSection:p,themeSection:u,dialogSection:w,actionRow:x,themes:g,setEditMode:c,applyFont:f,customFontInput:m})}(); \ No newline at end of file diff --git a/site/version.json b/site/version.json index aa2a7a0..a70bc80 100644 --- a/site/version.json +++ b/site/version.json @@ -1,4 +1,4 @@ { "version": "1.0.2", - "buildDate": "2026-05-20T14:10:23.296Z" + "buildDate": "2026-05-22T09:24:09.245Z" } \ No newline at end of file diff --git a/src/bookmarklet/20-sections.js b/src/bookmarklet/20-sections.js index da864fb..62b8b86 100644 --- a/src/bookmarklet/20-sections.js +++ b/src/bookmarklet/20-sections.js @@ -66,6 +66,10 @@ function wbCreateHeader(ui, container) { if (window._webbenderMoveMode) { window._webbenderToggleMove(false); } + if (typeof window._webbenderCloseImmersiveSheet === 'function') { + const canClose = window._webbenderCloseImmersiveSheet(); + if (!canClose) return; + } container.remove(); }, } @@ -125,14 +129,65 @@ function wbCreateUpdateBanner(ui) { function wbCreateEditRemoveSection(ui, container, state) { const { settings, saveSettings } = state; - const MOVE_OUTLINE = '2px solid #22c55e'; - const REMOVE_OUTLINE = '2px solid #ef4444'; + const SELECT_OUTLINE = '2px dashed #60a5fa'; + const immersiveSection = ui.create('div', { + style: { display: 'flex', flexDirection: 'column', gap: '8px' }, + }); + const immersiveLabel = ui.create('span', { + textContent: 'Immersive Edit', + style: { color: '#a1a1aa', fontSize: '11px', fontWeight: '600', textTransform: 'uppercase' }, + }); + const startBtn = ui.button( + 'Start Immersive Edit', + { + background: '#22c55e', + color: '#052e16', + border: 'none', + borderRadius: '8px', + padding: '8px', + fontSize: '12px', + fontWeight: '700', + cursor: 'pointer', + transition: 'all 0.2s', + }, + { + mouseover: () => (startBtn.style.background = '#4ade80'), + mouseout: () => (startBtn.style.background = '#22c55e'), + click: () => openImmersivePanel(), + } + ); + const immersiveHint = ui.create('span', { + textContent: 'Paint-style sheet with icon tools appears at the bottom.', + style: { color: '#71717a', fontSize: '10px' }, + }); + ui.append(immersiveSection, [immersiveLabel, startBtn, immersiveHint]); + + const editSection = ui.create('div', { style: { display: 'none' } }); + const moveSection = ui.create('div', { style: { display: 'none' } }); + const removeSection = ui.create('div', { style: { display: 'none' } }); + settings.editMode = false; + settings.moveMode = false; + settings.removeMode = false; + saveSettings(); + + let immersivePanel = null; + let optionsBody = null; + let selectedElement = null; + let copiedElement = null; + let activeTool = 'select'; + let unsavedChanges = false; + let pointerDrag = null; + const undoStack = []; + const redoStack = []; + const sessionStack = []; function isBookmarkletElement(target) { return ( !target || target === container || container.contains(target) || + target === immersivePanel || + (immersivePanel && immersivePanel.contains(target)) || target.tagName === 'HTML' || target.tagName === 'BODY' ); @@ -150,233 +205,480 @@ function wbCreateEditRemoveSection(ui, container, state) { delete target.dataset.webbenderOutlineBackup; } - function applyMoveTransform(target, x, y) { - const baseTransform = target.dataset.webbenderBaseTransform || ''; - const translate = `translate(${x}px, ${y}px)`; - target.style.transform = baseTransform ? `${translate} ${baseTransform}` : translate; - target.dataset.webbenderMoveX = String(x); - target.dataset.webbenderMoveY = String(y); + function setSelectedElement(target) { + clearOutline(selectedElement); + selectedElement = isBookmarkletElement(target) ? null : target; + if (selectedElement) setOutline(selectedElement, SELECT_OUTLINE); + refreshOptions(); } - const editSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const editLabel = ui.create('label', { - textContent: 'Edit Text (Design Mode)', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const editToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - editToggle.checked = document.designMode === 'on'; + function markUnsaved(value) { + unsavedChanges = value; + if (saveBtn) { + saveBtn.style.background = unsavedChanges ? '#22c55e' : '#14532d'; + saveBtn.style.color = unsavedChanges ? '#052e16' : '#86efac'; + } + } function setEditMode(enabled) { - if (enabled && window._webbenderRemoveMode) { - window._webbenderToggleRemove(false); - } - if (enabled && window._webbenderMoveMode) { - window._webbenderToggleMove(false); - } document.designMode = enabled ? 'on' : 'off'; document.body.contentEditable = enabled ? 'true' : 'false'; - editToggle.checked = enabled; settings.editMode = enabled; saveSettings(); } - editToggle.onchange = (e) => setEditMode(e.target.checked); - editLabel.appendChild(editToggle); - editSection.appendChild(editLabel); - - const moveSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const moveLabel = ui.create('label', { - textContent: 'Grab & Move', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const moveToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - - const removeSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const removeLabel = ui.create('label', { - textContent: 'Remove Elements', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const removeToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - - let moveHoverTarget = null; - let activeMoveTarget = null; window._webbenderMoveMode = false; window._webbenderRemoveMode = false; + window._webbenderDrawMode = false; + window._webbenderToggleMove = () => { + window._webbenderMoveMode = false; + settings.moveMode = false; + saveSettings(); + }; + window._webbenderToggleRemove = () => { + window._webbenderRemoveMode = false; + settings.removeMode = false; + saveSettings(); + }; + window._webbenderToggleDraw = () => { + window._webbenderDrawMode = false; + }; - function setMoveHoverTarget(target) { - if (moveHoverTarget === target) return; - clearOutline(moveHoverTarget); - moveHoverTarget = target; - if (moveHoverTarget) { - setOutline(moveHoverTarget, MOVE_OUTLINE); - } + function createOperation(doAction, undoAction) { + doAction(); + undoStack.push({ doAction, undoAction }); + sessionStack.push({ doAction, undoAction }); + redoStack.length = 0; + markUnsaved(true); } - const moveHoverHandler = (e) => { - if (activeMoveTarget || isBookmarkletElement(e.target)) return; - setMoveHoverTarget(e.target); - }; - const moveLeaveHandler = (e) => { - if (activeMoveTarget || e.target !== moveHoverTarget) return; - setMoveHoverTarget(null); - }; - const moveDownHandler = (e) => { - if (e.button !== 0 || isBookmarkletElement(e.target)) return; + function undo() { + const op = undoStack.pop(); + if (!op) return; + op.undoAction(); + redoStack.push(op); + markUnsaved(sessionStack.length > 0 && undoStack.length > 0); + } - const rect = e.target.getBoundingClientRect(); - const moveX = parseFloat(e.target.dataset.webbenderMoveX || '0'); - const moveY = parseFloat(e.target.dataset.webbenderMoveY || '0'); + function redo() { + const op = redoStack.pop(); + if (!op) return; + op.doAction(); + undoStack.push(op); + markUnsaved(true); + } - if (e.target.dataset.webbenderBaseTransform === undefined) { - e.target.dataset.webbenderBaseTransform = e.target.style.transform || ''; + function insertNodeAfter(target, node) { + if (target && target.parentNode) { + target.parentNode.insertBefore(node, target.nextSibling); + } else { + document.body.appendChild(node); } + } - activeMoveTarget = { - target: e.target, - startX: e.clientX, - startY: e.clientY, - moveX, - moveY, - minDeltaX: -rect.right + 1, - maxDeltaX: window.innerWidth - rect.left - 1, - minDeltaY: -rect.bottom + 1, - maxDeltaY: window.innerHeight - rect.top - 1, - }; + function applyStyleWithUndo(target, prop, value) { + if (!target) return; + const previous = target.style[prop] || ''; + if (previous === value) return; + createOperation( + () => (target.style[prop] = value), + () => (target.style[prop] = previous) + ); + } - setMoveHoverTarget(e.target); - e.preventDefault(); - e.stopPropagation(); - }; - const moveHandler = (e) => { - if (!activeMoveTarget) return; + function refreshOptions() { + if (!optionsBody) return; + optionsBody.innerHTML = ''; + if (!selectedElement) { + optionsBody.textContent = 'Pick an element to edit options.'; + return; + } - const deltaX = Math.min( - Math.max(e.clientX - activeMoveTarget.startX, activeMoveTarget.minDeltaX), - activeMoveTarget.maxDeltaX - ); - const deltaY = Math.min( - Math.max(e.clientY - activeMoveTarget.startY, activeMoveTarget.minDeltaY), - activeMoveTarget.maxDeltaY - ); + const elementTag = selectedElement.tagName.toLowerCase(); + const info = ui.create('div', { + textContent: `Selected: <${elementTag}>`, + style: { color: '#a1a1aa', fontSize: '10px' }, + }); + optionsBody.appendChild(info); + + const colorInput = ui.create('input', { + attrs: { type: 'color', value: '#2563eb', 'aria-label': 'Color picker' }, + style: { width: '100%', height: '30px', border: 'none', background: 'transparent' }, + }); + colorInput.oninput = (e) => { + if (!selectedElement) return; + const next = e.target.value; + const prop = selectedElement.tagName === 'IMG' ? 'borderColor' : 'color'; + applyStyleWithUndo(selectedElement, prop, next); + }; + optionsBody.appendChild(colorInput); + + if (selectedElement.tagName === 'IMG') { + const widthInput = ui.create('input', { + attrs: { + type: 'range', + min: '80', + max: '1200', + value: String(Math.round(selectedElement.getBoundingClientRect().width || 300)), + 'aria-label': 'Image width', + }, + style: { width: '100%' }, + }); + widthInput.onchange = (e) => { + const target = selectedElement; + if (!target) return; + const previous = target.style.width || ''; + const next = `${e.target.value}px`; + createOperation( + () => (target.style.width = next), + () => (target.style.width = previous) + ); + }; + optionsBody.appendChild(widthInput); + } else { + const radiusBtn = ui.button( + 'Rounded +', + { + background: '#27272a', + color: '#f4f4f5', + border: '1px solid #3f3f46', + borderRadius: '6px', + padding: '6px', + fontSize: '10px', + cursor: 'pointer', + width: '100%', + }, + { + click: () => { + if (!selectedElement) return; + const radius = + parseFloat(window.getComputedStyle(selectedElement).borderRadius || '0') || 0; + applyStyleWithUndo(selectedElement, 'borderRadius', `${radius + 4}px`); + }, + } + ); + optionsBody.appendChild(radiusBtn); + } + } - applyMoveTransform( - activeMoveTarget.target, - activeMoveTarget.moveX + deltaX, - activeMoveTarget.moveY + deltaY - ); + function setActiveTool(name) { + activeTool = name; + toolButtons.forEach((button, key) => { + button.style.background = key === name ? '#3f3f46' : '#27272a'; + }); + } - e.preventDefault(); - e.stopPropagation(); - }; - const moveUpHandler = (e) => { - if (!activeMoveTarget) return; - setMoveHoverTarget(activeMoveTarget.target); - activeMoveTarget = null; - e.preventDefault(); - e.stopPropagation(); - }; - const moveClickHandler = (e) => { + function onDocumentClick(e) { if (isBookmarkletElement(e.target)) return; + if (activeTool === 'select') { + setSelectedElement(e.target); + e.preventDefault(); + e.stopPropagation(); + } + } + + function onDocumentMouseDown(e) { + if (activeTool !== 'pan' || isBookmarkletElement(e.target) || e.button !== 0) return; + setSelectedElement(e.target); + const target = selectedElement; + if (!target) return; + const startX = e.clientX; + const startY = e.clientY; + const startTransform = target.style.transform || ''; + const baseX = parseFloat(target.dataset.webbenderPanX || '0'); + const baseY = parseFloat(target.dataset.webbenderPanY || '0'); + pointerDrag = { target, startX, startY, baseX, baseY, startTransform }; e.preventDefault(); e.stopPropagation(); - }; + } - const hoverHandler = (e) => { - if (isBookmarkletElement(e.target)) return; - setOutline(e.target, REMOVE_OUTLINE); - }; - const leaveHandler = (e) => { - if (isBookmarkletElement(e.target)) return; - clearOutline(e.target); - }; - const clickHandler = (e) => { - if (isBookmarkletElement(e.target)) return; + function onDocumentMouseMove(e) { + if (!pointerDrag) return; + const dx = e.clientX - pointerDrag.startX; + const dy = e.clientY - pointerDrag.startY; + const x = pointerDrag.baseX + dx; + const y = pointerDrag.baseY + dy; + pointerDrag.target.dataset.webbenderPanX = String(x); + pointerDrag.target.dataset.webbenderPanY = String(y); + pointerDrag.target.style.transform = `translate(${x}px, ${y}px)`; e.preventDefault(); e.stopPropagation(); - clearOutline(e.target); - e.target.remove(); - }; + } - window._webbenderToggleMove = function (enabled) { - if (enabled && document.designMode === 'on') { - setEditMode(false); - } - if (enabled && window._webbenderRemoveMode) { - window._webbenderToggleRemove(false); + function onDocumentMouseUp(e) { + if (!pointerDrag) return; + const drag = pointerDrag; + pointerDrag = null; + const finalTransform = drag.target.style.transform || ''; + const finalX = drag.target.dataset.webbenderPanX || '0'; + const finalY = drag.target.dataset.webbenderPanY || '0'; + if (finalTransform !== drag.startTransform) { + createOperation( + () => { + drag.target.style.transform = finalTransform; + drag.target.dataset.webbenderPanX = finalX; + drag.target.dataset.webbenderPanY = finalY; + }, + () => { + drag.target.style.transform = drag.startTransform; + drag.target.dataset.webbenderPanX = String(drag.baseX); + drag.target.dataset.webbenderPanY = String(drag.baseY); + } + ); } + e.preventDefault(); + e.stopPropagation(); + } - window._webbenderMoveMode = enabled; - moveToggle.checked = enabled; - settings.moveMode = enabled; - saveSettings(); - - if (enabled) { - document.addEventListener('mouseover', moveHoverHandler); - document.addEventListener('mouseout', moveLeaveHandler); - document.addEventListener('mousedown', moveDownHandler, true); - document.addEventListener('mousemove', moveHandler, true); - document.addEventListener('mouseup', moveUpHandler, true); - document.addEventListener('click', moveClickHandler, true); - } else { - document.removeEventListener('mouseover', moveHoverHandler); - document.removeEventListener('mouseout', moveLeaveHandler); - document.removeEventListener('mousedown', moveDownHandler, true); - document.removeEventListener('mousemove', moveHandler, true); - document.removeEventListener('mouseup', moveUpHandler, true); - document.removeEventListener('click', moveClickHandler, true); - setMoveHoverTarget(null); - activeMoveTarget = null; - } - }; + function closeImmersivePanel() { + if (!immersivePanel) return; + document.removeEventListener('click', onDocumentClick, true); + document.removeEventListener('mousedown', onDocumentMouseDown, true); + document.removeEventListener('mousemove', onDocumentMouseMove, true); + document.removeEventListener('mouseup', onDocumentMouseUp, true); + setSelectedElement(null); + immersivePanel.remove(); + immersivePanel = null; + pointerDrag = null; + startBtn.textContent = 'Start Immersive Edit'; + } - window._webbenderToggleRemove = function (enabled) { - if (enabled && document.designMode === 'on') { - setEditMode(false); - } - if (enabled && window._webbenderMoveMode) { - window._webbenderToggleMove(false); + function discardUnsaved() { + for (let i = undoStack.length - 1; i >= 0; i--) { + undoStack[i].undoAction(); } + undoStack.length = 0; + redoStack.length = 0; + sessionStack.length = 0; + markUnsaved(false); + } - window._webbenderRemoveMode = enabled; - removeToggle.checked = enabled; - settings.removeMode = enabled; - saveSettings(); - - if (enabled) { - document.addEventListener('mouseover', hoverHandler); - document.addEventListener('mouseout', leaveHandler); - document.addEventListener('click', clickHandler, true); - } else { - document.removeEventListener('mouseover', hoverHandler); - document.removeEventListener('mouseout', leaveHandler); - document.removeEventListener('click', clickHandler, true); + const toolButtons = new Map(); + let saveBtn = null; + window._webbenderCloseImmersiveSheet = function () { + if (!immersivePanel) return true; + if (unsavedChanges) { + const confirmClose = confirm('Delete unsaved immersive edits?'); + if (!confirmClose) return false; + discardUnsaved(); } + closeImmersivePanel(); + return true; }; - moveToggle.onchange = (e) => window._webbenderToggleMove(e.target.checked); - moveLabel.appendChild(moveToggle); - moveSection.appendChild(moveLabel); + function makeIconButton(icon, label, onClick) { + const btn = ui.create('button', { + textContent: icon, + attrs: { title: label, 'aria-label': label }, + style: { + background: '#27272a', + color: '#f4f4f5', + border: '1px solid #3f3f46', + borderRadius: '6px', + width: '34px', + height: '34px', + fontSize: '16px', + cursor: 'pointer', + }, + }); + btn.onclick = onClick; + return btn; + } - removeToggle.onchange = (e) => window._webbenderToggleRemove(e.target.checked); - removeLabel.appendChild(removeToggle); - removeSection.appendChild(removeLabel); + function makeDraggablePanel(panel, handle) { + let dragging = false; + let offsetX = 0; + let offsetY = 0; + handle.onmousedown = (e) => { + dragging = true; + panel.style.transform = 'none'; + offsetX = e.clientX - panel.getBoundingClientRect().left; + offsetY = e.clientY - panel.getBoundingClientRect().top; + e.preventDefault(); + }; + document.addEventListener('mousemove', (e) => { + if (!dragging || panel !== immersivePanel) return; + panel.style.left = `${Math.max(8, Math.min(window.innerWidth - panel.offsetWidth - 8, e.clientX - offsetX))}px`; + panel.style.top = `${Math.max(8, Math.min(window.innerHeight - panel.offsetHeight - 8, e.clientY - offsetY))}px`; + panel.style.bottom = 'auto'; + }); + document.addEventListener('mouseup', () => { + dragging = false; + }); + } + + function openImmersivePanel() { + if (immersivePanel) return; + immersivePanel = ui.create('div', { + attrs: { id: 'webbender-immersive-sheet' }, + style: { + position: 'fixed', + left: '50%', + bottom: '16px', + transform: 'translateX(-50%)', + width: 'min(720px, calc(100vw - 24px))', + background: '#0f172a', + border: '1px solid #334155', + borderRadius: '12px', + padding: '10px', + zIndex: '2147483646', + color: '#f8fafc', + boxShadow: '0 10px 35px rgba(0,0,0,0.5)', + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + }); + const handle = ui.create('div', { + textContent: 'Immersive Actions', + style: { + cursor: 'move', + textAlign: 'center', + fontSize: '11px', + color: '#94a3b8', + borderBottom: '1px solid #1e293b', + paddingBottom: '6px', + }, + }); + const toolbar = ui.create('div', { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(12, 34px)', + gap: '6px', + justifyContent: 'center', + }, + }); + optionsBody = ui.create('div', { + style: { + border: '1px solid #334155', + borderRadius: '8px', + padding: '8px', + minHeight: '44px', + fontSize: '11px', + color: '#cbd5e1', + }, + }); + + const selectBtn = makeIconButton('🖱️', 'Select', () => setActiveTool('select')); + const panBtn = makeIconButton('✋', 'Pan', () => setActiveTool('pan')); + const textBtn = makeIconButton('T', 'Text', () => { + const textNode = ui.create('div', { + textContent: 'Editable text', + attrs: { contenteditable: 'true' }, + style: { padding: '4px', border: '1px dashed #60a5fa', minHeight: '24px' }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, textNode), + () => textNode.remove() + ); + setSelectedElement(textNode); + setActiveTool('select'); + }); + const shapeBtn = makeIconButton('⬜', 'Shapes', () => { + const shape = ui.create('div', { + style: { + width: '120px', + height: '80px', + background: '#38bdf8', + borderRadius: '8px', + border: '1px solid #0ea5e9', + }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, shape), + () => shape.remove() + ); + setSelectedElement(shape); + }); + const imageBtn = makeIconButton('🖼️', 'Image', () => { + const src = prompt('Image URL:', 'https://'); + if (!src) return; + const image = ui.create('img', { + attrs: { src, alt: 'Immersive inserted image' }, + style: { maxWidth: '320px', borderRadius: '8px', border: '1px solid #334155' }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, image), + () => image.remove() + ); + setSelectedElement(image); + }); + const duplicateBtn = makeIconButton('⧉', 'Duplicate', () => { + if (!selectedElement) return; + copiedElement = selectedElement.cloneNode(true); + const clone = copiedElement.cloneNode(true); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, clone), + () => clone.remove() + ); + setSelectedElement(clone); + }); + const deleteBtn = makeIconButton('🗑️', 'Delete', () => { + if (!selectedElement || !selectedElement.parentNode) return; + const node = selectedElement; + const parent = node.parentNode; + const next = node.nextSibling; + createOperation( + () => node.remove(), + () => parent.insertBefore(node, next) + ); + setSelectedElement(null); + }); + const colorBtn = makeIconButton('🎨', 'Color picker', () => { + setActiveTool('color'); + refreshOptions(); + }); + const optionsBtn = makeIconButton('⚙️', 'Options', () => refreshOptions()); + const undoBtn = makeIconButton('↶', 'Undo', () => undo()); + const redoBtn = makeIconButton('↷', 'Redo', () => redo()); + saveBtn = makeIconButton('✅', 'Save', () => { + undoStack.length = 0; + redoStack.length = 0; + sessionStack.length = 0; + markUnsaved(false); + }); + const closeBtn = makeIconButton('❌', 'Close', () => window._webbenderCloseImmersiveSheet()); + closeBtn.style.background = '#7f1d1d'; + closeBtn.style.color = '#fecaca'; + + toolButtons.set('select', selectBtn); + toolButtons.set('pan', panBtn); + toolButtons.set('color', colorBtn); + + ui.append(toolbar, [ + undoBtn, + redoBtn, + selectBtn, + panBtn, + textBtn, + shapeBtn, + imageBtn, + duplicateBtn, + deleteBtn, + colorBtn, + optionsBtn, + saveBtn, + closeBtn, + ]); + ui.append(immersivePanel, [handle, toolbar, optionsBody]); + document.body.appendChild(immersivePanel); + + document.addEventListener('click', onDocumentClick, true); + document.addEventListener('mousedown', onDocumentMouseDown, true); + document.addEventListener('mousemove', onDocumentMouseMove, true); + document.addEventListener('mouseup', onDocumentMouseUp, true); + setActiveTool('select'); + refreshOptions(); + markUnsaved(false); + makeDraggablePanel(immersivePanel, handle); + startBtn.textContent = 'Immersive Edit Running'; + } - return { editSection, moveSection, removeSection, setEditMode }; + return { editSection, moveSection, removeSection, immersiveSection, setEditMode }; } function wbGetThemeCss(bgColor, fgColor) { @@ -607,6 +909,12 @@ function wbCreateDialogsActions(ui, state, controls) { mouseover: () => (resetBtn.style.background = '#991b1b'), mouseout: () => (resetBtn.style.background = '#dc2626'), click: () => { + if ( + typeof window._webbenderCloseImmersiveSheet === 'function' && + !window._webbenderCloseImmersiveSheet() + ) { + return; + } setEditMode(false); window._webbenderToggleRemove(false); window._webbenderToggleMove(false); @@ -667,6 +975,7 @@ function wbRestoreAndAssemble(state, controls) { editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, @@ -698,6 +1007,7 @@ function wbRestoreAndAssemble(state, controls) { editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, diff --git a/src/bookmarklet/99-main.js b/src/bookmarklet/99-main.js index 1cdea22..81a84c9 100644 --- a/src/bookmarklet/99-main.js +++ b/src/bookmarklet/99-main.js @@ -9,11 +9,8 @@ const { header } = wbCreateHeader(ui, container); const { updateBanner, updateText } = wbCreateUpdateBanner(ui); - const { editSection, moveSection, removeSection, setEditMode } = wbCreateEditRemoveSection( - ui, - container, - state - ); + const { editSection, moveSection, removeSection, immersiveSection, setEditMode } = + wbCreateEditRemoveSection(ui, container, state); const { fontSection, themeSection, fontSelect, customFontInput, applyFont, themes } = wbCreateFontThemeSection(ui, state); const { dialogSection, actionRow } = wbCreateDialogsActions(ui, state, { @@ -32,6 +29,7 @@ editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, diff --git a/src/webbender.js b/src/webbender.js index bb3ccd6..a3c2da9 100644 --- a/src/webbender.js +++ b/src/webbender.js @@ -156,6 +156,10 @@ function wbCreateHeader(ui, container) { if (window._webbenderMoveMode) { window._webbenderToggleMove(false); } + if (typeof window._webbenderCloseImmersiveSheet === 'function') { + const canClose = window._webbenderCloseImmersiveSheet(); + if (!canClose) return; + } container.remove(); }, } @@ -215,14 +219,65 @@ function wbCreateUpdateBanner(ui) { function wbCreateEditRemoveSection(ui, container, state) { const { settings, saveSettings } = state; - const MOVE_OUTLINE = '2px solid #22c55e'; - const REMOVE_OUTLINE = '2px solid #ef4444'; + const SELECT_OUTLINE = '2px dashed #60a5fa'; + const immersiveSection = ui.create('div', { + style: { display: 'flex', flexDirection: 'column', gap: '8px' }, + }); + const immersiveLabel = ui.create('span', { + textContent: 'Immersive Edit', + style: { color: '#a1a1aa', fontSize: '11px', fontWeight: '600', textTransform: 'uppercase' }, + }); + const startBtn = ui.button( + 'Start Immersive Edit', + { + background: '#22c55e', + color: '#052e16', + border: 'none', + borderRadius: '8px', + padding: '8px', + fontSize: '12px', + fontWeight: '700', + cursor: 'pointer', + transition: 'all 0.2s', + }, + { + mouseover: () => (startBtn.style.background = '#4ade80'), + mouseout: () => (startBtn.style.background = '#22c55e'), + click: () => openImmersivePanel(), + } + ); + const immersiveHint = ui.create('span', { + textContent: 'Paint-style sheet with icon tools appears at the bottom.', + style: { color: '#71717a', fontSize: '10px' }, + }); + ui.append(immersiveSection, [immersiveLabel, startBtn, immersiveHint]); + + const editSection = ui.create('div', { style: { display: 'none' } }); + const moveSection = ui.create('div', { style: { display: 'none' } }); + const removeSection = ui.create('div', { style: { display: 'none' } }); + settings.editMode = false; + settings.moveMode = false; + settings.removeMode = false; + saveSettings(); + + let immersivePanel = null; + let optionsBody = null; + let selectedElement = null; + let copiedElement = null; + let activeTool = 'select'; + let unsavedChanges = false; + let pointerDrag = null; + const undoStack = []; + const redoStack = []; + const sessionStack = []; function isBookmarkletElement(target) { return ( !target || target === container || container.contains(target) || + target === immersivePanel || + (immersivePanel && immersivePanel.contains(target)) || target.tagName === 'HTML' || target.tagName === 'BODY' ); @@ -240,233 +295,480 @@ function wbCreateEditRemoveSection(ui, container, state) { delete target.dataset.webbenderOutlineBackup; } - function applyMoveTransform(target, x, y) { - const baseTransform = target.dataset.webbenderBaseTransform || ''; - const translate = `translate(${x}px, ${y}px)`; - target.style.transform = baseTransform ? `${translate} ${baseTransform}` : translate; - target.dataset.webbenderMoveX = String(x); - target.dataset.webbenderMoveY = String(y); + function setSelectedElement(target) { + clearOutline(selectedElement); + selectedElement = isBookmarkletElement(target) ? null : target; + if (selectedElement) setOutline(selectedElement, SELECT_OUTLINE); + refreshOptions(); } - const editSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const editLabel = ui.create('label', { - textContent: 'Edit Text (Design Mode)', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const editToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - editToggle.checked = document.designMode === 'on'; + function markUnsaved(value) { + unsavedChanges = value; + if (saveBtn) { + saveBtn.style.background = unsavedChanges ? '#22c55e' : '#14532d'; + saveBtn.style.color = unsavedChanges ? '#052e16' : '#86efac'; + } + } function setEditMode(enabled) { - if (enabled && window._webbenderRemoveMode) { - window._webbenderToggleRemove(false); - } - if (enabled && window._webbenderMoveMode) { - window._webbenderToggleMove(false); - } document.designMode = enabled ? 'on' : 'off'; document.body.contentEditable = enabled ? 'true' : 'false'; - editToggle.checked = enabled; settings.editMode = enabled; saveSettings(); } - editToggle.onchange = (e) => setEditMode(e.target.checked); - editLabel.appendChild(editToggle); - editSection.appendChild(editLabel); - - const moveSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const moveLabel = ui.create('label', { - textContent: 'Grab & Move', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const moveToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - - const removeSection = ui.create('div', { - style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, - }); - const removeLabel = ui.create('label', { - textContent: 'Remove Elements', - style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', flex: '1' }, - }); - const removeToggle = ui.create('input', { - attrs: { type: 'checkbox' }, - style: { cursor: 'pointer', width: '16px', height: '16px' }, - }); - - let moveHoverTarget = null; - let activeMoveTarget = null; window._webbenderMoveMode = false; window._webbenderRemoveMode = false; + window._webbenderDrawMode = false; + window._webbenderToggleMove = () => { + window._webbenderMoveMode = false; + settings.moveMode = false; + saveSettings(); + }; + window._webbenderToggleRemove = () => { + window._webbenderRemoveMode = false; + settings.removeMode = false; + saveSettings(); + }; + window._webbenderToggleDraw = () => { + window._webbenderDrawMode = false; + }; - function setMoveHoverTarget(target) { - if (moveHoverTarget === target) return; - clearOutline(moveHoverTarget); - moveHoverTarget = target; - if (moveHoverTarget) { - setOutline(moveHoverTarget, MOVE_OUTLINE); - } + function createOperation(doAction, undoAction) { + doAction(); + undoStack.push({ doAction, undoAction }); + sessionStack.push({ doAction, undoAction }); + redoStack.length = 0; + markUnsaved(true); } - const moveHoverHandler = (e) => { - if (activeMoveTarget || isBookmarkletElement(e.target)) return; - setMoveHoverTarget(e.target); - }; - const moveLeaveHandler = (e) => { - if (activeMoveTarget || e.target !== moveHoverTarget) return; - setMoveHoverTarget(null); - }; - const moveDownHandler = (e) => { - if (e.button !== 0 || isBookmarkletElement(e.target)) return; + function undo() { + const op = undoStack.pop(); + if (!op) return; + op.undoAction(); + redoStack.push(op); + markUnsaved(sessionStack.length > 0 && undoStack.length > 0); + } - const rect = e.target.getBoundingClientRect(); - const moveX = parseFloat(e.target.dataset.webbenderMoveX || '0'); - const moveY = parseFloat(e.target.dataset.webbenderMoveY || '0'); + function redo() { + const op = redoStack.pop(); + if (!op) return; + op.doAction(); + undoStack.push(op); + markUnsaved(true); + } - if (e.target.dataset.webbenderBaseTransform === undefined) { - e.target.dataset.webbenderBaseTransform = e.target.style.transform || ''; + function insertNodeAfter(target, node) { + if (target && target.parentNode) { + target.parentNode.insertBefore(node, target.nextSibling); + } else { + document.body.appendChild(node); } + } - activeMoveTarget = { - target: e.target, - startX: e.clientX, - startY: e.clientY, - moveX, - moveY, - minDeltaX: -rect.right + 1, - maxDeltaX: window.innerWidth - rect.left - 1, - minDeltaY: -rect.bottom + 1, - maxDeltaY: window.innerHeight - rect.top - 1, - }; + function applyStyleWithUndo(target, prop, value) { + if (!target) return; + const previous = target.style[prop] || ''; + if (previous === value) return; + createOperation( + () => (target.style[prop] = value), + () => (target.style[prop] = previous) + ); + } - setMoveHoverTarget(e.target); - e.preventDefault(); - e.stopPropagation(); - }; - const moveHandler = (e) => { - if (!activeMoveTarget) return; + function refreshOptions() { + if (!optionsBody) return; + optionsBody.innerHTML = ''; + if (!selectedElement) { + optionsBody.textContent = 'Pick an element to edit options.'; + return; + } - const deltaX = Math.min( - Math.max(e.clientX - activeMoveTarget.startX, activeMoveTarget.minDeltaX), - activeMoveTarget.maxDeltaX - ); - const deltaY = Math.min( - Math.max(e.clientY - activeMoveTarget.startY, activeMoveTarget.minDeltaY), - activeMoveTarget.maxDeltaY - ); + const elementTag = selectedElement.tagName.toLowerCase(); + const info = ui.create('div', { + textContent: `Selected: <${elementTag}>`, + style: { color: '#a1a1aa', fontSize: '10px' }, + }); + optionsBody.appendChild(info); + + const colorInput = ui.create('input', { + attrs: { type: 'color', value: '#2563eb', 'aria-label': 'Color picker' }, + style: { width: '100%', height: '30px', border: 'none', background: 'transparent' }, + }); + colorInput.oninput = (e) => { + if (!selectedElement) return; + const next = e.target.value; + const prop = selectedElement.tagName === 'IMG' ? 'borderColor' : 'color'; + applyStyleWithUndo(selectedElement, prop, next); + }; + optionsBody.appendChild(colorInput); + + if (selectedElement.tagName === 'IMG') { + const widthInput = ui.create('input', { + attrs: { + type: 'range', + min: '80', + max: '1200', + value: String(Math.round(selectedElement.getBoundingClientRect().width || 300)), + 'aria-label': 'Image width', + }, + style: { width: '100%' }, + }); + widthInput.onchange = (e) => { + const target = selectedElement; + if (!target) return; + const previous = target.style.width || ''; + const next = `${e.target.value}px`; + createOperation( + () => (target.style.width = next), + () => (target.style.width = previous) + ); + }; + optionsBody.appendChild(widthInput); + } else { + const radiusBtn = ui.button( + 'Rounded +', + { + background: '#27272a', + color: '#f4f4f5', + border: '1px solid #3f3f46', + borderRadius: '6px', + padding: '6px', + fontSize: '10px', + cursor: 'pointer', + width: '100%', + }, + { + click: () => { + if (!selectedElement) return; + const radius = + parseFloat(window.getComputedStyle(selectedElement).borderRadius || '0') || 0; + applyStyleWithUndo(selectedElement, 'borderRadius', `${radius + 4}px`); + }, + } + ); + optionsBody.appendChild(radiusBtn); + } + } - applyMoveTransform( - activeMoveTarget.target, - activeMoveTarget.moveX + deltaX, - activeMoveTarget.moveY + deltaY - ); + function setActiveTool(name) { + activeTool = name; + toolButtons.forEach((button, key) => { + button.style.background = key === name ? '#3f3f46' : '#27272a'; + }); + } - e.preventDefault(); - e.stopPropagation(); - }; - const moveUpHandler = (e) => { - if (!activeMoveTarget) return; - setMoveHoverTarget(activeMoveTarget.target); - activeMoveTarget = null; - e.preventDefault(); - e.stopPropagation(); - }; - const moveClickHandler = (e) => { + function onDocumentClick(e) { if (isBookmarkletElement(e.target)) return; + if (activeTool === 'select') { + setSelectedElement(e.target); + e.preventDefault(); + e.stopPropagation(); + } + } + + function onDocumentMouseDown(e) { + if (activeTool !== 'pan' || isBookmarkletElement(e.target) || e.button !== 0) return; + setSelectedElement(e.target); + const target = selectedElement; + if (!target) return; + const startX = e.clientX; + const startY = e.clientY; + const startTransform = target.style.transform || ''; + const baseX = parseFloat(target.dataset.webbenderPanX || '0'); + const baseY = parseFloat(target.dataset.webbenderPanY || '0'); + pointerDrag = { target, startX, startY, baseX, baseY, startTransform }; e.preventDefault(); e.stopPropagation(); - }; + } - const hoverHandler = (e) => { - if (isBookmarkletElement(e.target)) return; - setOutline(e.target, REMOVE_OUTLINE); - }; - const leaveHandler = (e) => { - if (isBookmarkletElement(e.target)) return; - clearOutline(e.target); - }; - const clickHandler = (e) => { - if (isBookmarkletElement(e.target)) return; + function onDocumentMouseMove(e) { + if (!pointerDrag) return; + const dx = e.clientX - pointerDrag.startX; + const dy = e.clientY - pointerDrag.startY; + const x = pointerDrag.baseX + dx; + const y = pointerDrag.baseY + dy; + pointerDrag.target.dataset.webbenderPanX = String(x); + pointerDrag.target.dataset.webbenderPanY = String(y); + pointerDrag.target.style.transform = `translate(${x}px, ${y}px)`; e.preventDefault(); e.stopPropagation(); - clearOutline(e.target); - e.target.remove(); - }; + } - window._webbenderToggleMove = function (enabled) { - if (enabled && document.designMode === 'on') { - setEditMode(false); - } - if (enabled && window._webbenderRemoveMode) { - window._webbenderToggleRemove(false); + function onDocumentMouseUp(e) { + if (!pointerDrag) return; + const drag = pointerDrag; + pointerDrag = null; + const finalTransform = drag.target.style.transform || ''; + const finalX = drag.target.dataset.webbenderPanX || '0'; + const finalY = drag.target.dataset.webbenderPanY || '0'; + if (finalTransform !== drag.startTransform) { + createOperation( + () => { + drag.target.style.transform = finalTransform; + drag.target.dataset.webbenderPanX = finalX; + drag.target.dataset.webbenderPanY = finalY; + }, + () => { + drag.target.style.transform = drag.startTransform; + drag.target.dataset.webbenderPanX = String(drag.baseX); + drag.target.dataset.webbenderPanY = String(drag.baseY); + } + ); } + e.preventDefault(); + e.stopPropagation(); + } - window._webbenderMoveMode = enabled; - moveToggle.checked = enabled; - settings.moveMode = enabled; - saveSettings(); - - if (enabled) { - document.addEventListener('mouseover', moveHoverHandler); - document.addEventListener('mouseout', moveLeaveHandler); - document.addEventListener('mousedown', moveDownHandler, true); - document.addEventListener('mousemove', moveHandler, true); - document.addEventListener('mouseup', moveUpHandler, true); - document.addEventListener('click', moveClickHandler, true); - } else { - document.removeEventListener('mouseover', moveHoverHandler); - document.removeEventListener('mouseout', moveLeaveHandler); - document.removeEventListener('mousedown', moveDownHandler, true); - document.removeEventListener('mousemove', moveHandler, true); - document.removeEventListener('mouseup', moveUpHandler, true); - document.removeEventListener('click', moveClickHandler, true); - setMoveHoverTarget(null); - activeMoveTarget = null; - } - }; + function closeImmersivePanel() { + if (!immersivePanel) return; + document.removeEventListener('click', onDocumentClick, true); + document.removeEventListener('mousedown', onDocumentMouseDown, true); + document.removeEventListener('mousemove', onDocumentMouseMove, true); + document.removeEventListener('mouseup', onDocumentMouseUp, true); + setSelectedElement(null); + immersivePanel.remove(); + immersivePanel = null; + pointerDrag = null; + startBtn.textContent = 'Start Immersive Edit'; + } - window._webbenderToggleRemove = function (enabled) { - if (enabled && document.designMode === 'on') { - setEditMode(false); - } - if (enabled && window._webbenderMoveMode) { - window._webbenderToggleMove(false); + function discardUnsaved() { + for (let i = undoStack.length - 1; i >= 0; i--) { + undoStack[i].undoAction(); } + undoStack.length = 0; + redoStack.length = 0; + sessionStack.length = 0; + markUnsaved(false); + } - window._webbenderRemoveMode = enabled; - removeToggle.checked = enabled; - settings.removeMode = enabled; - saveSettings(); - - if (enabled) { - document.addEventListener('mouseover', hoverHandler); - document.addEventListener('mouseout', leaveHandler); - document.addEventListener('click', clickHandler, true); - } else { - document.removeEventListener('mouseover', hoverHandler); - document.removeEventListener('mouseout', leaveHandler); - document.removeEventListener('click', clickHandler, true); + const toolButtons = new Map(); + let saveBtn = null; + window._webbenderCloseImmersiveSheet = function () { + if (!immersivePanel) return true; + if (unsavedChanges) { + const confirmClose = confirm('Delete unsaved immersive edits?'); + if (!confirmClose) return false; + discardUnsaved(); } + closeImmersivePanel(); + return true; }; - moveToggle.onchange = (e) => window._webbenderToggleMove(e.target.checked); - moveLabel.appendChild(moveToggle); - moveSection.appendChild(moveLabel); + function makeIconButton(icon, label, onClick) { + const btn = ui.create('button', { + textContent: icon, + attrs: { title: label, 'aria-label': label }, + style: { + background: '#27272a', + color: '#f4f4f5', + border: '1px solid #3f3f46', + borderRadius: '6px', + width: '34px', + height: '34px', + fontSize: '16px', + cursor: 'pointer', + }, + }); + btn.onclick = onClick; + return btn; + } - removeToggle.onchange = (e) => window._webbenderToggleRemove(e.target.checked); - removeLabel.appendChild(removeToggle); - removeSection.appendChild(removeLabel); + function makeDraggablePanel(panel, handle) { + let dragging = false; + let offsetX = 0; + let offsetY = 0; + handle.onmousedown = (e) => { + dragging = true; + panel.style.transform = 'none'; + offsetX = e.clientX - panel.getBoundingClientRect().left; + offsetY = e.clientY - panel.getBoundingClientRect().top; + e.preventDefault(); + }; + document.addEventListener('mousemove', (e) => { + if (!dragging || panel !== immersivePanel) return; + panel.style.left = `${Math.max(8, Math.min(window.innerWidth - panel.offsetWidth - 8, e.clientX - offsetX))}px`; + panel.style.top = `${Math.max(8, Math.min(window.innerHeight - panel.offsetHeight - 8, e.clientY - offsetY))}px`; + panel.style.bottom = 'auto'; + }); + document.addEventListener('mouseup', () => { + dragging = false; + }); + } + + function openImmersivePanel() { + if (immersivePanel) return; + immersivePanel = ui.create('div', { + attrs: { id: 'webbender-immersive-sheet' }, + style: { + position: 'fixed', + left: '50%', + bottom: '16px', + transform: 'translateX(-50%)', + width: 'min(720px, calc(100vw - 24px))', + background: '#0f172a', + border: '1px solid #334155', + borderRadius: '12px', + padding: '10px', + zIndex: '2147483646', + color: '#f8fafc', + boxShadow: '0 10px 35px rgba(0,0,0,0.5)', + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + }); + const handle = ui.create('div', { + textContent: 'Immersive Actions', + style: { + cursor: 'move', + textAlign: 'center', + fontSize: '11px', + color: '#94a3b8', + borderBottom: '1px solid #1e293b', + paddingBottom: '6px', + }, + }); + const toolbar = ui.create('div', { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(12, 34px)', + gap: '6px', + justifyContent: 'center', + }, + }); + optionsBody = ui.create('div', { + style: { + border: '1px solid #334155', + borderRadius: '8px', + padding: '8px', + minHeight: '44px', + fontSize: '11px', + color: '#cbd5e1', + }, + }); + + const selectBtn = makeIconButton('🖱️', 'Select', () => setActiveTool('select')); + const panBtn = makeIconButton('✋', 'Pan', () => setActiveTool('pan')); + const textBtn = makeIconButton('T', 'Text', () => { + const textNode = ui.create('div', { + textContent: 'Editable text', + attrs: { contenteditable: 'true' }, + style: { padding: '4px', border: '1px dashed #60a5fa', minHeight: '24px' }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, textNode), + () => textNode.remove() + ); + setSelectedElement(textNode); + setActiveTool('select'); + }); + const shapeBtn = makeIconButton('⬜', 'Shapes', () => { + const shape = ui.create('div', { + style: { + width: '120px', + height: '80px', + background: '#38bdf8', + borderRadius: '8px', + border: '1px solid #0ea5e9', + }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, shape), + () => shape.remove() + ); + setSelectedElement(shape); + }); + const imageBtn = makeIconButton('🖼️', 'Image', () => { + const src = prompt('Image URL:', 'https://'); + if (!src) return; + const image = ui.create('img', { + attrs: { src, alt: 'Immersive inserted image' }, + style: { maxWidth: '320px', borderRadius: '8px', border: '1px solid #334155' }, + }); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, image), + () => image.remove() + ); + setSelectedElement(image); + }); + const duplicateBtn = makeIconButton('⧉', 'Duplicate', () => { + if (!selectedElement) return; + copiedElement = selectedElement.cloneNode(true); + const clone = copiedElement.cloneNode(true); + const target = selectedElement; + createOperation( + () => insertNodeAfter(target, clone), + () => clone.remove() + ); + setSelectedElement(clone); + }); + const deleteBtn = makeIconButton('🗑️', 'Delete', () => { + if (!selectedElement || !selectedElement.parentNode) return; + const node = selectedElement; + const parent = node.parentNode; + const next = node.nextSibling; + createOperation( + () => node.remove(), + () => parent.insertBefore(node, next) + ); + setSelectedElement(null); + }); + const colorBtn = makeIconButton('🎨', 'Color picker', () => { + setActiveTool('color'); + refreshOptions(); + }); + const optionsBtn = makeIconButton('⚙️', 'Options', () => refreshOptions()); + const undoBtn = makeIconButton('↶', 'Undo', () => undo()); + const redoBtn = makeIconButton('↷', 'Redo', () => redo()); + saveBtn = makeIconButton('✅', 'Save', () => { + undoStack.length = 0; + redoStack.length = 0; + sessionStack.length = 0; + markUnsaved(false); + }); + const closeBtn = makeIconButton('❌', 'Close', () => window._webbenderCloseImmersiveSheet()); + closeBtn.style.background = '#7f1d1d'; + closeBtn.style.color = '#fecaca'; + + toolButtons.set('select', selectBtn); + toolButtons.set('pan', panBtn); + toolButtons.set('color', colorBtn); + + ui.append(toolbar, [ + undoBtn, + redoBtn, + selectBtn, + panBtn, + textBtn, + shapeBtn, + imageBtn, + duplicateBtn, + deleteBtn, + colorBtn, + optionsBtn, + saveBtn, + closeBtn, + ]); + ui.append(immersivePanel, [handle, toolbar, optionsBody]); + document.body.appendChild(immersivePanel); + + document.addEventListener('click', onDocumentClick, true); + document.addEventListener('mousedown', onDocumentMouseDown, true); + document.addEventListener('mousemove', onDocumentMouseMove, true); + document.addEventListener('mouseup', onDocumentMouseUp, true); + setActiveTool('select'); + refreshOptions(); + markUnsaved(false); + makeDraggablePanel(immersivePanel, handle); + startBtn.textContent = 'Immersive Edit Running'; + } - return { editSection, moveSection, removeSection, setEditMode }; + return { editSection, moveSection, removeSection, immersiveSection, setEditMode }; } function wbGetThemeCss(bgColor, fgColor) { @@ -697,6 +999,12 @@ function wbCreateDialogsActions(ui, state, controls) { mouseover: () => (resetBtn.style.background = '#991b1b'), mouseout: () => (resetBtn.style.background = '#dc2626'), click: () => { + if ( + typeof window._webbenderCloseImmersiveSheet === 'function' && + !window._webbenderCloseImmersiveSheet() + ) { + return; + } setEditMode(false); window._webbenderToggleRemove(false); window._webbenderToggleMove(false); @@ -757,6 +1065,7 @@ function wbRestoreAndAssemble(state, controls) { editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, @@ -788,6 +1097,7 @@ function wbRestoreAndAssemble(state, controls) { editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, @@ -858,11 +1168,8 @@ function wbRestoreAndAssemble(state, controls) { const { header } = wbCreateHeader(ui, container); const { updateBanner, updateText } = wbCreateUpdateBanner(ui); - const { editSection, moveSection, removeSection, setEditMode } = wbCreateEditRemoveSection( - ui, - container, - state - ); + const { editSection, moveSection, removeSection, immersiveSection, setEditMode } = + wbCreateEditRemoveSection(ui, container, state); const { fontSection, themeSection, fontSelect, customFontInput, applyFont, themes } = wbCreateFontThemeSection(ui, state); const { dialogSection, actionRow } = wbCreateDialogsActions(ui, state, { @@ -881,6 +1188,7 @@ function wbRestoreAndAssemble(state, controls) { editSection, moveSection, removeSection, + immersiveSection, fontSection, themeSection, dialogSection, diff --git a/tests/e2e/bookmarklet.spec.ts b/tests/e2e/bookmarklet.spec.ts index 558c44c..2a7a61e 100644 --- a/tests/e2e/bookmarklet.spec.ts +++ b/tests/e2e/bookmarklet.spec.ts @@ -7,6 +7,44 @@ async function getBookmarkletCode(page: Page) { } test.describe('Webbender E2E Tests', () => { + test('should open immersive floating action sheet from start button', async ({ page }) => { + await page.goto('/index.html'); + const bookmarkletCode = await page.locator('#drag-btn').getAttribute('href'); + expect(bookmarkletCode).toContain('javascript:'); + + await page.evaluate((code) => { + const jsCode = code.substring('javascript:'.length); + new Function(jsCode)(); + }, bookmarkletCode); + + const panel = page.locator('#webbender-ui'); + await expect(panel).toContainText('Immersive Edit'); + await panel.getByRole('button', { name: 'Start Immersive Edit' }).click(); + + const sheet = page.locator('#webbender-immersive-sheet'); + await expect(sheet).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Undo' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Redo' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Select' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Pan' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Text' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Shapes' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Image' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Duplicate' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Delete' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Color picker' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Options' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Save' })).toBeVisible(); + await expect(sheet.getByRole('button', { name: 'Close' })).toBeVisible(); + + await sheet.getByRole('button', { name: 'Text' }).click(); + await expect(page.locator('text=Editable text')).toBeVisible(); + + await sheet.getByRole('button', { name: 'Save' }).click(); + await sheet.getByRole('button', { name: 'Close' }).click(); + await expect(sheet).not.toBeVisible(); + }); + test('should load install page without errors', async ({ page }) => { await page.goto('/index.html'); expect(page).toHaveTitle('Webbender');