Buckets:
AdhyanshVerma/data-gen-storage / exported_code /Q109_Build a responsive Virtual File Explorer interface /solution_110.html
| **index.html** | |
| ```html | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Virtual File Explorer</title> | |
| <link rel="stylesheet" href="styles.css" /> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <aside class="pane pane-tree" id="treePane"> | |
| <div class="pane-header"> | |
| <h2>Explorer</h2> | |
| <input type="search" id="searchInput" placeholder="Search files & folders…" autocomplete="off" /> | |
| </div> | |
| <div class="tree" id="tree" tabindex="0" role="tree" aria-label="File system tree"></div> | |
| </aside> | |
| <main class="pane pane-content" id="contentPane"> | |
| <div class="pane-header content-header"> | |
| <nav class="breadcrumbs" id="breadcrumbs" aria-label="Breadcrumb"></nav> | |
| <div class="actions"> | |
| <button id="newFileBtn" class="btn">+ New File</button> | |
| <button id="newFolderBtn" class="btn">+ New Folder</button> | |
| </div> | |
| </div> | |
| <div class="content" id="content"></div> | |
| </main> | |
| </div> | |
| <ul class="context-menu" id="contextMenu" role="menu" hidden> | |
| <li role="menuitem" data-action="rename">Rename</li> | |
| <li role="menuitem" data-action="delete">Delete</li> | |
| </ul> | |
| <script src="app.js"></script> | |
| </body> | |
| </html> | |
| ``` | |
| **styles.css** | |
| ```css | |
| :root{ | |
| --bg:#0f1115; | |
| --bg-2:#161922; | |
| --bg-3:#1d212c; | |
| --bg-hover:#232838; | |
| --border:#2a3040; | |
| --text:#e6e9ef; | |
| --text-dim:#8b93a7; | |
| --accent:#5b9dff; | |
| --accent-2:#7c5cff; | |
| --folder:#f5c043; | |
| --file:#5b9dff; | |
| --danger:#ff5d6c; | |
| --radius:10px; | |
| --shadow:0 10px 30px rgba(0,0,0,.35); | |
| --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%} | |
| body{ | |
| margin:0; | |
| font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| background:radial-gradient(1200px 600px at 80% -10%,rgba(124,92,255,.12),transparent 60%), | |
| radial-gradient(900px 500px at -10% 110%,rgba(91,157,255,.10),transparent 60%), | |
| var(--bg); | |
| color:var(--text); | |
| overflow:hidden; | |
| } | |
| .app{ | |
| display:grid; | |
| grid-template-columns:320px 1fr; | |
| height:100vh; | |
| gap:1px; | |
| background:var(--border); | |
| } | |
| .pane{ | |
| background:var(--bg-2); | |
| display:flex; | |
| flex-direction:column; | |
| min-width:0; | |
| min-height:0; | |
| } | |
| .pane-header{ | |
| padding:14px 16px; | |
| border-bottom:1px solid var(--border); | |
| display:flex; | |
| flex-direction:column; | |
| gap:10px; | |
| background:linear-gradient(180deg,rgba(255,255,255,.02),transparent); | |
| } | |
| .pane-header h2{ | |
| margin:0; | |
| font-size:13px; | |
| letter-spacing:.12em; | |
| text-transform:uppercase; | |
| color:var(--text-dim); | |
| } | |
| .content-header{ | |
| flex-direction:row; | |
| align-items:center; | |
| justify-content:space-between; | |
| gap:12px; | |
| flex-wrap:wrap; | |
| } | |
| #searchInput{ | |
| width:100%; | |
| padding:9px 12px; | |
| background:var(--bg-3); | |
| border:1px solid var(--border); | |
| border-radius:8px; | |
| color:var(--text); | |
| font-size:13px; | |
| outline:none; | |
| transition:border-color .15s, box-shadow .15s; | |
| } | |
| #searchInput:focus{ | |
| border-color:var(--accent); | |
| box-shadow:0 0 0 3px rgba(91,157,255,.18); | |
| } | |
| .tree{ | |
| overflow:auto; | |
| padding:8px 6px; | |
| font-size:13px; | |
| line-height:1.9; | |
| outline:none; | |
| flex:1; | |
| } | |
| .tree-node{user-select:none} | |
| .tree-row{ | |
| display:flex; | |
| align-items:center; | |
| gap:6px; | |
| padding:4px 8px; | |
| border-radius:6px; | |
| cursor:pointer; | |
| color:var(--text); | |
| white-space:nowrap; | |
| } | |
| .tree-row:hover{background:var(--bg-hover)} | |
| .tree-row.selected{background:rgba(91,157,255,.16);color:#fff} | |
| .tree-row.kb-focus{ | |
| background:rgba(124,92,255,.22); | |
| outline:1px solid rgba(124,92,255,.5); | |
| outline-offset:-1px; | |
| } | |
| .tree-row.hidden{display:none} | |
| .chevron{ | |
| width:14px; | |
| display:inline-flex; | |
| justify-content:center; | |
| color:var(--text-dim); | |
| transition:transform .15s ease; | |
| font-size:10px; | |
| } | |
| .chevron.open{transform:rotate(90deg)} | |
| .chevron.invisible{visibility:hidden} | |
| .node-icon{font-size:14px;width:16px;text-align:center} | |
| .icon-folder{color:var(--folder)} | |
| .icon-file{color:var(--file)} | |
| .tree-children{padding-left:14px;border-left:1px dashed var(--border);margin-left:10px} | |
| .tree-children.collapsed{display:none} | |
| .content{flex:1;overflow:auto;padding:20px 22px} | |
| .breadcrumbs{display:flex;flex-wrap:wrap;align-items:center;gap:4px;font-size:13px;min-height:24px} | |
| .crumb{ | |
| background:none;border:none;color:var(--text-dim); | |
| cursor:pointer;padding:4px 8px;border-radius:6px;font-size:13px; | |
| } | |
| .crumb:hover{background:var(--bg-hover);color:var(--text)} | |
| .crumb.current{color:var(--text);font-weight:600} | |
| .crumb-sep{color:var(--text-dim);opacity:.5} | |
| .actions{display:flex;gap:8px} | |
| .btn{ | |
| background:var(--bg-3); | |
| border:1px solid var(--border); | |
| color:var(--text); | |
| padding:7px 12px; | |
| border-radius:8px; | |
| font-size:12px; | |
| cursor:pointer; | |
| transition:all .15s; | |
| } | |
| .btn:hover{background:var(--bg-hover);border-color:var(--accent)} | |
| .btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent-2));border-color:transparent;color:#fff} | |
| .grid{ | |
| display:grid; | |
| grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); | |
| gap:14px; | |
| } | |
| .grid-item{ | |
| display:flex; | |
| flex-direction:column; | |
| align-items:center; | |
| gap:8px; | |
| padding:16px 8px; | |
| border-radius:var(--radius); | |
| background:var(--bg-3); | |
| border:1px solid transparent; | |
| cursor:pointer; | |
| text-align:center; | |
| transition:transform .12s, border-color .15s, background .15s; | |
| } | |
| .grid-item:hover{ | |
| transform:translateY(-2px); | |
| border-color:var(--accent); | |
| background:var(--bg-hover); | |
| } | |
| .grid-icon{font-size:34px;line-height:1} | |
| .grid-name{ | |
| font-size:12px; | |
| word-break:break-word; | |
| width:100%; | |
| line-height:1.3; | |
| } | |
| .grid-meta{font-size:10px;color:var(--text-dim);font-family:var(--mono)} | |
| .preview{ | |
| background:var(--bg-3); | |
| border:1px solid var(--border); | |
| border-radius:var(--radius); | |
| padding:28px; | |
| max-width:560px; | |
| box-shadow:var(--shadow); | |
| } | |
| .preview h3{margin:0 0 4px;font-size:18px;word-break:break-word} | |
| .preview .ext{color:var(--accent);font-family:var(--mono);font-size:13px} | |
| .preview .meta-row{ | |
| display:flex;justify-content:space-between; | |
| padding:10px 0;border-bottom:1px dashed var(--border);font-size:13px; | |
| } | |
| .preview .meta-row:last-of-type{border-bottom:none} | |
| .preview .placeholder{ | |
| margin-top:18px; | |
| border:2px dashed var(--border); | |
| border-radius:8px; | |
| padding:34px; | |
| text-align:center; | |
| color:var(--text-dim); | |
| font-size:13px; | |
| } | |
| .preview .placeholder .big{font-size:42px;display:block;margin-bottom:8px} | |
| .empty{color:var(--text-dim);font-size:13px;padding:30px;text-align:center} | |
| .context-menu{ | |
| position:fixed; | |
| z-index:1000; | |
| min-width:160px; | |
| background:var(--bg-3); | |
| border:1px solid var(--border); | |
| border-radius:8px; | |
| box-shadow:var(--shadow); | |
| padding:6px; | |
| margin:0; | |
| list-style:none; | |
| font-size:13px; | |
| } | |
| .context-menu li{ | |
| padding:8px 12px; | |
| border-radius:6px; | |
| cursor:pointer; | |
| } | |
| .context-menu li:hover{background:var(--bg-hover)} | |
| .context-menu li[data-action="delete"]:hover{background:rgba(255,93,108,.18);color:var(--danger)} | |
| .toast{ | |
| position:fixed; | |
| bottom:20px;left:50%; | |
| transform:translateX(-50%); | |
| background:var(--bg-3); | |
| border:1px solid var(--border); | |
| padding:10px 16px;border-radius:8px; | |
| font-size:13px;box-shadow:var(--shadow); | |
| animation:fade .25s ease; | |
| } | |
| @keyframes fade{from{opacity:0;transform:translate(-50%,8px)}to{opacity:1;transform:translate(-50%,0)}} | |
| @media (max-width:760px){ | |
| .app{grid-template-columns:1fr;grid-template-rows:45vh 1fr} | |
| .tree{max-height:none} | |
| } | |
| ``` | |
| **app.js** | |
| ```javascript | |
| /* ---------- Data ---------- */ | |
| const fileSystem = { | |
| name: "root", type: "folder", children: [ | |
| { name: "Documents", type: "folder", children: [ | |
| { name: "Resume.pdf", type: "file", extension: "pdf", size: 245678 }, | |
| { name: "CoverLetter.docx", type: "file", extension: "docx", size: 88120 }, | |
| { name: "Notes", type: "folder", children: [ | |
| { name: "ideas.txt", type: "file", extension: "txt", size: 2048 }, | |
| { name: "todo.md", type: "file", extension: "md", size: 1536 } | |
| ]} | |
| ]}, | |
| { name: "Pictures", type: "folder", children: [ | |
| { name: "vacation.jpg", type: "file", extension: "jpg", size: 1843200 }, | |
| { name: "sunset.png", type: "file", extension: "png", size: 982432 }, | |
| { name: "Screenshots", type: "folder", children: [ | |
| { name: "shot-2024.png", type: "file", extension: "png", size: 440000 } | |
| ]} | |
| ]}, | |
| { name: "Music", type: "folder", children: [ | |
| { name: "track01.mp3", type: "file", extension: "mp3", size: 5242880 }, | |
| { name: "track02.mp3", type: "file", extension: "mp3", size: 4980736 } | |
| ]}, | |
| { name: "Projects", type: "folder", children: [ | |
| { name: "webapp", type: "folder", children: [ | |
| { name: "index.html", type: "file", extension: "html", size: 4096 }, | |
| { name: "styles.css", type: "file", extension: "css", size: 6144 }, | |
| { name: "app.js", type: "file", extension: "js", size: 12288 } | |
| ]}, | |
| { name: "README.md", type: "file", extension: "md", size: 1024 } | |
| ]}, | |
| { name: "config.json", type: "file", extension: "json", size: 512 } | |
| ] | |
| }; | |
| /* ---------- State ---------- */ | |
| let nextId = 1; | |
| function assignIds(node, parent=null){ | |
| node.__id = nextId++; | |
| node.__parent = parent; | |
| if (node.children) node.children.forEach(c => assignIds(c, node)); | |
| } | |
| assignIds(fileSystem); | |
| const state = { | |
| expanded: new Set([fileSystem.__id]), | |
| selectedId: fileSystem.__id, // currently viewed folder | |
| kbFocusId: null, // keyboard-focused tree node | |
| previewNode: null, // file being previewed | |
| query: "" | |
| }; | |
| const byId = id => { | |
| let found = null; | |
| (function walk(n){ if(n.__id===id){found=n;return} if(n.children) n.children.forEach(walk); })(fileSystem); | |
| return found; | |
| }; | |
| /* ---------- Helpers ---------- */ | |
| function formatSize(b){ | |
| if (b == null) return "—"; | |
| const u=["B","KB","MB","GB"]; let i=0; | |
| while(b>=1024 && i<u.length-1){b/=1024;i++} | |
| return b.toFixed(b<10 && i>0?1:0)+" "+u[i]; | |
| } | |
| function iconFor(node){ | |
| if (node.type==="folder") return "📁"; | |
| const e=node.extension||""; | |
| const m={ | |
| pdf:"📄",docx:"📝",doc:"📝",txt:"📃",md:"📑", | |
| jpg:"🖼️",jpeg:"🖼️",png:"🖼️",gif:"🖼️",svg:"🖼️", | |
| mp3:"🎵",wav:"🎵",flac:"🎵", | |
| html:"🌐",css:"🎨",js:"⚙️",json:"🔧",zip:"🗜️" | |
| }; | |
| return m[e]||"📄"; | |
| } | |
| function pathTo(node){ | |
| const p=[]; let n=node; | |
| while(n){ p.unshift(n); n=n.__parent; } | |
| return p; | |
| } | |
| function isFolder(n){ return n.type==="folder"; } | |
| /* ---------- Tree rendering ---------- */ | |
| const treeEl = document.getElementById("tree"); | |
| function nodeMatches(node, q){ | |
| return node.name.toLowerCase().includes(q); | |
| } | |
| function subtreeHasMatch(node, q){ | |
| if (nodeMatches(node,q)) return true; | |
| if (node.children) return node.children.some(c=>subtreeHasMatch(c,q)); | |
| return false; | |
| } | |
| function renderTree(){ | |
| treeEl.innerHTML=""; | |
| renderNode(fileSystem, treeEl, 0); | |
| } | |
| function renderNode(node, container, depth){ | |
| const q = state.query.trim().toLowerCase(); | |
| if (q){ | |
| if (!subtreeHasMatch(node, q)) return; | |
| } | |
| const wrap = document.createElement("div"); | |
| wrap.className = "tree-node"; | |
| wrap.dataset.id = node.__id; | |
| const row = document.createElement("div"); | |
| row.className = "tree-row"; | |
| row.dataset.id = node.__id; | |
| row.tabIndex = -1; | |
| if (state.selectedId === node.__id && isFolder(node)) row.classList.add("selected"); | |
| if (state.kbFocusId === node.__id) row.classList.add("kb-focus"); | |
| const chev = document.createElement("span"); | |
| chev.className = "chevron"; | |
| if (isFolder(node) && node.children && node.children.length){ | |
| chev.textContent = "▶"; | |
| if (state.expanded.has(node.__id)) chev.classList.add("open"); | |
| } else { | |
| chev.classList.add("invisible"); | |
| } | |
| const ic = document.createElement("span"); | |
| ic.className = "node-icon " + (isFolder(node)?"icon-folder":"icon-file"); | |
| ic.textContent = iconFor(node); | |
| const nm = document.createElement("span"); | |
| nm.className = "node-name"; | |
| nm.textContent = node.name; | |
| row.appendChild(chev); row.appendChild(ic); row.appendChild(nm); | |
| wrap.appendChild(row); | |
| // events | |
| row.addEventListener("click", e=>{ | |
| e.stopPropagation(); | |
| if (isFolder(node)){ | |
| toggleExpand(node); | |
| selectFolder(node); | |
| } else { | |
| selectFile(node); | |
| } | |
| }); | |
| row.addEventListener("contextmenu", e=>{ | |
| e.preventDefault(); | |
| showContextMenu(e.clientX, e.clientY, node); | |
| }); | |
| if (isFolder(node) && node.children){ | |
| const childWrap = document.createElement("div"); | |
| childWrap.className = "tree-children"; | |
| if (!state.expanded.has(node.__id)) childWrap.classList.add("collapsed"); | |
| node.children.forEach(c => renderNode(c, childWrap, depth+1)); | |
| wrap.appendChild(childWrap); | |
| } | |
| container.appendChild(wrap); | |
| } | |
| function toggleExpand(node){ | |
| if (!isFolder(node)) return; | |
| if (state.expanded.has(node.__id)) state.expanded.delete(node.__id); | |
| else state.expanded.add(node.__id); | |
| renderTree(); | |
| } | |
| /* ---------- Selection ---------- */ | |
| function selectFolder(node){ | |
| state.selectedId = node.__id; | |
| state.previewNode = null; | |
| state.kbFocusId = node.__id; | |
| renderTree(); | |
| renderContent(); | |
| } | |
| function selectFile(node){ | |
| state.previewNode = node; | |
| state.kbFocusId = node.__id; | |
| renderTree(); | |
| renderContent(); | |
| } | |
| /* ---------- Content / breadcrumb / grid / preview ---------- */ | |
| const contentEl = document.getElementById("content"); | |
| const crumbsEl = document.getElementById("breadcrumbs"); | |
| function renderContent(){ | |
| // breadcrumbs | |
| crumbsEl.innerHTML=""; | |
| const target = state.previewNode || byId(state.selectedId) || fileSystem; | |
| const path = pathTo(target); | |
| path.forEach((n, i)=>{ | |
| const b = document.createElement("button"); | |
| b.className = "crumb" + (i===path.length-1?" current":""); | |
| b.textContent = n.name; | |
| b.addEventListener("click", ()=>{ | |
| if (isFolder(n)) selectFolder(n); | |
| }); | |
| crumbsEl.appendChild(b); | |
| if (i < path.length-1){ | |
| const sep = document.createElement("span"); | |
| sep.className="crumb-sep"; sep.textContent="/"; | |
| crumbsEl.appendChild(sep); | |
| } | |
| }); | |
| contentEl.innerHTML=""; | |
| if (state.previewNode && state.previewNode.type==="file"){ | |
| renderFilePreview(state.previewNode); | |
| return; | |
| } | |
| const folder = byId(state.selectedId) || fileSystem; | |
| if (!folder.children || !folder.children.length){ | |
| const e=document.createElement("div"); | |
| e.className="empty"; | |
| e.textContent="This folder is empty."; | |
| contentEl.appendChild(e); | |
| return; | |
| } | |
| const grid = document.createElement("div"); | |
| grid.className="grid"; | |
| folder.children.forEach(child=>{ | |
| const item = document.createElement("div"); | |
| item.className="grid-item"; | |
| item.dataset.id = child.__id; | |
| const ic = document.createElement("div"); | |
| ic.className="grid-icon"; | |
| ic.textContent = iconFor(child); | |
| const nm = document.createElement("div"); | |
| nm.className="grid-name"; | |
| nm.textContent = child.name; | |
| const mt = document.createElement("div"); | |
| mt.className="grid-meta"; | |
| mt.textContent = isFolder(child) ? "Folder" : formatSize(child.size); | |
| item.appendChild(ic); item.appendChild(nm); item.appendChild(mt); | |
| item.addEventListener("click", ()=>{ | |
| if (isFolder(child)){ | |
| state.expanded.add(child.__id); | |
| selectFolder(child); | |
| } else { | |
| selectFile(child); | |
| } | |
| }); | |
| item.addEventListener("contextmenu", e=>{ | |
| e.preventDefault(); | |
| showContextMenu(e.clientX, e.clientY, child); | |
| }); | |
| grid.appendChild(item); | |
| }); | |
| contentEl.appendChild(grid); | |
| } | |
| function renderFilePreview(node){ | |
| const wrap = document.createElement("div"); | |
| wrap.className="preview"; | |
| const h = document.createElement("h3"); | |
| h.textContent = node.name; | |
| const ext = document.createElement("div"); | |
| ext.className="ext"; | |
| ext.textContent = node.extension ? "."+node.extension : "no extension"; | |
| wrap.appendChild(h); wrap.appendChild(ext); | |
| const rows = [ | |
| ["Type","File"], | |
| ["Extension", node.extension || "—"], | |
| ["Size", formatSize(node.size)], | |
| ["Path", pathTo(node).map(n=>n.name).join(" / ")] | |
| ]; | |
| rows.forEach(([k,v])=>{ | |
| const r=document.createElement("div"); | |
| r.className="meta-row"; | |
| const k2=document.createElement("span"); k2.textContent=k; k2.style.color="var(--text-dim)"; | |
| const v2=document.createElement("span"); v2.textContent=v; v2.style.fontFamily="var(--mono)"; | |
| r.appendChild(k2); r.appendChild(v2); | |
| wrap.appendChild(r); | |
| }); | |
| const ph=document.createElement("div"); | |
| ph.className="placeholder"; | |
| ph.innerHTML = `<span class="big">${iconFor(node)}</span>No inline preview available for <strong>.${node.extension||"file"}</strong> files.`; | |
| wrap.appendChild(ph); | |
| contentEl.appendChild(wrap); | |
| } | |
| /* ---------- Search ---------- */ | |
| const searchInput = document.getElementById("searchInput"); | |
| searchInput.addEventListener("input", e=>{ | |
| state.query = e.target.value; | |
| if (state.query.trim()){ | |
| // auto-expand parents of matches | |
| (function expand(n){ | |
| if (subtreeHasMatch(n, state.query.trim().toLowerCase()) && isFolder(n)){ | |
| state.expanded.add(n.__id); | |
| if (n.children) n.children.forEach(expand); | |
| } | |
| })(fileSystem); | |
| } | |
| renderTree(); | |
| }); | |
| /* ---------- Keyboard navigation ---------- */ | |
| function visibleTreeNodes(){ | |
| const out=[]; | |
| function walk(node, parentExpanded){ | |
| if (!parentExpanded) return; | |
| const q = state.query.trim().toLowerCase(); | |
| if (q && !subtreeHasMatch(node,q)) return; | |
| out.push(node); | |
| if (isFolder(node) && node.children && state.expanded.has(node.__id)){ | |
| node.children.forEach(c=>walk(c, true)); | |
| } | |
| } | |
| walk(fileSystem, true); | |
| return out; | |
| } | |
| treeEl.addEventListener("keydown", e=>{ | |
| const visible = visibleTreeNodes(); | |
| if (!visible.length) return; | |
| let idx = visible.findIndex(n=>n.__id===state.kbFocusId); | |
| if (idx === -1) idx = 0; | |
| if (e.key === "ArrowDown"){ | |
| e.preventDefault(); | |
| idx = Math.min(visible.length-1, idx+1); | |
| state.kbFocusId = visible[idx].__id; | |
| renderTree(); scrollKbIntoView(); | |
| } else if (e.key === "ArrowUp"){ | |
| e.preventDefault(); | |
| idx = Math.max(0, idx-1); | |
| state.kbFocusId = visible[idx].__id; | |
| renderTree(); scrollKbIntoView(); | |
| } else if (e.key === "ArrowRight"){ | |
| e.preventDefault(); | |
| const n = visible[idx]; | |
| if (isFolder(n) && !state.expanded.has(n.__id)){ state.expanded.add(n.__id); renderTree(); } | |
| } else if (e.key === "ArrowLeft"){ | |
| e.preventDefault(); | |
| const n = visible[idx]; | |
| if (isFolder(n) && state.expanded.has(n.__id)){ state.expanded.delete(n.__id); renderTree(); } | |
| else if (n.__parent){ state.kbFocusId = n.__parent.__id; renderTree(); scrollKbIntoView(); } | |
| } else if (e.key === "Enter"){ | |
| e.preventDefault(); | |
| const n = visible[idx]; | |
| if (isFolder(n)) selectFolder(n); | |
| else selectFile(n); | |
| } | |
| }); | |
| function scrollKbIntoView(){ | |
| const el = treeEl.querySelector(".kb-focus"); | |
| if (el) el.scrollIntoView({block:"nearest"}); | |
| } | |
| /* ---------- Context menu ---------- */ | |
| const ctxMenu = document.getElementById("contextMenu"); | |
| let ctxTarget = null; | |
| function showContextMenu(x,y,node){ | |
| ctxTarget = node; | |
| ctxMenu.hidden = false; | |
| ctxMenu.style.left = x+"px"; | |
| ctxMenu.style.top = y+"px"; | |
| // keep in viewport | |
| const r = ctxMenu.getBoundingClientRect(); | |
| if (r.right > innerWidth) ctxMenu.style.left = (innerWidth - r.width - 8)+"px"; | |
| if (r.bottom > innerHeight) ctxMenu.style.top = (innerHeight - r.height - 8)+"px"; | |
| } | |
| function hideContextMenu(){ ctxMenu.hidden = true; ctxTarget = null; } | |
| document.addEventListener("click", hideContextMenu); | |
| document.addEventListener("contextmenu", e=>{ | |
| if (!e.target.closest(".tree-row") && !e.target.closest(".grid-item")) hideContextMenu(); | |
| }); | |
| ctxMenu.addEventListener("click", e=>{ | |
| const li = e.target.closest("li[data-action]"); | |
| if (!li || !ctxTarget) return; | |
| const action = li.dataset.action; | |
| const node = ctxTarget; | |
| hideContextMenu(); | |
| if (action==="rename") doRename(node); | |
| else if (action==="delete") doDelete(node); | |
| }); | |
| function doRename(node){ | |
| const v = prompt("Rename to:", node.name); | |
| if (v && v.trim()){ | |
| node.name = v.trim(); | |
| renderTree(); renderContent(); | |
| toast(`Renamed to "${node.name}"`); | |
| } | |
| } | |
| function doDelete(node){ | |
| if (!confirm(`Delete "${node.name}"?`)) return; | |
| const parent = node.__parent; | |
| if (!parent || !parent.children) return; | |
| parent.children = parent.children.filter(c=>c.__id!==node.__id); | |
| if (state.selectedId === node.__id) state.selectedId = parent.__id; | |
| if (state.previewNode && state.previewNode.__id === node.__id) state.previewNode = null; | |
| if (state.kbFocusId === node.__id) state.kbFocusId = parent.__id; | |
| renderTree(); renderContent(); | |
| toast(`Deleted "${node.name}"`); | |
| } | |
| /* ---------- Create new ---------- */ | |
| document.getElementById("newFileBtn").addEventListener("click", ()=>{ | |
| const folder = byId(state.selectedId) || fileSystem; | |
| if (!isFolder(folder)){ toast("Select a folder first."); return; } | |
| const name = prompt("New file name (with extension):", "untitled.txt"); | |
| if (!name || !name.trim()) return; | |
| const parts = name.trim().split("."); | |
| const ext = parts.length>1 ? parts.pop() : ""; | |
| const node = { name: parts.join("."), type:"file", extension:ext, size:0 }; | |
| node.__parent = folder; | |
| node.__id = nextId++; | |
| folder.children = folder.children || []; | |
| folder.children.push(node); | |
| state.expanded.add(folder.__id); | |
| renderTree(); renderContent(); | |
| toast(`Created file "${name}"`); | |
| }); | |
| document.getElementById("newFolderBtn").addEventListener("click", ()=>{ | |
| const folder = byId(state.selectedId) || fileSystem; | |
| if (!isFolder(folder)){ toast("Select a folder first."); return; } | |
| const name = prompt("New folder name:", "New Folder"); | |
| if (!name || !name.trim()) return; | |
| const node = { name: name.trim(), type:"folder", children:[] }; | |
| node.__parent = folder; | |
| node.__id = nextId++; | |
| folder.children = folder.children || []; | |
| folder.children.push(node); | |
| state.expanded.add(folder.__id); | |
| renderTree(); renderContent(); | |
| toast(`Created folder "${name}"`); | |
| }); | |
| /* ---------- Toast ---------- */ | |
| let toastTimer; | |
| function toast(msg){ | |
| document.querySelectorAll(".toast").forEach(t=>t.remove()); | |
| const t = document.createElement("div"); | |
| t.className="toast"; t.textContent=msg; | |
| document.body.appendChild(t); | |
| clearTimeout(toastTimer); | |
| toastTimer = setTimeout(()=>t.remove(), 2200); | |
| } | |
| /* ---------- Init ---------- */ | |
| renderTree(); | |
| renderContent(); | |
| treeEl.focus(); | |
| ``` |
Xet Storage Details
- Size:
- 22.5 kB
- Xet hash:
- 900992f563101d6577a24d271d0fcdd1aa101ad34994ecfdd7d2c767d81f27e6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.