tinyhtmleditor / index.html
airabbitX's picture
Create index.html
0547596 verified
<!DOCTYPE html>
<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 !important;
outline-offset: 2px !important;
background: rgba(255, 107, 107, 0.1) !important;
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.3) !important;
}
.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>