saved synced
CSS
JS
Images
1 line
Image Library Click to copy tag  ·  Drag to editor
preview
Ctrl+Enter to run
Press Run to preview your site
Ready Normal Normal
Examples
My Projects
Loading…
\n"; } function extractLinkedPageNames(html) { var names = []; var re = /]*\bhref\s*=\s*["']([^"']+)["']/gi, m; while ((m = re.exec(html)) !== null) { var raw = m[1]; // Strip a leading "/" or "./" so absolute-style links match the same as relative ones var href = raw.replace(/^\.\//, "").replace(/^\//, ""); if (/^[a-zA-Z0-9_-]+\.html$/.test(href) && names.indexOf(href) === -1) { names.push(href); } } return names; } function createStubPage(name) { var newPage = { name: name, html: stubPageHtml(name) }; pages.push(newPage); createPageEditor(pages.length - 1); } async function deletePage(i) { if (pages[i].name === "index.html") { showToast("index.html cannot be deleted.", "info"); return; } if (!await showConfirm("Delete page \"" + pages[i].name + "\"? This cannot be undone.")) return; // Remove editor DOM var panelId = "cm-page-" + i + "-panel"; var panel = $(panelId); if (panel) panel.remove(); // Shift editors pageEditors.splice(i, 1); pages.splice(i, 1); // Rebuild all page editor panel IDs (panels after i need renumbering) // Simplest: recreate all from scratch next render rebuildPageEditorPanels(); if (activePageIndex >= pages.length) activePageIndex = pages.length - 1; activeShared = null; renderPageTabs(); showEditorPanel("cm-page-" + activePageIndex + "-panel"); if (pageEditors[activePageIndex]) pageEditors[activePageIndex].refresh(); markDirty(); autosave(); showToast("Deleted page", "ok"); } function rebuildPageEditorPanels() { // Remove all existing page panels document.querySelectorAll(".cm-panel[id^='cm-page-']").forEach(function(p) { p.remove(); }); var savedEditors = pageEditors.slice(); pageEditors = []; pages.forEach(function(page, i) { createPageEditor(i); // Restore value if (pageEditors[i]) pageEditors[i].setValue(page.html || ""); }); } function startPageRename(i, tab, nameSpan) { if (pages[i].name === "index.html") { showToast("index.html cannot be renamed.", "info"); return; } const input = document.createElement("input"); input.className = "page-tab-rename-input"; input.value = pages[i].name; function sizeInput() { input.style.width = Math.max(6, input.value.length + 2) + "ch"; } sizeInput(); input.addEventListener("input", sizeInput); input.onclick = function(e) { e.stopPropagation(); }; input.onmousedown = function(e) { e.stopPropagation(); }; // Suppress the tab's click-to-switch while editing, so a stray click // anywhere inside this tab (not just the input) can't prematurely end the rename. var originalTabClick = tab.onclick; tab.onclick = function(e) { e.stopPropagation(); }; nameSpan.replaceWith(input); input.focus(); input.select(); var finished = false; function restoreTabClick() { tab.onclick = originalTabClick; } function commit() { if (finished) return; finished = true; restoreTabClick(); var val = input.value.trim(); if (!val || val === pages[i].name) { input.replaceWith(nameSpan); return; } if (!/^[a-zA-Z0-9_-]+\.html$/.test(val)) { showToast("Filename must end in .html and contain only letters, numbers, - or _", "error", 4000); input.replaceWith(nameSpan); return; } if (pages.some(function(p,j) { return j !== i && p.name === val; })) { showToast("A page with that name already exists.", "error"); input.replaceWith(nameSpan); return; } pages[i].name = val; nameSpan.textContent = val; input.replaceWith(nameSpan); renderPageTabs(); markDirty(); autosave(); showToast("Renamed to " + val, "ok"); } function cancel() { if (finished) return; finished = true; restoreTabClick(); input.replaceWith(nameSpan); } input.addEventListener("keydown", function(e) { if (e.key === "Enter") { e.preventDefault(); commit(); } if (e.key === "Escape") { e.preventDefault(); cancel(); } }); input.addEventListener("blur", commit); } /* ════════════════════════════════════════════════════════════════ BUILD SRCDOC (shared CSS+JS injected into each page) ════════════════════════════════════════════════════════════════ */ function buildSrcdoc(pageIndex, allPages) { var html = allPages[pageIndex].html || ""; var css = cssEditor ? cssEditor.getValue().trim() : ""; var js = jsEditor ? jsEditor.getValue().trim() : ""; // Inject CSS before if (css) { var styleTag = ""; html = /<\/head>/i.test(html) ? html.replace(/<\/head>/i, styleTag + "\n") : html + "\n" + styleTag; } // Build page name→srcdoc map for link interception var pageMap = {}; allPages.forEach(function(p, i) { pageMap[p.name] = i; }); // Inject link-interception script + user JS before var linkScript = " ") : html + "\n" + linkScript; return html; } /* ════════════════════════════════════════════════════════════════ BUILD STANDALONE BUNDLE (for New Tab / Download — no parent frame) ════════════════════════════════════════════════════════════════ */ function buildStandaloneBundle(startIndex, allPages, cssText, jsText) { var pagesData = {}; allPages.forEach(function(p) { var titleMatch = p.html.match(/]*>([\s\S]*?)<\/title>/i); var bodyMatch = p.html.match(/]*>([\s\S]*?)<\/body>/i); pagesData[p.name] = { title: titleMatch ? titleMatch[1] : "", body: bodyMatch ? bodyMatch[1] : p.html }; }); var startPage = allPages[startIndex].name; var startData = pagesData[startPage]; var doc = "\n\n\n" + "\n" + "\n" + "" + (startData.title || "My Website") + "\n"; if (cssText.trim()) doc += "\n"; doc += "\n\n" + startData.body + "\n"; doc += " \n"; return doc; } /* ════════════════════════════════════════════════════════════════ RUN ════════════════════════════════════════════════════════════════ */ function runCode() { // Flush active editor to pages array if (activeShared === null && pageEditors[activePageIndex]) { pages[activePageIndex].html = pageEditors[activePageIndex].getValue(); } clearTimeout(lintTimer); var errors = lintAll(); const iframe = $("preview-iframe"); const ph = $("preview-placeholder"); ph.style.display = "none"; ph.style.visibility = "hidden"; iframe.style.display = "block"; // Show current active page (or index.html if on CSS/JS/Images tab) var showIndex = (activeShared === null) ? activePageIndex : 0; previewPage = pages[showIndex].name; $("preview-page-indicator").textContent = previewPage; iframe.srcdoc = buildSrcdoc(showIndex, pages); if (errors.length) { setStatus("error", "Ran with warnings — check mascot"); } else { hideMascotHint(); setStatus("run", "Running\u2026"); iframe.onload = function() { setStatus("ok", "Ready \u2014 " + previewPage); try { $("previewSize").textContent = iframe.clientWidth + "\u00d7" + iframe.clientHeight + "px"; } catch(e) {} }; } } // Listen for link-navigation postMessages from iframe window.addEventListener("message", function(e) { if (!e.data || e.data.type !== "html-ide-navigate") return; var pageName = e.data.page; var idx = pages.findIndex(function(p) { return p.name === pageName; }); if (idx === -1) { showToast("Page not found: " + pageName, "error"); return; } // Switch editor to that page switchToPage(idx); // Update preview previewPage = pageName; $("preview-page-indicator").textContent = previewPage; $("preview-iframe").srcdoc = buildSrcdoc(idx, pages); setStatus("ok", "Navigated to " + pageName); }); /* ════════════════════════════════════════════════════════════════ NEW TAB — open full site with page switcher ════════════════════════════════════════════════════════════════ */ function openNewTab() { // Show active page (or index) — bundled with all pages so internal links work var showIndex = (activeShared === null) ? activePageIndex : 0; var cssText = cssEditor ? cssEditor.getValue() : ""; var jsText = jsEditor ? jsEditor.getValue() : ""; var doc = buildStandaloneBundle(showIndex, pages, cssText, jsText); var w = window.open("about:blank", "_blank"); if (!w) { showToast("Pop-up blocked", "error", 4000); return; } w.document.open(); w.document.write(doc); w.document.close(); } /* ════════════════════════════════════════════════════════════════ DOWNLOAD — combined HTML file for active page ════════════════════════════════════════════════════════════════ */ function downloadProject() { var showIndex = (activeShared === null) ? activePageIndex : 0; var cssText = cssEditor ? cssEditor.getValue() : ""; var jsText = jsEditor ? jsEditor.getValue() : ""; var doc = buildStandaloneBundle(showIndex, pages, cssText, jsText); var blob = new Blob([doc], { type: "text/html" }); var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (activeProgram ? activeProgram.name.replace(/[^a-z0-9_-]/gi, "_") : "site") + ".html"; a.click(); URL.revokeObjectURL(a.href); showToast("Downloaded site (all pages, links work offline)", "ok"); } /* ════════════════════════════════════════════════════════════════ LOAD FILE ════════════════════════════════════════════════════════════════ */ function loadFile() { var inp = $("fileInput"); inp.value = ""; inp.onchange = function() { var file = inp.files[0]; if (!file) return; var ext = file.name.split(".").pop().toLowerCase(); var reader = new FileReader(); reader.onload = function(e) { var content = e.target.result; if (ext === "css") { if (cssEditor) { cssEditor.setValue(content); switchToShared("css"); } } else if (ext === "js") { if (jsEditor) { jsEditor.setValue(content); switchToShared("js"); } } else { // Load as a new page (or replace current page) if (activeShared === null) { setPageEditorValue(activePageIndex, content); } else { // Add as new page if (pages.length < MAX_PAGES) { pages.push({ name: file.name, html: content }); createPageEditor(pages.length - 1); renderPageTabs(); switchToPage(pages.length - 1); } else { showToast(PAGE_LIMIT_MSG, "error", 4000); return; } } } markDirty(); autosave(); showToast("Loaded " + file.name, "ok"); }; reader.readAsText(file); }; inp.click(); } /* ════════════════════════════════════════════════════════════════ IMAGES PANEL ════════════════════════════════════════════════════════════════ */ function renderImagesPanel() { var grid = $("images-grid"); var empty = $("images-empty"); grid.innerHTML = ""; var totalItems = IMAGE_LIBRARY.reduce(function(sum, cat) { return sum + cat.items.length; }, 0); if (!totalItems) { empty.style.display = "flex"; return; } empty.style.display = "none"; IMAGE_LIBRARY.forEach(function(cat) { if (!cat.items.length) return; var header = document.createElement("div"); header.className = "images-category-header"; header.textContent = cat.category; grid.appendChild(header); cat.items.forEach(function(img) { var card = document.createElement("div"); card.className = "image-card"; card.title = "Click to copy tag\nDrag to insert into editor"; card.draggable = true; var thumb = document.createElement("img"); thumb.src = IMAGE_BASE + img.file; thumb.alt = img.label; thumb.loading = "lazy"; thumb.onerror = function() { card.classList.add("img-error"); thumb.style.display = "none"; }; var label = document.createElement("div"); label.className = "image-card-label"; label.textContent = img.label; var size = document.createElement("div"); size.className = "image-card-size"; size.textContent = img.w + "×" + img.h; card.appendChild(thumb); card.appendChild(label); card.appendChild(size); var imgTag = '' + img.label + ''; // Click: copy tag card.addEventListener("click", function() { navigator.clipboard.writeText(imgTag).then(function() { card.classList.add("copied"); showToast("Copied! Paste into your HTML with Ctrl+V", "ok", 3000); setTimeout(function() { card.classList.remove("copied"); }, 2000); }).catch(function() { showToast("Copy failed — try dragging instead", "error"); }); }); // Drag: insert at cursor card.addEventListener("dragstart", function(e) { e.dataTransfer.setData("text/plain", imgTag); e.dataTransfer.effectAllowed = "copy"; }); grid.appendChild(card); }); }); } // Drop onto editor pane $("editor-pane").addEventListener("dragover", function(e) { if (e.dataTransfer.types.includes("text/plain")) e.preventDefault(); }); $("editor-pane").addEventListener("drop", function(e) { e.preventDefault(); var text = e.dataTransfer.getData("text/plain"); if (!text || !text.startsWith("'; return; } var programs = r.programs || []; programCount = programs.length; programLimit = r.max_programs || 50; if (programCount > 0) { quota.style.display = "block"; $("programsQuotaLabel").textContent = programCount + " of " + programLimit + " projects used"; $("programsQuotaCount").textContent = (programLimit - programCount) + " remaining"; var bar = $("programsQuotaBar"); bar.style.width = Math.min(100, (programCount/programLimit)*100) + "%"; bar.style.background = programCount >= programLimit ? "#ff7b72" : "#7c3aed"; } else { quota.style.display = "none"; } if (!programs.length) { list.innerHTML = '
No saved projects yet.
'; return; } programs.forEach(function(p) { var row = document.createElement("div"); row.className = "program-item" + (activeProgram && activeProgram.id === p.id ? " active-prog" : ""); var dateStr = p.updated ? new Date(p.updated).toLocaleDateString() : ""; var pageCount = ""; try { var pd = JSON.parse(p.pages_json || "{}"); pageCount = pd.pages ? (pd.pages.length + " page" + (pd.pages.length !== 1 ? "s" : "")) : ""; } catch(e) {} row.innerHTML = '
' + p.name + '
' + '
' + (pageCount ? pageCount + "  ·  " : "") + dateStr + '
' + '
'; row.querySelector(".prog-btn.load").onclick = async function() { $("programsModal").style.display = "none"; var r2 = await apiCall("html_load_program", { id: p.id }); if (r2.error) { showToast("Failed to load: " + r2.error, "error"); return; } var prog = r2.program; var projectData; try { projectData = JSON.parse(prog.pages_json || "{}"); } catch(e) { projectData = {}; } var d = { pages: (projectData.pages && projectData.pages.length) ? projectData.pages : [{ name: "index.html", html: DEFAULT_INDEX_HTML }], css: projectData.css || DEFAULT_CSS, js: projectData.js || DEFAULT_JS }; loadSiteData(d); activeProgram = { id: p.id, name: p.name }; isDirty = false; updateBadge(); autosave(); showToast('Loaded "' + p.name + '"', "ok"); setStatus("ok", "Ready"); }; row.querySelector(".prog-btn.del").onclick = async function(e) { e.stopPropagation(); if (!await showConfirm('Delete "' + p.name + '"? This cannot be undone.')) return; await apiCall("html_delete_program", { id: p.id }); renderProgramsList(await apiCall("list_programs")); if (activeProgram && activeProgram.id === p.id) { activeProgram = null; isDirty = false; updateBadge(); } }; list.appendChild(row); }); } async function saveProject(forceName) { if (!DB_ENABLED) { showToast("Save/load requires a DC360 account", "info", 3000); return; } var name = forceName; if (!name) { if (activeProgram) { var r = await apiCall("html_save_program", { id: activeProgram.id, name: activeProgram.name, pages_json: JSON.stringify(buildProject()) }); if (r.error) { showToast("Save failed: " + r.error, "error"); return; } isDirty = false; updateBadge(); showToast('Saved "' + activeProgram.name + '"', "ok"); return; } name = prompt("Project name:", "My Website"); } if (!name || !name.trim()) return; name = name.trim(); if (programCount >= programLimit) { showToast("Project limit reached (" + programLimit + ")", "error", 3000); return; } var r = await apiCall("html_save_program", { name: name, pages_json: JSON.stringify(buildProject()) }); if (r.error) { showToast("Save failed: " + r.error, "error"); return; } activeProgram = { id: r.id, name: name }; programCount++; isDirty = false; updateBadge(); showToast('Saved "' + name + '"', "ok"); } /* ════════════════════════════════════════════════════════════════ LINTER ════════════════════════════════════════════════════════════════ */ function lintAll() { var errors = []; // Lint active page HTML if (activeShared === null && pageEditors[activePageIndex]) { var he = lintHTML(pageEditors[activePageIndex].getValue()); if (he.length) errors.push(he[0]); } if (!errors.length && cssEditor) { var ce = lintCSS(cssEditor.getValue()); if (ce.length) errors.push(ce[0]); } if (!errors.length && jsEditor) { var je = lintJS(jsEditor.getValue()); if (je.length) errors.push(je[0]); } if (errors.length) { showMascotHint(errors[0], 9000); setStatus("error", errors[0].length > 60 ? errors[0].slice(0,57)+"..." : errors[0]); } else { hideMascotHint(); setStatus("ok", "Ready"); } return errors; } function scheduleLint() { clearTimeout(lintTimer); clearTimeout(tipTimer); lintTimer = setTimeout(function() { lintAll(); scheduleTip(); }, 1200); } function lintHTML(src) { if (!src.trim()) return []; var errs = []; try { var parser = new DOMParser(); var doc = parser.parseFromString(src, "text/html"); var pe = doc.querySelector("parsererror"); if (pe) { errs.push("HTML parse error: " + pe.textContent.split("\n")[0].trim().slice(0,120)); return errs; } } catch(e) {} var voidTags = {area:1,base:1,br:1,col:1,embed:1,hr:1,img:1,input:1,link:1,meta:1,param:1,source:1,track:1,wbr:1}; var stripped = src.replace(//g, ""); var openRe = /<([a-z][a-z0-9]*)(\s[^>]*)?\s*(?!\/)>/gi, closeRe = /<\/([a-z][a-z0-9]*)\s*>/gi; var stack = [], m; openRe.lastIndex = 0; while ((m = openRe.exec(stripped)) !== null) { var t = m[1].toLowerCase(); if (!voidTags[t]) stack.push(t); } closeRe.lastIndex = 0; while ((m = closeRe.exec(stripped)) !== null) { var ct = m[1].toLowerCase(); var idx = stack.lastIndexOf(ct); if (idx !== -1) stack.splice(idx,1); } if (stack.length) { errs.push("HTML: Unclosed tag(s) <" + stack.filter(function(v,i,a){return a.indexOf(v)===i;}).join(", ") + ">. Every opening tag needs a closing tag."); return errs; } if (/]*\balt\s*=)[^>]*>/i.test(src)) { errs.push("HTML: An is missing its alt attribute. Add alt=\"description\" for accessibility."); return errs; } if (!/^\s* at the top."); return errs; } if (/href\s*=\s*["']\s*["']/i.test(src)) { errs.push('HTML: An tag has an empty href="/". Add a URL or use href="#".'); return errs; } return errs; } function lintCSS(src) { if (!src.trim()) return []; var errs = [], opens = 0, closes = 0; for (var ci = 0; ci < src.length; ci++) { if (src[ci]==="{") opens++; else if (src[ci]==="}") closes++; } if (opens !== closes) { errs.push("CSS: Unbalanced braces — " + opens + " opening { but " + closes + " closing }."); return errs; } var rules = src.replace(/\/\*[\s\S]*?\*\//g, ""); var cssLines = rules.split("\n"); for (var li = 0; li < cssLines.length; li++) { var trimmed = cssLines[li].trim(); if (/^[a-z-]+\s*:[^/]/.test(trimmed) && !/;\s*$/.test(trimmed) && trimmed.indexOf("{")===-1 && trimmed.indexOf("}")===-1 && trimmed!=="") { errs.push("CSS: Missing semicolon after \"" + trimmed.split(":")[0].trim() + "\". Each property needs to end with ;"); return errs; } } return errs; } function getFriendlyJSError(msg) { if (msg.indexOf("Unexpected end of input")!==-1||msg.indexOf("Unexpected end of script")!==-1) return "JS: Missing a closing } or ). Check all your brackets are balanced."; if (msg.indexOf("Unexpected token '}'")!==-1) return "JS: Extra }. You may have one too many closing braces."; if (msg.indexOf("Unexpected token '{'")!==-1) return "JS: Unexpected {. Check your if/function/loop syntax."; if (msg.indexOf("Unexpected identifier")!==-1) return "JS: Unexpected word — you may be missing a comma or operator on the previous line."; if (msg.indexOf("is not defined")!==-1) { var nm=msg.match(/(\w+) is not defined/); return "JS: " + (nm ? '"'+nm[1]+'"' : "A variable") + " is not defined. Check the spelling or declare it first."; } if (msg.indexOf("is not a function")!==-1) { var fn=msg.match(/(\S+) is not a function/); return "JS: " + (fn?fn[1]:"Something") + " is not a function. Check the method name spelling."; } if (msg.indexOf("Cannot read propert")!==-1) return "JS: Trying to use a property of something that doesn't exist. Check your element ID is correct."; return null; } function lintJS(src) { if (!src.trim()) return []; var errs = []; try { new Function(src); } catch(e) { var raw = e && e.message ? e.message : "Syntax error"; errs.push(getFriendlyJSError(raw) || ("JS: " + raw)); return errs; } if (/document\.getElementByID\s*\(/.test(src)) { errs.push("JS: Did you mean getElementById? It's Id not ID (capital I, lowercase d)."); return errs; } if (/console\.Log\s*\(/.test(src)) { errs.push("JS: Did you mean console.log? JavaScript is case-sensitive — lowercase l."); return errs; } if (/\.innerText\s*\(/.test(src)) { errs.push("JS: innerText is a property, not a method. Use = to set it, don't call it with ()."); return errs; } if (/[^=!<>]==[^=]/.test(src)) { errs.push("JS tip: Consider === instead of ==. Triple equals checks both value and type."); return errs; } return errs; } /* ════════════════════════════════════════════════════════════════ LINE COUNT ════════════════════════════════════════════════════════════════ */ function updateLineCount() { var ed = getActiveEditor(); if (!ed) { $("lineCount").textContent = ""; return; } var n = ed.lineCount(); $("lineCount").textContent = n + " line" + (n !== 1 ? "s" : ""); } /* ════════════════════════════════════════════════════════════════ SPLITTER (exact Python IDE port) ════════════════════════════════════════════════════════════════ */ function initSplitter() { var layout = $("ide-layout"); var hDragging = false; $("splitter").addEventListener("mousedown", function(e) { if (e.target === $("swapBtn") || e.target.closest("#swapBtn")) return; hDragging = true; document.body.style.cursor = isVertical ? "row-resize" : "col-resize"; e.preventDefault(); }); document.addEventListener("mouseup", function() { hDragging = false; document.body.style.cursor = ""; }); document.addEventListener("mousemove", function(e) { if (!hDragging) return; var rect = layout.getBoundingClientRect(); if (isVertical) { var minSide = 120; var top = Math.min(Math.max(e.clientY - rect.top, minSide), rect.height - minSide); layout.children[0].style.flex = "none"; layout.children[0].style.height = top + "px"; layout.children[2].style.flex = "1"; layout.children[2].style.height = ""; } else { var minSide2 = 200; var left = Math.min(Math.max(e.clientX - rect.left, minSide2), rect.width - minSide2); layout.style.gridTemplateColumns = left + "px 6px " + (rect.width - left - 6) + "px"; } getAllEditors().forEach(function(ed) { ed.refresh(); }); }, { passive: true }); var vDragging = false; $("heightSplitter").addEventListener("mousedown", function() { vDragging = true; document.body.style.cursor = "row-resize"; }); document.addEventListener("mouseup", function() { vDragging = false; document.body.style.cursor = ""; }); document.addEventListener("mousemove", function(e) { if (!vDragging) return; var rect = layout.getBoundingClientRect(); layout.style.height = Math.max(220, Math.min(window.innerHeight * 0.9, e.clientY - rect.top)) + "px"; getAllEditors().forEach(function(ed) { ed.refresh(); }); }, { passive: true }); new ResizeObserver(function() { getAllEditors().forEach(function(ed) { ed.refresh(); }); }).observe($("editor-pane")); } function swapPanes() { var layout = $("ide-layout"), ew = $("editor-wrapper"), cw = $("console-wrapper"), sp = $("splitter"); var lw = layout.children[0].getBoundingClientRect().width, rw = layout.children[2].getBoundingClientRect().width; panesSwapped = !panesSwapped; if (panesSwapped) { layout.appendChild(cw); layout.appendChild(sp); layout.appendChild(ew); } else { layout.appendChild(ew); layout.appendChild(sp); layout.appendChild(cw); } layout.style.gridTemplateColumns = rw + "px 6px " + lw + "px"; getAllEditors().forEach(function(ed) { ed.refresh(); }); } function toggleOrientation() { isVertical = !isVertical; var layout = $("ide-layout"), sp = $("splitter"); if (isVertical) { layout.style.gridTemplateColumns = ""; layout.style.gridTemplateRows = "1fr 6px 1fr"; layout.style.display = "flex"; layout.style.flexDirection = "column"; sp.style.width = "100%"; sp.style.height = "6px"; sp.style.cursor = "row-resize"; $("swapBtn").classList.add("vertical"); } else { layout.style.display = ""; layout.style.flexDirection = ""; layout.style.gridTemplateColumns = "75% 6px calc(25% - 6px)"; layout.style.gridTemplateRows = ""; sp.style.width = "6px"; sp.style.height = ""; sp.style.cursor = "col-resize"; $("swapBtn").classList.remove("vertical"); layout.children[0].style.height = ""; layout.children[0].style.flex = ""; layout.children[2].style.height = ""; layout.children[2].style.flex = ""; } getAllEditors().forEach(function(ed) { ed.refresh(); }); } /* ════════════════════════════════════════════════════════════════ FONT SIZE ════════════════════════════════════════════════════════════════ */ const FONT_SIZES = [11,12,13,14,15,16,17,18,20,22,24]; const FONT_LABELS = ["Tiny","Small","Normal","Normal","Large","Large","X-Large","X-Large","Huge","Huge","Max"]; function applyFontSize(val) { val = parseInt(val); var sz = (FONT_SIZES[val] || 14) + "px"; document.querySelectorAll(".CodeMirror").forEach(function(cm) { cm.style.fontSize = sz; }); getAllEditors().forEach(function(ed) { ed.refresh(); }); $("fontSzOut").textContent = FONT_LABELS[val] || "Normal"; } /* ════════════════════════════════════════════════════════════════ BUTTON SIZE ════════════════════════════════════════════════════════════════ */ const BUTTON_SIZES = [11,12,13,14,15,16,17,18,20,22,24]; const BUTTON_LABELS = ["Tiny","Small","Normal","Normal","Large","Large","X-Large","X-Large","Huge","Huge","Max"]; function applyButtonSize(val) { val = parseInt(val); var sz = (BUTTON_SIZES[val] || 14) + "px"; document.querySelectorAll("button.tb, select.tb").forEach(function(el) { el.style.fontSize = sz; }); $("btnSzOut").textContent = BUTTON_LABELS[val] || "Normal"; } /* ════════════════════════════════════════════════════════════════ EXAMPLES MODAL ════════════════════════════════════════════════════════════════ */ const EXAMPLES = [ { category: "HTML Structure", items: [ { label: "Basic Page Template", html: '\n\n\n \n My Page\n\n\n

Page Title

\n

Your content here.

\n \n', css: "", js: "" }, { label: "Navigation Bar", html: '\n\nNav Example\n\n
\n

Welcome

\n \n', css: 'nav { background: #1e6be6; padding: 12px 24px; }\nnav a { color: white; text-decoration: none; margin-right: 16px; font-weight: 600; }\nnav a:hover { text-decoration: underline; }', js: "" }, { label: "Hero Section", html: '\n\nHero\n\n
\n

Welcome to My Site

\n

A great tagline goes here.

\n Learn More\n
\n \n', css: 'body { margin: 0; font-family: sans-serif; }\n.hero { background: #1e3a5f; color: white; text-align: center; padding: 80px 20px; }\n.hero h1 { font-size: 2.5rem; margin-bottom: 16px; }\n.btn { background: #f59e0b; color: #1e3a5f; padding: 12px 28px; border-radius: 6px; text-decoration: none; font-weight: 700; }', js: "" }, ]}, { category: "CSS Layouts", items: [ { label: "Flexbox Card Row", html: '\n\nCards\n\n
\n

Card 1

Some text here.

\n

Card 2

Some text here.

\n

Card 3

Some text here.

\n
\n \n', css: 'body { font-family: sans-serif; padding: 20px; }\n.cards { display: flex; gap: 16px; flex-wrap: wrap; }\n.card { flex: 1; min-width: 180px; background: #f0f4f8; border-radius: 10px; padding: 20px; box-shadow: 0 2px 6px rgba(0,0,0,.1); }', js: "" }, { label: "CSS Grid Layout", html: '\n\nGrid\n\n
\n
Header
\n \n
Main Content
\n
Footer
\n
\n \n', css: 'body { margin: 0; font-family: sans-serif; }\n.layout { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: auto 1fr auto; min-height: 100vh; gap: 0; }\nheader { grid-column: 1 / -1; background: #1e6be6; color: white; padding: 16px 24px; font-size: 1.2rem; font-weight: 700; }\naside { background: #f8f9fa; padding: 20px; border-right: 1px solid #e5e7eb; }\nmain { padding: 24px; }\nfooter { grid-column: 1 / -1; background: #374151; color: white; padding: 12px 24px; text-align: center; }', js: "" }, ]}, { category: "JavaScript", items: [ { label: "Button Click", html: '\n\nButton\n\n \n

\n \n', css: 'body { font-family: sans-serif; padding: 30px; }\nbutton { padding: 10px 24px; background: #1e6be6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; }', js: 'document.getElementById("myBtn").addEventListener("click", function() {\n document.getElementById("msg").textContent = "You clicked the button!";\n});' }, { label: "To-Do List", html: '\n\nTo-Do\n\n

To-Do List

\n
\n \n \n
\n
    \n \n', css: 'body { font-family: sans-serif; max-width: 400px; margin: 30px auto; padding: 0 20px; }\n.input-row { display: flex; gap: 8px; margin-bottom: 16px; }\ninput { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }\nbutton { padding: 8px 16px; background: #1e6be6; color: white; border: none; border-radius: 4px; cursor: pointer; }\nul { list-style: none; padding: 0; }\nli { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: #f8f9fa; margin-bottom: 6px; border-radius: 6px; }\nli.done span { text-decoration: line-through; opacity: 0.5; }', js: 'function addTask() {\n var input = document.getElementById("taskInput");\n var text = input.value.trim();\n if (!text) return;\n var li = document.createElement("li");\n var span = document.createElement("span");\n span.textContent = text; span.style.flex = "1";\n span.onclick = function() { li.classList.toggle("done"); };\n var del = document.createElement("button");\n del.textContent = "x"; del.onclick = function() { li.remove(); };\n li.append(span, del);\n document.getElementById("taskList").appendChild(li);\n input.value = ""; input.focus();\n}' }, ]}, ]; function openExamplesModal() { var list = $("examplesList"); list.innerHTML = ""; EXAMPLES.forEach(function(cat) { var section = document.createElement("div"); var catHdr = document.createElement("div"); catHdr.className = "ex-category-header"; catHdr.innerHTML = '' + cat.category + ''; var catBody = document.createElement("div"); var catOpen = true; catHdr.onclick = function() { catOpen = !catOpen; catBody.style.display = catOpen ? "block" : "none"; catHdr.querySelector(".ex-category-arrow").style.transform = catOpen ? "rotate(90deg)" : ""; }; cat.items.forEach(function(ex) { var row = document.createElement("div"); var hdr = document.createElement("div"); hdr.className = "ex-item-header"; hdr.innerHTML = '' + ex.label + ''; var codeBlock = document.createElement("div"); codeBlock.className = "ex-item-code"; var activeExTab = "html"; var tabs = document.createElement("div"); tabs.className = "ex-tabs"; var pre = document.createElement("pre"); pre.className = "ex-pre"; pre.textContent = ex.html || ""; ["html","css","js"].forEach(function(t) { var btn = document.createElement("button"); btn.className = "ex-tab-btn" + (t === "html" ? " active" : ""); btn.textContent = t.toUpperCase(); btn.onclick = function() { tabs.querySelectorAll(".ex-tab-btn").forEach(function(b) { b.classList.remove("active"); }); btn.classList.add("active"); activeExTab = t; pre.textContent = ex[t] || "/* No " + t.toUpperCase() + " for this example */"; }; tabs.appendChild(btn); }); var copyRow = document.createElement("div"); copyRow.className = "ex-copy-row"; var loadBtn = document.createElement("button"); loadBtn.className = "ex-load-btn"; loadBtn.textContent = "Load into editor"; loadBtn.onclick = async function() { var linked = extractLinkedPageNames(ex.html || ""); var willCreate = linked.filter(function(n) { return n !== "index.html"; }).slice(0, MAX_PAGES - 1); var wontCreate = linked.filter(function(n) { return n !== "index.html"; }).slice(willCreate.length); var confirmMsg = 'Load "' + ex.label + '"? This clears all current pages, shared CSS, and JS and replaces them with this example.'; if (willCreate.length) { confirmMsg += ' It links to ' + willCreate.length + ' other page' + (willCreate.length !== 1 ? "s" : "") + ' (' + willCreate.join(", ") + ') — these will be created automatically.'; } if (wontCreate.length) { confirmMsg += ' Note: ' + wontCreate.join(", ") + ' can\'t be created (5-page limit reached).'; } var ok = await showConfirm(confirmMsg); if (!ok) return; // Wipe every existing page editor/panel, start fresh with just index.html document.querySelectorAll(".cm-panel[id^='cm-page-']").forEach(function(p) { p.remove(); }); pageEditors = []; pages = [{ name: "index.html", html: ex.html || "" }]; createPageEditor(0); if (ex.css && cssEditor) cssEditor.setValue(ex.css); else if (cssEditor) cssEditor.setValue(""); if (ex.js && jsEditor) jsEditor.setValue(ex.js); else if (jsEditor) jsEditor.setValue(""); willCreate.forEach(function(name) { createStubPage(name); }); activePageIndex = 0; activeShared = null; $("examplesModal").style.display = "none"; renderPageTabs(); switchToPage(0); markDirty(); autosave(); showToast('Loaded "' + ex.label + '"' + (willCreate.length ? " + created " + willCreate.length + " linked page(s)" : ""), "ok"); }; var copyBtn = document.createElement("button"); copyBtn.className = "ex-copy-btn"; copyBtn.textContent = "Copy"; copyBtn.onclick = function() { navigator.clipboard.writeText(ex[activeExTab] || "").then(function() { copyBtn.textContent = "Copied!"; copyBtn.classList.add("copied"); showToast("Copied " + activeExTab.toUpperCase() + " to clipboard", "ok"); setTimeout(function() { copyBtn.textContent = "Copy"; copyBtn.classList.remove("copied"); }, 2500); }); }; copyRow.append(loadBtn, copyBtn); codeBlock.append(tabs, pre, copyRow); var exOpen = false; hdr.onclick = function() { exOpen = !exOpen; codeBlock.classList.toggle("open", exOpen); hdr.querySelector(".ex-item-arrow").style.transform = exOpen ? "rotate(90deg)" : ""; }; row.append(hdr, codeBlock); catBody.appendChild(row); }); section.append(catHdr, catBody); list.appendChild(section); }); $("examplesModal").style.display = "flex"; } /* ════════════════════════════════════════════════════════════════ MAIN ════════════════════════════════════════════════════════════════ */ function main() { // Create shared CSS editor cssEditor = CodeMirror.fromTextArea($("cm-css"), { mode: "css", theme: "default", lineNumbers: true, lineWrapping: true, autoCloseBrackets: true, matchBrackets: true, extraKeys: { "Ctrl-Enter": runCode, "Ctrl-/": function(cm){cm.execCommand("toggleComment");}, "Tab": function(cm){if(cm.somethingSelected())cm.indentSelection("add");else cm.replaceSelection(" ");} } }); cssEditor.setValue(DEFAULT_CSS); cssEditor.on("change", function() { clearTimeout(autosaveTimer); autosaveTimer=setTimeout(autosave,800); hideMascotHint(); resetTipCountdown(); markDirty(); scheduleDbAutosave(); updateLineCount(); scheduleLint(); }); cssEditor.on("cursorActivity", updateLineCount); // Create shared JS editor jsEditor = CodeMirror.fromTextArea($("cm-js"), { mode: "javascript", theme: "default", lineNumbers: true, lineWrapping: true, autoCloseBrackets: true, matchBrackets: true, extraKeys: { "Ctrl-Enter": runCode, "Ctrl-/": function(cm){cm.execCommand("toggleComment");}, "Tab": function(cm){if(cm.somethingSelected())cm.indentSelection("add");else cm.replaceSelection(" ");} } }); jsEditor.setValue(DEFAULT_JS); jsEditor.on("change", function() { clearTimeout(autosaveTimer); autosaveTimer=setTimeout(autosave,800); hideMascotHint(); resetTipCountdown(); markDirty(); scheduleDbAutosave(); updateLineCount(); scheduleLint(); }); jsEditor.on("cursorActivity", updateLineCount); // Create page editors for initial pages pages.forEach(function(p, i) { createPageEditor(i); }); // Restore autosave restoreAutosave(); // Render tabs and show first page renderPageTabs(); showEditorPanel("cm-page-0-panel"); syncSharedTabActive(); if (pageEditors[0]) pageEditors[0].refresh(); updateLineCount(); setStatus("ok", "Ready"); // Tab bar events $("addPageBtn").onclick = function() { var name = prompt("New page filename (e.g. about.html):", "about.html"); if (name) addPage(name.trim()); }; $("tabCSS").onclick = function() { switchToShared("css"); }; $("tabJS").onclick = function() { switchToShared("js"); }; $("tabImages").onclick = function() { switchToShared("images"); }; // Toolbar $("runBtn").onclick = runCode; $("newTabBtn").onclick = openNewTab; $("downloadBtn").onclick = downloadProject; $("loadBtn").onclick = loadFile; $("examplesBtn").onclick = openExamplesModal; $("imagesBtn").onclick = function() { switchToShared("images"); }; $("undoBtn").onclick = function() { var ed=getActiveEditor(); if(ed) ed.undo(); }; $("redoBtn").onclick = function() { var ed=getActiveEditor(); if(ed) ed.redo(); }; $("orientBtn").onclick = toggleOrientation; $("swapBtn").onclick = swapPanes; $("dbSaveBtn").onclick = function() { saveProject(); }; $("programsBtn").onclick = openProgramsModal; $("newProgBtn").onclick = async function() { if (isDirty && !await showConfirm("Start a new project? Unsaved changes will be lost.")) return; document.querySelectorAll(".cm-panel[id^='cm-page-']").forEach(function(p){p.remove();}); pageEditors = []; pages = [{ name: "index.html", html: DEFAULT_INDEX_HTML }]; createPageEditor(0); cssEditor.setValue(DEFAULT_CSS); jsEditor.setValue(DEFAULT_JS); activeProgram = null; isDirty = false; updateBadge(); renderPageTabs(); activePageIndex = 0; activeShared = null; showEditorPanel("cm-page-0-panel"); syncSharedTabActive(); try { localStorage.removeItem("html-ide-unsaved-" + USER_ID); } catch(e) {} var iframe = $("preview-iframe"); iframe.style.display="none"; iframe.srcdoc=""; var ph = $("preview-placeholder"); ph.style.display="flex"; ph.style.visibility="visible"; setStatus("ok","Ready"); pageEditors[0].refresh(); showToast("New project started","ok"); }; // Programs modal $("programsClose").onclick = function() { $("programsModal").style.display="none"; }; $("programsModal").addEventListener("click", function(e) { if(e.target===$("programsModal")) $("programsModal").style.display="none"; }); $("programsSaveNewBtn").onclick = async function() { var name = $("programsNewName").value.trim(); if(!name) return; await saveProject(name); $("programsNewName").value=""; $("programsModal").style.display="none"; }; $("programsSearch").addEventListener("input", function(e) { var q = e.target.value.toLowerCase(); document.querySelectorAll(".program-item").forEach(function(row) { row.style.display = row.querySelector(".program-item-name").textContent.toLowerCase().includes(q) ? "" : "none"; }); }); // Examples + confirm modals $("examplesClose").onclick = function() { $("examplesModal").style.display="none"; }; $("examplesModal").addEventListener("click", function(e) { if(e.target===$("examplesModal")) $("examplesModal").style.display="none"; }); // Theme var savedTheme = localStorage.getItem("html-ide-theme") || "default"; $("themeSelect").value = savedTheme; applyTheme(savedTheme); $("themeSelect").onchange = function() { applyTheme(this.value); localStorage.setItem("html-ide-theme", this.value); }; // Font size var savedFont = localStorage.getItem("html-ide-fontSz"); if (savedFont !== null) { $("fontSz").value = savedFont; applyFontSize(savedFont); } else applyFontSize(2); $("fontSz").oninput = function() { applyFontSize(this.value); localStorage.setItem("html-ide-fontSz", this.value); }; // Button size var savedBtnSz = localStorage.getItem("html-ide-btnSz"); if (savedBtnSz !== null) { $("btnSz").value = savedBtnSz; applyButtonSize(savedBtnSz); } else applyButtonSize(2); $("btnSz").oninput = function() { applyButtonSize(this.value); localStorage.setItem("html-ide-btnSz", this.value); }; // Keyboard shortcuts document.addEventListener("keydown", function(e) { if (e.ctrlKey && e.key==="Enter") { e.preventDefault(); runCode(); } if (e.ctrlKey && e.key==="s") { e.preventDefault(); saveProject(); } }); // Splitter + guest locks initSplitter(); lockForGuests($("dbSaveBtn"), $("programsBtn")); // Guest gate function guestGate(e) { e.stopImmediatePropagation(); e.preventDefault(); showToast("This feature is only available with a DC360 subscription","info",3000); } function lockForGuests() { if (LOGGED_IN) return; Array.from(arguments).forEach(function(el) { el.classList.add("locked"); el.addEventListener("click",guestGate,true); }); } $("ide-root").style.visibility = "visible"; setTimeout(function() { getAllEditors().forEach(function(ed){ed.refresh();}); }, 100); scheduleTip(); } async function init() { await loadConfig(); main(); } init(); })();