Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>HTML Pro Editor</title> | |
| <!-- Host-page CSS (the iframe can’t touch these rules) --> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{ | |
| font-family:system-ui,sans-serif; | |
| background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); | |
| min-height:100vh;display:flex;flex-direction:column; | |
| } | |
| /* MODIFIED TOOLBAR STYLES */ | |
| .toolbar{ | |
| padding:12px 16px; | |
| background:#fff; | |
| box-shadow:0 2px 8px rgba(0,0,0,.15); | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:flex-start; | |
| gap:15px; | |
| flex-wrap: wrap; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| width: 100%; | |
| z-index: 100; | |
| transition: top 0.3s ease-out; | |
| } | |
| .toolbar.hidden { top: -150px; } | |
| .toolbar.manual-hidden { top: -150px; } | |
| .toolbar-branding { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 1.2em; | |
| font-weight: bold; | |
| color: #333; | |
| padding-bottom: 5px; | |
| } | |
| .toolbar-icon { font-size: 1.5em; line-height: 1; } | |
| .toolbar button, | |
| .toolbar select, | |
| .toolbar input[type="color"]{ | |
| font:14px/1.2 system-ui,sans-serif; | |
| padding:6px 10px; | |
| border:1px solid #ccc; | |
| border-radius:4px; | |
| background:#fdfdfd; | |
| cursor:pointer; | |
| height: 34px; | |
| } | |
| .toolbar input[type="color"] { | |
| width: 40px; | |
| padding: 0; | |
| } | |
| .toolbar-left-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| flex-grow: 1; | |
| } | |
| .toolbar-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| align-items: center; | |
| } | |
| #imageBtn {background:#fffbea} | |
| #clipBtn {background:#eaf0ff} | |
| #resetBtn {background:#ffecec} | |
| #helpBtn {background:#ecf8ff} | |
| #selectBtn {background:#e8f4fd} | |
| #deleteSelectedBtn {background:#ffecec} | |
| .element-selector-overlay { | |
| position: fixed; | |
| background: rgba(0, 100, 200, 0.2); | |
| border: 2px solid #0066cc; | |
| pointer-events: none; | |
| z-index: 999999; | |
| box-sizing: border-box; | |
| } | |
| .selected-element { | |
| outline: 4px solid #ff6b6b ; | |
| outline-offset: 2px ; | |
| background: rgba(255, 107, 107, 0.1) ; | |
| box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.3) ; | |
| } | |
| .toolbar-right-side { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| align-items: flex-end; | |
| flex-shrink: 0; | |
| } | |
| .views{ | |
| display:flex; | |
| gap:4px; | |
| } | |
| .views button{padding:4px 8px;font-size:12px} | |
| .floating-toolbar-toggle { | |
| position: fixed; | |
| top: 120px; | |
| right: 0; | |
| z-index: 101; | |
| padding: 8px 6px; | |
| border: none; | |
| border-radius: 6px 0 0 6px; | |
| background: rgba(255, 255, 255, 0.95); | |
| cursor: pointer; | |
| font-size: 12px; | |
| writing-mode: vertical-rl; | |
| text-orientation: mixed; | |
| box-shadow: -2px 0 8px rgba(0,0,0,.15); | |
| transition: all 0.3s ease; | |
| border: 1px solid rgba(0,0,0,0.1); | |
| border-right: none; | |
| transform: translateX(0); | |
| } | |
| .floating-toolbar-toggle:hover { | |
| background: #f5f5f5; | |
| transform: translateX(-5px); | |
| box-shadow: -4px 0 12px rgba(0,0,0,.2); | |
| } | |
| .floating-toolbar-toggle:active { | |
| transform: translateX(-2px); | |
| } | |
| .main{ | |
| flex:1;display:flex;flex-direction:column;margin:12px; | |
| background:rgba(255,255,255,.94);border-radius:12px;overflow:hidden; | |
| box-shadow:0 10px 40px rgba(0,0,0,.1); | |
| margin-top: 0; | |
| transition: margin-top 0.3s ease-out; | |
| } | |
| #editorFrame{flex:1;width:100%;border:none} | |
| #codeView{ | |
| flex:1;width:100%;border:none;padding:16px;resize:none;outline:none; | |
| font:13px/1.4 Monaco,Menlo,monospace;display:none | |
| } | |
| #ctxMenu{ | |
| position:absolute;display:none;flex-direction:column; | |
| background:#fff;border:1px solid #ccc;border-radius:4px; | |
| box-shadow:0 4px 16px rgba(0,0,0,.15);z-index:3;min-width:140px; | |
| } | |
| #ctxMenu button{all:unset;padding:8px 14px;font:13px system-ui;cursor:pointer;white-space:nowrap;} | |
| #ctxMenu button:hover{background:#f0f2ff} | |
| #helpModal{ | |
| position:fixed;inset:0;background:rgba(0,0,0,.55); | |
| display:none;align-items:center;justify-content:center;z-index:4; | |
| } | |
| #helpModal .panel{ | |
| background:#fff;border-radius:8px;max-width:600px;width:90%;padding:24px; | |
| box-shadow:0 6px 24px rgba(0,0,0,.25);overflow-y:auto;max-height:80vh; | |
| } | |
| #helpModal h2{margin-bottom:12px;font-size:20px} | |
| #helpModal ul{margin-left:18px;margin-top:6px} | |
| #helpModal button{margin-top:16px;padding:6px 14px;font:14px system-ui;border:1px solid #ccc;border-radius:4px;cursor:pointer} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ────────── TOOLBAR ────────── --> | |
| <div class="toolbar"> | |
| <div class="toolbar-left-section"> | |
| <div class="toolbar-row"> | |
| <select onchange="cmd('formatBlock',this.value)"> | |
| <option value="">Format</option><option value="<h1>">H1</option> | |
| <option value="<h2>">H2</option><option value="<h3>">H3</option> | |
| <option value="<p>">Paragraph</option><option value="blockquote">Quote</option> | |
| <option value="<pre>">Code</option> | |
| </select> | |
| <button onclick="cmd('bold')"><b>B</b></button> | |
| <button onclick="cmd('italic')"><i>I</i></button> | |
| <button onclick="cmd('underline')"><u>U</u></button> | |
| <button onclick="cmd('strikeThrough')"><s>S</s></button> | |
| <button onclick="cmd('insertUnorderedList')">• List</button> | |
| <button onclick="cmd('insertOrderedList')">1. List</button> | |
| <!-- Color Controls Group --> | |
| <span style="margin-left: 8px; margin-right: 8px; border-left: 1px solid #ddd; padding-left: 8px;"> | |
| <input type="color" title="Document text color" oninput="setDocumentTextColor(this.value)"> | |
| </span> | |
| <!-- File Controls --> | |
| <input type="file" id="htmlFileInput" accept=".html,.htm" style="display:none" onchange="loadHTMLFile(this)"> | |
| <button onclick="document.getElementById('htmlFileInput').click()">📂 Upload HTML</button> | |
| </div> | |
| <div class="toolbar-row"> | |
| <button onclick="insertLink()">🔗 Link</button> | |
| <button onclick="insertImage()">🖼️ Image URL</button> | |
| <button id="imageBtn" onclick="replaceSelectionWithImage()">Add Image</button> | |
| <button onclick="insertTable()">📊 Table</button> | |
| <button onclick="cmd('insertHorizontalRule')">─ Rule</button> | |
| <button id="selectBtn" onclick="activateElementSelector()">🎯 Select Element</button> | |
| <button id="deleteSelectedBtn" onclick="deleteSelectedElement()" style="display:none">🗑️ Delete Selected</button> | |
| <button onclick="undo()">↶ Undo</button> | |
| <button onclick="redo()">↷ Redo</button> | |
| <button onclick="downloadHTML()">💾 Save</button> | |
| <button onclick="exportPDF()">📄 PDF</button> | |
| <button id="clipBtn" onclick="importClipboard()">Import Clipboard</button> | |
| <button id="helpBtn" onclick="toggleHelp()">Help</button> | |
| </div> | |
| </div> | |
| <!-- NEW: Container for branding and views on the right --> | |
| <div class="toolbar-right-side"> | |
| <!-- BRANDING SECTION --> | |
| <div class="toolbar-branding"> | |
| <span class="toolbar-icon">✍️</span> | |
| <span class="toolbar-name">Tiny HTML Editor</span> | |
| </div> | |
| <!----------------------> | |
| <div class="views"> | |
| <button id="vDesign" onclick="view('design')" style="background:#667eea;color:#fff;">Design</button> | |
| <button id="vCode" onclick="view('code')">Code</button> | |
| <button id="vSplit" onclick="view('split')">Split</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Slide-out Tab Toggle Button --> | |
| <button id="toggleToolbarBtn" onclick="toggleToolbar()" class="floating-toolbar-toggle">▲ Hide</button> | |
| <!-- Main editor area --> | |
| <div class="main"> | |
| <iframe id="editorFrame" sandbox="allow-scripts allow-same-origin"></iframe> | |
| <textarea id="codeView"></textarea> | |
| </div> | |
| <!-- Repeatable context menu --> | |
| <div id="ctxMenu"> | |
| <button onclick="dupRepeat()">Duplicate</button> | |
| <button onclick="delRepeat()">Delete</button> | |
| </div> | |
| <!-- Help modal --> | |
| <div id="helpModal"> | |
| <div class="panel"> | |
| <h2>Editor Help</h2> | |
| <ul> | |
| <li><strong>Add Image</strong> – click “Add Image” to upload; no text selection needed.</li> | |
| <li><strong>Select Element</strong> – click "Select Element", hover and click to select, then delete.</li> | |
| <li><strong>Resize & Move</strong> – drag image corners to resize or drag to move.</li> | |
| <li><strong>Import Clipboard</strong> – pastes raw HTML from your clipboard into the editor.</li> | |
| <li><strong>Undo / Redo</strong> – toolbar buttons or <kbd>Ctrl/Cmd + Z/Y</kbd>.</li> | |
| <li><strong>Views</strong> – switch between Design, Code, or Split view.</li> | |
| </ul> | |
| <button onclick="toggleHelp()">Close</button> | |
| </div> | |
| </div> | |
| <!-- ────────── SCRIPT ────────── --> | |
| <script> | |
| const frame = document.getElementById('editorFrame'); | |
| const codeBox = document.getElementById('codeView'); | |
| const ctxMenu = document.getElementById('ctxMenu'); | |
| const helpModal=document.getElementById('helpModal'); | |
| const toolbar = document.querySelector('.toolbar'); | |
| const mainContent = document.querySelector('.main'); | |
| const toggleToolbarBtn = document.getElementById('toggleToolbarBtn'); | |
| let currentRepeatable=null; | |
| /* UNDO / REDO STACK */ | |
| const undoStack=[], redoStack=[], MAX_HIST=100; | |
| function pushState(){ | |
| const html=frame.contentDocument.body.innerHTML; | |
| if(!undoStack.length||undoStack[undoStack.length-1]!==html){ | |
| undoStack.push(html); | |
| if(undoStack.length>MAX_HIST)undoStack.shift(); | |
| redoStack.length=0; | |
| } | |
| } | |
| function restore(html){loadHTMLWithScripts(html);scanPlaceholders();sync(false);} | |
| function undo(){if(undoStack.length>1){redoStack.push(undoStack.pop());restore(undoStack[undoStack.length-1]);}} | |
| function redo(){if(redoStack.length){const h=redoStack.pop();undoStack.push(h);restore(h);}} | |
| /* INITIAL DOC */ | |
| const initialHTML=`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><style> | |
| body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; | |
| padding:20px;line-height:1.6;min-height:100vh} | |
| table{border-collapse:collapse;width:100%;margin:15px 0} | |
| td,th{border:1px solid #ddd;padding:8px;text-align:left} | |
| th{background:#f8f9fa;font-weight:600} | |
| blockquote{border-left:4px solid #667eea;padding:10px 20px;background:#f8f9fa} | |
| img{max-width:100%;height:auto;margin:10px 0} | |
| .placeholder-image{border:2px dashed #667eea;opacity:.8;cursor:pointer;position:relative} | |
| .placeholder-image::after{content:'📷 Click to upload';position:absolute;top:50%;left:50%; | |
| transform:translate(-50%,-50%);background:rgba(102,126,234,.9);color:#fff; | |
| padding:6px 10px;border-radius:4px;font-size:12px;font-weight:600;pointer-events:none} | |
| [repeatable]{outline:1px dashed #999} | |
| </style></head><body contenteditable="true"> | |
| <h1>John Doe</h1> | |
| <p><b>Web Developer</b> | Passionate about building interactive experiences</p> | |
| <h2>Contact</h2> | |
| <ul repeatable> | |
| <li>Email: <a href="mailto:[email protected]">[email protected]</a></li> | |
| <li>Phone: +1 (123) 456-7890</li> | |
| <li>LinkedIn: <a href="#">linkedin.com/in/johndoe</a></li> | |
| <li>Portfolio: <a href="#">johndoe.dev</a></li> | |
| </ul> | |
| <h2>Summary</h2> | |
| <p>Enthusiastic and results-driven Web Developer with 3+ years of experience in creating responsive and user-friendly websites. Proficient in HTML, CSS, JavaScript, and modern front-end frameworks.</p> | |
| <h2>Experience</h2> | |
| <div repeatable style="padding:10px; border:1px solid #e0e0e0; margin:10px 0; border-radius:4px; background-color:#f9f9f9;"> | |
| <h3>Junior Web Developer</h3> | |
| <p><i>Tech Solutions Inc.</i> | Jan 2021 - Present</p> | |
| <ul> | |
| <li>Developed and maintained client websites using HTML, CSS, and JavaScript.</li> | |
| <li>Collaborated with design teams to translate mockups into functional web pages.</li> | |
| <li>Optimized web applications for maximum speed and scalability.</li> | |
| </ul> | |
| </div> | |
| <div repeatable style="padding:10px; border:1px solid #e0e0e0; margin:10px 0; border-radius:4px; background-color:#f9f9f9;"> | |
| <h3>Intern Developer</h3> | |
| <p><i>Startup Innovations</i> | Summer 2020</p> | |
| <ul> | |
| <li>Assisted senior developers in building new features for a SaaS platform.</li> | |
| <li>Contributed to front-end development and bug fixing.</li> | |
| </ul> | |
| </div> | |
| <h2>Education</h2> | |
| <p><b>Bachelor of Science in Computer Science</b> | University of Tech | 2017 - 2020</p> | |
| <h2>Skills</h2> | |
| <ul repeatable> | |
| <li><b>Languages:</b> HTML, CSS, JavaScript (ES6+), Python</li> | |
| <li><b>Frameworks:</b> React, Vue.js, Node.js</li> | |
| <li><b>Tools:</b> Git, Webpack, Babel, npm</li> | |
| </ul> | |
| <img src="https://via.placeholder.com/200x150/667eea/ffffff?text=Your+Photo" alt="Placeholder for user photo" class="placeholder-image"> | |
| </body></html>`; | |
| frame.srcdoc=initialHTML; | |
| frame.addEventListener('load',setupFrame); | |
| /* execCommand wrapper */ | |
| function cmd(c,v=null){ | |
| const d=frame.contentDocument;if(!d)return; | |
| d.execCommand(c,false,v);frame.contentWindow.focus(); | |
| scanPlaceholders();sync();pushState(); | |
| } | |
| /* INSERT FUNCTIONS */ | |
| function insertLink(){const u=prompt('Enter URL:');if(u)cmd('createLink',u);} | |
| function insertImage(){ | |
| const u=prompt('Image URL (leave blank to upload):');if(u===null)return; | |
| u===''?upload(data=>cmd('insertImage',data)):cmd('insertImage',u); | |
| } | |
| function upload(cb){ | |
| const inp=document.createElement('input');inp.type='file';inp.accept='image/*'; | |
| inp.onchange=e=>{ | |
| const f=e.target.files[0];if(f){const r=new FileReader();r.onload=ev=>cb(ev.target.result);r.readAsDataURL(f);} | |
| };inp.click(); | |
| } | |
| function insertTable(){ | |
| const r=+prompt('Rows:',3),c=+prompt('Cols:',3); | |
| if(r&&c){ | |
| let h='<table><tbody>'; | |
| for(let i=0;i<r;i++){h+='<tr>';for(let j=0;j<c;j++)h+=i?'<td>Cell</td>':'<th>Header</th>';h+='</tr>'; } | |
| h+='</tbody></table>';cmd('insertHTML',h); | |
| } | |
| } | |
| /* PLACEHOLDER LOGIC */ | |
| function isPH(src){return /placeholder\.com|via\.placeholder\.com|placehold\.it|picsum\.photos|dummyimage\.com|fakeimg\.pl|placeholder\.pics/i.test(src);} | |
| function scanPlaceholders(){ | |
| frame.contentDocument.querySelectorAll('img').forEach(img=>{ | |
| if(isPH(img.src)||img.classList.contains('placeholder-image')){ | |
| img.classList.add('placeholder-image'); | |
| img.onclick=()=>replacePlaceholder(img); | |
| } | |
| }); | |
| } | |
| function replacePlaceholder(img){ | |
| upload(data=>{ | |
| img.src=data;img.classList.remove('placeholder-image');img.onclick=null; | |
| sync();pushState(); | |
| }); | |
| } | |
| /* ADD IMAGE (replace selection or append if none) */ | |
| function replaceSelectionWithImage(){ | |
| const d = frame.contentDocument, sel = d.getSelection(); | |
| upload(data => { | |
| // 1) Create a wrapper that’s resizable | |
| const wrapper = d.createElement('div'); | |
| wrapper.style.resize = 'both'; | |
| wrapper.style.overflow = 'auto'; | |
| wrapper.style.display = 'inline-block'; | |
| wrapper.style.border = '1px dashed #666'; // optional visual cue | |
| // 2) Create the image inside it | |
| const img = d.createElement('img'); | |
| img.src = data; | |
| img.style.display = 'block'; | |
| img.style.width = '100%'; // make it fill the wrapper | |
| img.style.height = 'auto'; | |
| wrapper.appendChild(img); | |
| // 3) Insert wrapper (or append if no selection) | |
| if (sel.rangeCount) { | |
| const r = sel.getRangeAt(0); | |
| r.deleteContents(); | |
| r.insertNode(wrapper); | |
| } else { | |
| d.body.appendChild(wrapper); | |
| } | |
| sel.removeAllRanges(); | |
| scanPlaceholders(); | |
| sync(); | |
| pushState(); | |
| }); | |
| } | |
| /* CLIPBOARD IMPORT */ | |
| let lastImported=''; | |
| async function importClipboard(silent = false){ | |
| try{ | |
| let rawHtml = await navigator.clipboard.readText(); | |
| let cleanedHtml = rawHtml; | |
| if (cleanedHtml) { | |
| if (cleanedHtml.startsWith("```html")) { | |
| cleanedHtml = cleanedHtml.substring(7); | |
| } | |
| if (cleanedHtml.endsWith("```")) { | |
| cleanedHtml = cleanedHtml.substring(0, cleanedHtml.length - 3); | |
| } | |
| cleanedHtml = cleanedHtml.trim(); | |
| } | |
| const html = cleanedHtml; | |
| if(html && html !== lastImported && /<[a-z]/i.test(html)){ | |
| loadHTMLWithScripts(html); | |
| lastImported=html; | |
| scanPlaceholders(); | |
| sync(); | |
| pushState(); | |
| if (!silent) { alert('Clipboard content imported successfully!'); } | |
| } else if (!silent) { | |
| if (rawHtml && (!html || !/<[a-z]/i.test(html))) { | |
| alert('Clipboard content was not valid HTML after cleaning, or became empty.'); | |
| } else if (!rawHtml) { | |
| alert('Clipboard is empty.'); | |
| } else { | |
| alert('Clipboard does not contain valid HTML or is unchanged from last import.'); | |
| } | |
| } | |
| }catch(err){ | |
| if (!silent) { | |
| alert('Clipboard access denied or content unavailable. Please ensure you have granted permission.'); | |
| console.error("Clipboard read error:", err); | |
| } | |
| } | |
| } | |
| function handlePaste(e){ | |
| const html=e.clipboardData.getData('text/html'); | |
| if(html){e.preventDefault();cmd('insertHTML',html);} | |
| } | |
| /* CODE SYNC */ | |
| function tidy(h){return h.replace(/></g,'>\n<').trim();} | |
| function sync(updateCode=true){ | |
| const html=frame.contentDocument.body.innerHTML; | |
| if(updateCode)codeBox.value=tidy(html); | |
| } | |
| codeBox.addEventListener('input',()=>{ | |
| loadHTMLWithScripts(codeBox.value); | |
| scanPlaceholders();pushState(); | |
| }); | |
| /* VIEW SWITCH */ | |
| const vBtns={design:vDesign,code:vCode,split:vSplit}; | |
| function view(v){ | |
| Object.values(vBtns).forEach(b=>{b.style.background='';b.style.color='';}); | |
| vBtns[v].style.background='#667eea';vBtns[v].style.color='#fff'; | |
| if(v==='design'){frame.style.display='block';codeBox.style.display='none'} | |
| else if(v==='code'){sync();frame.style.display='none';codeBox.style.display='block'} | |
| else {sync();frame.style.display='block';codeBox.style.display='block';frame.style.height='50%';codeBox.style.height='50%'} | |
| } | |
| /* RESET */ | |
| function resetContent(){ | |
| if(confirm('Clear the editor content?')){ | |
| frame.contentDocument.body.innerHTML='<p></p>'; | |
| scanPlaceholders();sync();pushState(); | |
| } | |
| } | |
| /* SAVE & PDF */ | |
| function downloadHTML(){ | |
| const blob=new Blob([`<!DOCTYPE html><html><body>${frame.contentDocument.body.innerHTML}</body></html>`],{type:'text/html'}); | |
| const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='document.html';a.click();URL.revokeObjectURL(a.href); | |
| } | |
| function exportPDF(){ | |
| const w=window.open('','_blank','width=800,height=1000'); | |
| w.document.write(`<!DOCTYPE html><html><body>${frame.contentDocument.body.innerHTML}</body></html>`);w.document.close(); | |
| w.onload=()=>setTimeout(()=>w.print(),300); | |
| } | |
| /* REPEATABLE CONTEXT MENU */ | |
| function handleContext(e){ | |
| let n=e.target, d=frame.contentDocument; | |
| while(n&&n!==d.body&&!n.hasAttribute('repeatable'))n=n.parentElement; | |
| if(n&&n.hasAttribute('repeatable')){ | |
| e.preventDefault();currentRepeatable=n; | |
| const r=frame.getBoundingClientRect(); | |
| ctxMenu.style.left=r.left+e.clientX+'px'; | |
| ctxMenu.style.top =r.top +e.clientY+'px'; | |
| ctxMenu.style.display='flex'; | |
| }else hideCtx(); | |
| } | |
| function dupRepeat(){if(currentRepeatable){currentRepeatable.parentNode.insertBefore(currentRepeatable.cloneNode(true),currentRepeatable.nextSibling);hideCtx();sync();pushState();}} | |
| function delRepeat(){if(currentRepeatable){currentRepeatable.remove();hideCtx();sync();pushState();}} | |
| function hideCtx(){ctxMenu.style.display='none';currentRepeatable=null;} | |
| /* ELEMENT SELECTOR */ | |
| let elementSelectorMode = false; | |
| let selectorOverlay = null; | |
| let selectedElement = null; | |
| function activateElementSelector() { | |
| if (selectedElement && !elementSelectorMode) { | |
| selectedElement.classList.remove('selected-element'); | |
| selectedElement = null; | |
| const deleteBtn = document.getElementById('deleteSelectedBtn'); | |
| deleteBtn.style.display = 'none'; | |
| if (selectorOverlay) { | |
| selectorOverlay.remove(); | |
| selectorOverlay = null; | |
| } | |
| const selectBtn = document.getElementById('selectBtn'); | |
| selectBtn.textContent = '🎯 Select Element'; | |
| selectBtn.style.background = '#e8f4fd'; | |
| return; | |
| } | |
| if (elementSelectorMode) { | |
| deactivateElementSelector(); | |
| return; | |
| } | |
| elementSelectorMode = true; | |
| const selectBtn = document.getElementById('selectBtn'); | |
| selectBtn.textContent = '❌ Cancel Select'; | |
| selectBtn.style.background = '#ff9999'; | |
| selectorOverlay = document.createElement('div'); | |
| selectorOverlay.className = 'element-selector-overlay'; | |
| document.body.appendChild(selectorOverlay); | |
| frame.contentDocument.body.style.cursor = 'crosshair'; | |
| frame.contentDocument.addEventListener('mousemove', handleSelectorMouseMove); | |
| frame.contentDocument.addEventListener('click', handleSelectorClick); | |
| frame.contentDocument.addEventListener('keydown', handleSelectorKeyDown); | |
| } | |
| function deactivateElementSelector() { | |
| elementSelectorMode = false; | |
| const selectBtn = document.getElementById('selectBtn'); | |
| selectBtn.textContent = '🎯 Select Element'; | |
| selectBtn.style.background = '#e8f4fd'; | |
| if (selectorOverlay) { | |
| selectorOverlay.remove(); | |
| selectorOverlay = null; | |
| } | |
| frame.contentDocument.body.style.cursor = 'auto'; | |
| frame.contentDocument.removeEventListener('mousemove', handleSelectorMouseMove); | |
| frame.contentDocument.removeEventListener('click', handleSelectorClick); | |
| frame.contentDocument.removeEventListener('keydown', handleSelectorKeyDown); | |
| } | |
| function handleSelectorMouseMove(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const element = frame.contentDocument.elementFromPoint(e.clientX, e.clientY); | |
| if (element && element !== frame.contentDocument.body) { | |
| updateSelectorOverlay(element); | |
| } | |
| } | |
| function updateSelectorOverlay(element) { | |
| if (!selectorOverlay) return; | |
| const rect = element.getBoundingClientRect(); | |
| const frameRect = frame.getBoundingClientRect(); | |
| selectorOverlay.style.top = (frameRect.top + rect.top) + 'px'; | |
| selectorOverlay.style.left = (frameRect.left + rect.left) + 'px'; | |
| selectorOverlay.style.width = rect.width + 'px'; | |
| selectorOverlay.style.height = rect.height + 'px'; | |
| selectorOverlay.style.display = 'block'; | |
| } | |
| function handleSelectorClick(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const element = frame.contentDocument.elementFromPoint(e.clientX, e.clientY); | |
| if (element && element !== frame.contentDocument.body) { | |
| if (element.tagName.toLowerCase() === 'html' || element.tagName.toLowerCase() === 'body') { | |
| alert('Cannot select the body or html element.'); | |
| return; | |
| } | |
| if (selectedElement) { | |
| selectedElement.classList.remove('selected-element'); | |
| } | |
| selectedElement = element; | |
| selectedElement.classList.add('selected-element'); | |
| const deleteBtn = document.getElementById('deleteSelectedBtn'); | |
| deleteBtn.style.display = 'inline-block'; | |
| updateSelectorOverlay(selectedElement); | |
| elementSelectorMode = false; | |
| const selectBtn = document.getElementById('selectBtn'); | |
| selectBtn.textContent = '🎯 Unselect Element'; | |
| selectBtn.style.background = '#ffdddd'; | |
| frame.contentDocument.body.style.cursor = 'auto'; | |
| frame.contentDocument.removeEventListener('mousemove', handleSelectorMouseMove); | |
| frame.contentDocument.removeEventListener('click', handleSelectorClick); | |
| frame.contentDocument.removeEventListener('keydown', handleSelectorKeyDown); | |
| } | |
| } | |
| function handleSelectorKeyDown(e) { | |
| if (e.key === 'Escape') { | |
| deactivateElementSelector(); | |
| } | |
| } | |
| function deleteSelectedElement() { | |
| if (!selectedElement) { | |
| alert('No element selected. Please select an element first.'); | |
| return; | |
| } | |
| selectedElement.remove(); | |
| selectedElement = null; | |
| const deleteBtn = document.getElementById('deleteSelectedBtn'); | |
| deleteBtn.style.display = 'none'; | |
| if (selectorOverlay) { | |
| selectorOverlay.remove(); | |
| selectorOverlay = null; | |
| } | |
| const selectBtn = document.getElementById('selectBtn'); | |
| selectBtn.textContent = '🎯 Select Element'; | |
| selectBtn.style.background = '#e8f4fd'; | |
| sync(); | |
| pushState(); | |
| } | |
| /* HELP */ | |
| function toggleHelp(){helpModal.style.display=helpModal.style.display==='flex'?'none':'flex';} | |
| /* TOOLBAR AUTO-HIDE / TOGGLE */ | |
| let lastScrollY = 0; | |
| let toolbarInitialHeight = 0; | |
| window.addEventListener('scroll', () => { | |
| const currentScrollY = window.scrollY; | |
| if (toolbar.classList.contains('manual-hidden')) { | |
| mainContent.style.marginTop = '0px'; | |
| lastScrollY = currentScrollY; | |
| return; | |
| } | |
| if (currentScrollY > lastScrollY && currentScrollY > toolbarInitialHeight) { | |
| toolbar.classList.add('hidden'); | |
| mainContent.style.marginTop = '0px'; | |
| } | |
| else if (currentScrollY < lastScrollY || currentScrollY <= toolbarInitialHeight) { | |
| toolbar.classList.remove('hidden'); | |
| mainContent.style.marginTop = toolbarInitialHeight + 'px'; | |
| } | |
| lastScrollY = currentScrollY; | |
| }); | |
| function toggleToolbar() { | |
| const isManuallyHidden = toolbar.classList.contains('manual-hidden'); | |
| if (isManuallyHidden) { | |
| toolbar.classList.remove('manual-hidden', 'hidden'); | |
| mainContent.style.marginTop = toolbarInitialHeight + 'px'; | |
| toggleToolbarBtn.innerHTML = '▲ Hide'; | |
| } else { | |
| toolbar.classList.add('manual-hidden', 'hidden'); | |
| mainContent.style.marginTop = '0px'; | |
| toggleToolbarBtn.innerHTML = '▼ Show'; | |
| } | |
| } | |
| /* IFRAME SETUP */ | |
| function setupFrame(){ | |
| const d=frame.contentDocument; | |
| d.designMode = "on"; | |
| d.execCommand('enableObjectResizing', false, true); | |
| d.execCommand('enableAbsolutePositionEditor', false, true); | |
| d.body.addEventListener('input',()=>{scanPlaceholders();sync();pushState();}); | |
| d.body.addEventListener('paste',handlePaste); | |
| d.body.addEventListener('keydown',e=>{ | |
| if (selectedElement && (e.key === 'Delete' || e.key === 'Backspace')) { | |
| e.preventDefault(); | |
| deleteSelectedElement(); | |
| return; | |
| } | |
| if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='z'){e.preventDefault();e.shiftKey?redo():undo();} | |
| if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='y'){e.preventDefault();redo();} | |
| }); | |
| d.body.addEventListener('contextmenu',handleContext); | |
| window.addEventListener('click',hideCtx); | |
| requestAnimationFrame(() => { | |
| toolbarInitialHeight = toolbar.offsetHeight; | |
| mainContent.style.marginTop = toolbarInitialHeight + 'px'; | |
| }); | |
| importClipboard(true); | |
| scanPlaceholders();sync();pushState(); | |
| } | |
| /* FUNCTION TO LOAD HTML WITH SCRIPTS ENABLED */ | |
| function loadHTMLWithScripts(html) { | |
| const doc = frame.contentDocument; | |
| // Set the HTML content | |
| doc.body.innerHTML = html; | |
| // Find and execute all script tags | |
| const scripts = doc.body.querySelectorAll('script'); | |
| scripts.forEach(oldScript => { | |
| const newScript = doc.createElement('script'); | |
| // Copy attributes | |
| Array.from(oldScript.attributes).forEach(attr => { | |
| newScript.setAttribute(attr.name, attr.value); | |
| }); | |
| // Copy content | |
| newScript.textContent = oldScript.textContent; | |
| // Replace old script with new one to trigger execution | |
| oldScript.parentNode.replaceChild(newScript, oldScript); | |
| }); | |
| } | |
| /* NEW FUNCTIONS FOR HTML UPLOAD AND COLOR CONTROLS */ | |
| function loadHTMLFile(input) { | |
| const file = input.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| let rawHtml = e.target.result; | |
| let cleanedHtml = rawHtml; | |
| // Apply the same cleaning logic as clipboard import | |
| if (cleanedHtml) { | |
| if (cleanedHtml.startsWith("```html")) { | |
| cleanedHtml = cleanedHtml.substring(7); | |
| } | |
| if (cleanedHtml.endsWith("```")) { | |
| cleanedHtml = cleanedHtml.substring(0, cleanedHtml.length - 3); | |
| } | |
| cleanedHtml = cleanedHtml.trim(); | |
| } | |
| const html = cleanedHtml; | |
| if(html && /<[a-z]/i.test(html)){ | |
| loadHTMLWithScripts(html); | |
| scanPlaceholders(); | |
| sync(); | |
| pushState(); | |
| } | |
| // Clear the input so the same file can be selected again | |
| input.value = ''; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| let colorChangeTimeout; | |
| function setDocumentTextColor(color) { | |
| const doc = frame.contentDocument; | |
| const body = doc.body; | |
| // Push state before first change only | |
| if (!colorChangeTimeout) { | |
| pushState(); | |
| } | |
| body.style.color = color; | |
| sync(); | |
| // Debounce pushState for rapid color changes | |
| clearTimeout(colorChangeTimeout); | |
| colorChangeTimeout = setTimeout(() => { | |
| pushState(); | |
| colorChangeTimeout = null; | |
| }, 500); | |
| } | |
| function setElementBackgroundColor(color) { | |
| const doc = frame.contentDocument; | |
| const selection = doc.getSelection(); | |
| // Push state before first change only | |
| if (!colorChangeTimeout) { | |
| pushState(); | |
| } | |
| if (selectedElement) { | |
| // If an element is selected via element selector, apply to that | |
| selectedElement.style.backgroundColor = color; | |
| } else if (selection.rangeCount > 0) { | |
| // If there's a text selection, apply to the parent element | |
| const range = selection.getRangeAt(0); | |
| const container = range.commonAncestorContainer; | |
| const element = container.nodeType === Node.TEXT_NODE ? container.parentElement : container; | |
| if (element && element !== doc.body && element !== doc.documentElement) { | |
| element.style.backgroundColor = color; | |
| } | |
| } else { | |
| // No selection, apply to body as fallback | |
| doc.body.style.backgroundColor = color; | |
| } | |
| sync(); | |
| // Debounce pushState for rapid color changes | |
| clearTimeout(colorChangeTimeout); | |
| colorChangeTimeout = setTimeout(() => { | |
| pushState(); | |
| colorChangeTimeout = null; | |
| }, 500); | |
| } | |
| </script> | |
| </body> | |
| </html> | |